main_user_id.ino INO FILE
// ============================================
// KODE ESP32 - Si-ShoPint (PROFESSIONAL UI & AUDIO) - DENGAN PWM BUZZER
// ============================================
// UPDATE: Tampilan OLED profesional
// UPDATE: Buzzer pasif dengan PWM menggunakan LEDC ESP32
// UPDATE: Variasi nada yang berbeda untuk setiap notifikasi
// ============================================

#include <WiFi.h>
#include <HTTPClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>

// ============================================
// OLED CONFIG
// ============================================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// ============================================
// HARDWARE PINS
// ============================================
#define RST_PIN 27
#define SS_PIN 5
#define BUZZER_PIN 4
#define LED_PIN 16

// PWM Configuration untuk buzzer
#define BUZZER_CHANNEL 0      // Channel PWM 0
#define BUZZER_RESOLUTION 8   // 8-bit resolution (0-255)
#define BUZZER_FREQ_BASE 1000 // Base frequency (akan diubah sesuai nada)

// ============================================
// BUZZER FREQUENCIES (Hz) untuk buzzer pasif
// ============================================
#define NOTE_C3  131
#define NOTE_D3  147
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_G3  196
#define NOTE_A3  220
#define NOTE_B3  247
#define NOTE_C4  262
#define NOTE_D4  294
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_G4  392
#define NOTE_A4  440
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_D5  587
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_G5  784
#define NOTE_A5  880
#define NOTE_B5  988
#define NOTE_C6  1047
#define NOTE_CS6 1109
#define NOTE_D6  1175
#define NOTE_DS6 1245
#define NOTE_E6  1319
#define NOTE_F6  1397
#define NOTE_FS6 1480
#define NOTE_G6  1568
#define NOTE_GS6 1661
#define NOTE_A6  1760
#define NOTE_AS6 1865
#define NOTE_B6  1976

// ============================================
// RFID
// ============================================
MFRC522 mfrc522(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;

// ============================================
// WIFI & TIME
// ============================================
const char* ssid = "Galapagos";
const char* password = "@Galapagos123";
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 25200); // GMT+7

// ============================================
// API CONFIG
// ============================================
const char* serverUrl = "https://zidcreative.com/sishopint/api/presensi.php";
const char* kegiatanUrl = "https://zidcreative.com/sishopint/api/kegiatan.php";
const int USER_ID = 4;
const char* LEMBAGA_NAME = "TPQ Al-Munawwarah";

// ============================================
// STRUKTUR DATA KEGIATAN
// ============================================
#define MAX_KEGIATAN 20

struct Kegiatan {
  int id;
  String nama;
  int jamMulai;
  int menitMulai;
  int jamSelesai;
  int menitSelesai;
  int poin;
  bool aktif;
};

Kegiatan daftarKegiatan[MAX_KEGIATAN];
int jumlahKegiatan = 0;
unsigned long lastKegiatanUpdate = 0;
const unsigned long KEGIATAN_UPDATE_INTERVAL = 3600000; // Update setiap 1 jam

// ============================================
// VARIABEL
// ============================================
String currentTimestamp = "";
int dailySholatCount = 0;
int dailyKegiatanCount = 0;
bool wifiConnected = false;

// ============================================
// FUNGSI PWM BUZZER UNTUK BUZZER PASIF
// ============================================

// Inisialisasi PWM untuk buzzer
void initBuzzerPWM() {
  // Setup channel PWM dengan frekuensi default
  ledcSetup(BUZZER_CHANNEL, BUZZER_FREQ_BASE, BUZZER_RESOLUTION);
  ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
  ledcWrite(BUZZER_CHANNEL, 0); // Matikan buzzer
}

// Fungsi untuk membunyikan buzzer dengan frekuensi dan durasi tertentu
void playBuzzerTone(int frequency, int duration) {
  if (frequency <= 0) {
    ledcWrite(BUZZER_CHANNEL, 0);
    return;
  }
  
  // Set frekuensi PWM
  ledcSetup(BUZZER_CHANNEL, frequency, BUZZER_RESOLUTION);
  ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
  
  // Set duty cycle 50% untuk volume maksimal
  int dutyCycle = (1 << (BUZZER_RESOLUTION - 1)) - 1; // 127 untuk 8-bit
  ledcWrite(BUZZER_CHANNEL, dutyCycle);
  
  // Tunggu selama durasi yang ditentukan
  delay(duration);
  
  // Matikan buzzer
  ledcWrite(BUZZER_CHANNEL, 0);
}

// Fungsi untuk memainkan melodi dengan beberapa nada
void playMelody(int frequencies[], int durations[], int count) {
  for (int i = 0; i < count; i++) {
    if (frequencies[i] > 0) {
      playBuzzerTone(frequencies[i], durations[i]);
      delay(durations[i] * 0.1); // Jeda kecil antar nada
    } else {
      delay(durations[i]); // Rest/silence
    }
  }
}

// ============================================
// FUNGSI TONE CUSTOM DENGAN PWM UNTUK BUZZER PASIF
// ============================================

// Nada startup - melodi naik (C4-E4-G4-C5)
void startupTone() {
  int freqs[] = {NOTE_C4, NOTE_E4, NOTE_G4, NOTE_C5};
  int durs[] = {150, 150, 150, 400};
  
  for (int i = 0; i < 4; i++) {
    playBuzzerTone(freqs[i], durs[i]);
    delay(20);
  }
}

// Nada kartu terdeteksi - double beep cepat (A5-A5)
void cardDetectedTone() {
  playBuzzerTone(NOTE_A5, 60);
  delay(30);
  playBuzzerTone(NOTE_A5, 60);
  delay(30);
}

// Nada sukses - melodi ceria (C5-E5-G5-C6)
void successTone() {
  int freqs[] = {NOTE_C5, NOTE_E5, NOTE_G5, NOTE_C6};
  int durs[] = {100, 100, 100, 300};
  
  for (int i = 0; i < 4; i++) {
    playBuzzerTone(freqs[i], durs[i]);
    delay(20);
  }
}

// Nada error - melodi turun (C5-B4-A4-G4)
void errorTone() {
  int freqs[] = {NOTE_C5, NOTE_B4, NOTE_A4, NOTE_G4};
  int durs[] = {120, 120, 120, 300};
  
  for (int i = 0; i < 4; i++) {
    playBuzzerTone(freqs[i], durs[i]);
    delay(20);
  }
}

// Nada warning - sirene pendek (A4-F4-A4-F4)
void warningTone() {
  int freqs[] = {NOTE_A4, NOTE_F4, NOTE_A4, NOTE_F4};
  int durs[] = {150, 150, 150, 300};
  
  for (int i = 0; i < 4; i++) {
    playBuzzerTone(freqs[i], durs[i]);
    delay(20);
  }
}

// Nada kartu invalid - tiga nada pendek menurun (C4-B3-A3-G3)
void invalidCardTone() {
  int freqs[] = {NOTE_C4, NOTE_B3, NOTE_A3, NOTE_G3};
  int durs[] = {80, 80, 80, 250};
  
  for (int i = 0; i < 4; i++) {
    playBuzzerTone(freqs[i], durs[i]);
    delay(15);
  }
}

// Nada WiFi sukses - dua nada naik (E4-G4-C5)
void wifiSuccessTone() {
  playBuzzerTone(NOTE_E4, 100);
  delay(20);
  playBuzzerTone(NOTE_G4, 100);
  delay(20);
  playBuzzerTone(NOTE_C5, 200);
  delay(20);
}

// Nada WiFi gagal - dua nada sama panjang (C4-C4)
void wifiFailedTone() {
  playBuzzerTone(NOTE_C4, 250);
  delay(30);
  playBuzzerTone(NOTE_C4, 250);
  delay(30);
}

// Nada reconnect - satu nada naik pendek (G4-C5)
void reconnectTone() {
  playBuzzerTone(NOTE_G4, 100);
  delay(20);
  playBuzzerTone(NOTE_C5, 200);
  delay(20);
}

// Nada data loaded - tiga nada staccato (C5-E5-G5)
void dataLoadedTone() {
  playBuzzerTone(NOTE_C5, 60);
  delay(10);
  playBuzzerTone(NOTE_E5, 60);
  delay(10);
  playBuzzerTone(NOTE_G5, 60);
  delay(10);
  playBuzzerTone(NOTE_C6, 200);
  delay(10);
}

// Nada duplicate - dua nada sama dengan jeda (F4-F4)
void duplicateTone() {
  playBuzzerTone(NOTE_F4, 200);
  delay(30);
  playBuzzerTone(NOTE_F4, 200);
  delay(30);
}

// ============================================
// FUNGSI KONTROL BUZZER PRO (DARI SERVER)
// ============================================
void kontrolBuzzerPro(String buzzerType) {
  if (buzzerType == "SHORT_BEEP") {
    // Single beep pendek - A5
    playBuzzerTone(NOTE_A5, 100);
    
  } else if (buzzerType == "LONG_BEEP") {
    // Long beep - G4 panjang
    playBuzzerTone(NOTE_G4, 600);
    
  } else if (buzzerType == "DOUBLE_BEEP") {
    // Double beep - dua nada berbeda (C5-E5)
    playBuzzerTone(NOTE_C5, 100);
    delay(20);
    playBuzzerTone(NOTE_E5, 100);
    
  } else if (buzzerType == "CONTINUOUS") {
    // Continuous beep - alarm naik turun (C5-G5-C5-G5)
    for(int i = 0; i < 3; i++) {
      playBuzzerTone(NOTE_C5, 200);
      delay(20);
      playBuzzerTone(NOTE_G5, 200);
      delay(20);
    }
  }
}

// ============================================
// FUNGSI TEST BUZZER UNTUK DEBUG
// ============================================
void testBuzzer() {
  Serial.println("Testing buzzer with PWM...");
  
  int testFreqs[] = {262, 330, 392, 523, 659, 784, 880, 1047};
  const char* noteNames[] = {"C4", "E4", "G4", "C5", "E5", "G5", "A5", "C6"};
  
  for (int i = 0; i < 8; i++) {
    Serial.print("Playing ");
    Serial.print(noteNames[i]);
    Serial.print(" (");
    Serial.print(testFreqs[i]);
    Serial.println(" Hz)");
    
    playBuzzerTone(testFreqs[i], 300);
    delay(350);
  }
  
  Serial.println("Playing startup melody:");
  startupTone();
  
  Serial.println("Playing success melody:");
  successTone();
  
  Serial.println("Buzzer test complete");
}

// ============================================
// SETUP
// ============================================
void setup() {
  Serial.begin(115200);
  Serial.println("\n=== Si-ShoPint v2.0 (Dengan PWM Buzzer) ===");
  
  // Init OLED
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("OLED failed");
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  
  // Init RFID
  SPI.begin();
  mfrc522.PCD_Init();
  for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;
  
  // Init pins
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  
  // Init PWM untuk buzzer
  initBuzzerPWM();
  
  // Test buzzer (opsional, bisa di-comment jika tidak perlu)
  // testBuzzer();
  
  showSplashScreen();
  
  // Connect WiFi
  connectWiFi();
  
  // Init time
  timeClient.begin();
  timeClient.update();
  updateTime();
  
  // Ambil jadwal kegiatan dari server
  if (wifiConnected) {
    ambilJadwalKegiatan();
  } else {
    showOfflineMode();
    delay(2000);
  }
  
  showHomeScreen();
  startupTone(); // Startup tone
}

// ============================================
// LOOP
// ============================================
void loop() {
  static unsigned long lastTimeUpdate = 0;
  static unsigned long lastWifiCheck = 0;
  
  unsigned long now = millis();
  
  // Update time setiap 30 detik
  if (now - lastTimeUpdate >= 30000) {
    timeClient.update();
    updateTime();
    lastTimeUpdate = now;
  }
  
  // Cek WiFi setiap 10 detik
  if (now - lastWifiCheck >= 10000) {
    checkWiFi();
    lastWifiCheck = now;
  }
  
  // Update jadwal kegiatan setiap 1 jam
  if (wifiConnected && (now - lastKegiatanUpdate >= KEGIATAN_UPDATE_INTERVAL)) {
    ambilJadwalKegiatan();
    lastKegiatanUpdate = now;
  }
  
  showHomeScreen();
  
  // Check RFID
  if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
    cardDetectedTone();
    processCard();
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
  }
  
  delay(100);
}

// ============================================
// FUNGSI WIFI
// ============================================
void connectWiFi() {
  showConnectingScreen();
  
  WiFi.begin(ssid, password);
  int attempts = 0;
  
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    Serial.print(".");
    attempts++;
    
    if (attempts % 4 == 0) {
      showWiFiProgress(attempts);
    }
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    wifiConnected = true;
    Serial.println("\nWiFi OK");
    Serial.print("IP: ");
    Serial.println(WiFi.localIP());
    wifiSuccessTone();
    showWiFiSuccess();
    delay(1000);
  } else {
    wifiConnected = false;
    Serial.println("\nWiFi GAGAL");
    wifiFailedTone();
    showOfflineMode();
    delay(2000);
  }
}

void checkWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    if (!wifiConnected) {
      wifiConnected = true;
      Serial.println("WiFi Reconnected");
      reconnectTone();
      ambilJadwalKegiatan();
    }
  } else {
    if (wifiConnected) {
      wifiConnected = false;
      Serial.println("WiFi Disconnected");
    }
  }
}

// ============================================
// FUNGSI AMBIL JADWAL KEGIATAN
// ============================================
void ambilJadwalKegiatan() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi offline, tidak bisa ambil jadwal");
    return;
  }
  
  showLoadingScreen("Muat Jadwal", "Kegiatan...");
  Serial.println("Mengambil jadwal kegiatan dari server...");
  
  String token = "2ce9d827ba3116b3152c82aa1782aaf772ea4855949551ef8c37b6fec9eb05bb";
  
  String url = String(kegiatanUrl) + 
               "?user_id=" + USER_ID + 
               "&status=aktif" + 
               "&limit=50" + 
               "&token=" + token;
  
  HTTPClient http;
  http.begin(url);
  http.addHeader("Accept", "application/json");
  http.setTimeout(10000);
  
  int httpCode = http.GET();
  
  if (httpCode == 200) {
    String response = http.getString();
    
    if (parseJadwalKegiatan(response)) {
      Serial.print("Berhasil memuat ");
      Serial.print(jumlahKegiatan);
      Serial.println(" kegiatan");
      
      dataLoadedTone();
      showDataLoaded(String(jumlahKegiatan) + " kegiatan");
      delay(1000);
    } else {
      Serial.println("Gagal parse jadwal kegiatan");
      errorTone();
      showErrorPro("Parse Error", "Gagal parse");
      delay(1000);
    }
  } else {
    Serial.print("HTTP Error: ");
    Serial.println(httpCode);
    errorTone();
    showErrorPro("HTTP Error", String(httpCode));
    delay(1000);
  }
  
  http.end();
  lastKegiatanUpdate = millis();
}

// ============================================
// PARSE JADWAL KEGIATAN
// ============================================
bool parseJadwalKegiatan(String json) {
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, json);
  
  if (error) {
    Serial.print("JSON Error: ");
    Serial.println(error.c_str());
    return false;
  }
  
  jumlahKegiatan = 0;
  
  if (!doc["success"].as<bool>()) {
    Serial.println("API return error");
    return false;
  }
  
  JsonArray kegiatanArray = doc["data"]["kegiatan"].as<JsonArray>();
  
  for (JsonObject keg : kegiatanArray) {
    if (jumlahKegiatan >= MAX_KEGIATAN) break;
    
    int id = keg["id"].as<int>();
    const char* nama = keg["nama_kegiatan"] | "Unknown";
    const char* waktuMulai = keg["waktu_mulai"] | "00:00";
    const char* waktuSelesai = keg["waktu_selesai"] | "00:00";
    int poin = keg["poin"].as<int>() | 5;
    const char* status = keg["status"] | "aktif";
    
    int jamMulai = String(waktuMulai).substring(0, 2).toInt();
    int menitMulai = String(waktuMulai).substring(3, 5).toInt();
    int jamSelesai = String(waktuSelesai).substring(0, 2).toInt();
    int menitSelesai = String(waktuSelesai).substring(3, 5).toInt();
    
    daftarKegiatan[jumlahKegiatan].id = id;
    daftarKegiatan[jumlahKegiatan].nama = String(nama);
    daftarKegiatan[jumlahKegiatan].jamMulai = jamMulai;
    daftarKegiatan[jumlahKegiatan].menitMulai = menitMulai;
    daftarKegiatan[jumlahKegiatan].jamSelesai = jamSelesai;
    daftarKegiatan[jumlahKegiatan].menitSelesai = menitSelesai;
    daftarKegiatan[jumlahKegiatan].poin = poin;
    daftarKegiatan[jumlahKegiatan].aktif = (String(status) == "aktif");
    
    jumlahKegiatan++;
  }
  
  return (jumlahKegiatan > 0);
}

// ============================================
// FUNGSI CEK KEGIATAN
// ============================================
int cekKegiatanSekarang(int jam, int menit, String &namaKegiatan) {
  int waktuSekarang = jam * 60 + menit;
  
  for (int i = 0; i < jumlahKegiatan; i++) {
    if (!daftarKegiatan[i].aktif) continue;
    
    int waktuMulai = daftarKegiatan[i].jamMulai * 60 + daftarKegiatan[i].menitMulai;
    int waktuSelesai = daftarKegiatan[i].jamSelesai * 60 + daftarKegiatan[i].menitSelesai;
    
    if (waktuSekarang >= waktuMulai && waktuSekarang <= waktuSelesai + 5) {
      namaKegiatan = daftarKegiatan[i].nama;
      return daftarKegiatan[i].id;
    }
  }
  
  return 0;
}

// ============================================
// FUNGSI RFID
// ============================================
void processCard() {
  Serial.println("\nCard detected!");
  showProcessing();
  
  String id_jari = readBlock(5);
  String nama = readBlock(4);
  
  if (id_jari.length() == 0) {
    Serial.println("Invalid card");
    invalidCardTone();
    showInvalidCard();
    beep(2);
    return;
  }
  
  if (nama.length() == 0) {
    nama = "Peserta " + id_jari.substring(0, 4);
  }
  
  Serial.print("ID Jari: ");
  Serial.println(id_jari);
  Serial.print("Nama: ");
  Serial.println(nama);
  
  updateTime();
  
  sendToServer(id_jari, nama);
}

String readBlock(int block) {
  byte buffer[18];
  byte size = sizeof(buffer);
  String result = "";
  
  MFRC522::StatusCode status = mfrc522.PCD_Authenticate(
    MFRC522::PICC_CMD_MF_AUTH_KEY_A, 
    block, 
    &key, 
    &(mfrc522.uid)
  );
  
  if (status != MFRC522::STATUS_OK) {
    return result;
  }
  
  status = mfrc522.MIFARE_Read(block, buffer, &size);
  if (status != MFRC522::STATUS_OK) {
    return result;
  }
  
  for (byte i = 0; i < 16; i++) {
    if (buffer[i] != 0x00 && buffer[i] != 0xFF) {
      result += (char)buffer[i];
    }
  }
  
  result.trim();
  return result;
}

// ============================================
// FUNGSI KIRIM KE SERVER
// ============================================
void sendToServer(String id_jari, String nama) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnected");
    wifiFailedTone();
    showErrorPro("WiFi", "Putus");
    beep(2);
    return;
  }
  
  int jam = currentTimestamp.substring(11, 13).toInt();
  int menit = currentTimestamp.substring(14, 16).toInt();
  
  String namaKegiatanTerdeteksi = "";
  int kegiatanId = cekKegiatanSekarang(jam, menit, namaKegiatanTerdeteksi);
  
  JsonDocument doc;
  doc["id_jari"] = id_jari;
  doc["nama"] = nama;
  doc["timestamp"] = currentTimestamp;
  doc["user_id"] = USER_ID;
  
  if (kegiatanId > 0) {
    doc["tipe"] = "kegiatan";
    doc["kegiatan_id"] = kegiatanId;
    Serial.println("✓ Mengirim sebagai PRESENSI KEGIATAN");
  } else {
    doc["tipe"] = "sholat";
    Serial.println("✓ Mengirim sebagai PRESENSI SHOLAT");
  }
  
  String json;
  serializeJson(doc, json);
  
  HTTPClient http;
  http.begin(serverUrl);
  http.addHeader("Content-Type", "application/json");
  
  int httpCode = http.POST(json);
  
  if (httpCode > 0) {
    String response = http.getString();
    processResponse(response, nama, kegiatanId, namaKegiatanTerdeteksi);
  } else {
    Serial.println("HTTP Error");
    errorTone();
    showErrorPro("Server", "Error");
    beep(3);
  }
  
  http.end();
}
// ============================================
// FUNGSI PROCESS RESPONSE - DIUPDATE
// ============================================
void processResponse(String response, String nama, int kegiatanId, String namaKegiatan) {
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, response);
  
  if (error) {
    Serial.print("JSON Error: ");
    Serial.println(error.c_str());
    errorTone();
    showErrorPro("Respon", "Error");
    beep(3);
    return;
  }
  
  bool success = doc["success"].as<bool>();
  String errorCode = doc["error_code"] | "";
  
  JsonObject feedback = doc["device_feedback"].as<JsonObject>();
  String ledColor = feedback["led_color"] | "";
  String buzzerType = feedback["buzzer"] | "";
  String displayMsg = feedback["display_message"] | "";
  
  kontrolLED(ledColor);
  kontrolBuzzerPro(buzzerType);
  
  if (success) {
    JsonObject data = doc["data"].as<JsonObject>();
    String tipe = data["tipe"] | "sholat";
    int poin = data["poin"].as<int>() | 0;
    int peserta_id = data["peserta_id"].as<int>() | 0;
    
    // AMBIL TOTAL POIN SANTRI
    int total_poin = -1;
    if (peserta_id > 0) {
      total_poin = getTotalPoinSantri(peserta_id);
    }
    
    if (tipe == "kegiatan") {
      String namaKegiatanRes = data["nama_kegiatan"] | (namaKegiatan.length() > 0 ? namaKegiatan : "Kegiatan");
      String statusPresensi = data["status"] | "hadir";
      String waktuMulai = data["waktu_mulai"] | "";
      String waktuSelesai = data["waktu_selesai"] | "";
      
      dailyKegiatanCount++;
      
      if (displayMsg.length() > 0) {
        // NEW: Panggil fungsi dengan total_poin
        showSuccessKegiatanPro(nama, namaKegiatanRes, displayMsg, poin, waktuMulai, waktuSelesai, total_poin);
      } else {
        String statusText = (statusPresensi == "hadir") ? "Hadir" : "Terlambat";
        // NEW: Panggil fungsi dengan total_poin
        showSuccessKegiatanPro(nama, namaKegiatanRes, statusText, poin, waktuMulai, waktuSelesai, total_poin);
      }
      
    } else {
      String sholat = data["sholat"] | "Unknown";
      String statusPresensi = data["status"] | "Unknown";
      
      dailySholatCount++;
      
      if (displayMsg.length() > 0) {
        // NEW: Panggil fungsi dengan total_poin
        showSuccessSholatPro(nama, sholat, displayMsg, poin, total_poin);
      } else {
        // NEW: Panggil fungsi dengan total_poin
        showSuccessSholatPro(nama, sholat, statusPresensi, poin, total_poin);
      }
    }
    
  } else {
    handleErrorPro(errorCode, doc, namaKegiatan);
  }
}

// ============================================
// FUNGSI KONTROL HARDWARE
// ============================================
void kontrolLED(String ledColor) {
  if (ledColor == "GREEN") {
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
    digitalWrite(LED_PIN, LOW);
  } else if (ledColor == "RED") {
    for(int i = 0; i < 3; i++) {
      digitalWrite(LED_PIN, HIGH);
      delay(200);
      digitalWrite(LED_PIN, LOW);
      delay(200);
    }
  } else if (ledColor == "YELLOW") {
    for(int i = 0; i < 2; i++) {
      digitalWrite(LED_PIN, HIGH);
      delay(300);
      digitalWrite(LED_PIN, LOW);
      delay(300);
    }
  }
}

// ============================================
// FUNGSI HANDLE ERROR PRO
// ============================================
void handleErrorPro(String errorCode, JsonDocument &doc, String namaKegiatan) {
  JsonObject data = doc["data"].as<JsonObject>();
  
  Serial.print("Error: ");
  Serial.println(errorCode);
  
  if (errorCode == "DUPLICATE_ABSENSI") {
    duplicateTone();
    String tipe = data["tipe"] | "sholat";
    
    if (tipe == "kegiatan") {
      String namaKegiatanErr = data["nama_kegiatan"] | (namaKegiatan.length() > 0 ? namaKegiatan : "Kegiatan");
      String waktu = data["waktu_sebelumnya"] | "";
      showDuplicateKegiatanPro(namaKegiatanErr, waktu);
    } else {
      String sholat = data["sholat"] | "Sholat";
      String waktu = data["waktu_sebelumnya"] | "";
      showDuplicateSholatPro(sholat, waktu);
    }
    
  } else if (errorCode == "ABSENSI_DITOLAK") {
    warningTone();
    String keterangan = data["keterangan"] | "Diluar waktu";
    String sholat = data["sholat"] | "";
    String namaKegiatanErr = data["nama_kegiatan"] | "";
    String displayMsg = doc["device_feedback"]["display_message"] | "";
    
    showDitolakPro(sholat, namaKegiatanErr, keterangan, displayMsg);
    
  } else if (errorCode == "KEGIATAN_NOT_FOUND") {
    errorTone();
    showErrorPro("Kegiatan", "Tidak ditemukan");
    
  } else if (errorCode == "KEGIATAN_INACTIVE") {
    warningTone();
    showErrorPro("Kegiatan", "Non-aktif");
    
  } else {
    errorTone();
    String msg = doc["message"] | "Error";
    showErrorPro("Error", msg.substring(0, 12));
  }
}

// ============================================
// FUNGSI DISPLAY
// ============================================

void showSplashScreen() {
  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, WHITE);
  display.setTextSize(1);
  display.setCursor(25, 10);
  display.print("Si-ShoPint");
  display.setTextSize(2);
  display.setCursor(20, 25);
  display.print("v2.0");
  display.setTextSize(1);
  display.setCursor(15, 48);
  display.print(LEMBAGA_NAME);
  display.display();
  delay(2000);
}

void showHomeScreen() {
  display.clearDisplay();
  
  // Nama Lembaga
  display.setTextSize(1);
  display.setTextColor(WHITE);
  int lembagaWidth = strlen(LEMBAGA_NAME) * 6;
  int lembagaX = (128 - lembagaWidth) / 2;
  display.setCursor(lembagaX, 2);
  display.print(LEMBAGA_NAME);
  display.drawLine(0, 12, 128, 12, WHITE);
  
  // Jam Digital
  display.setTextSize(3);
  String timeStr = currentTimestamp.substring(11, 16);
  int timeWidth = timeStr.length() * 18;
  int timeX = (128 - timeWidth) / 2;
  display.setCursor(timeX, 18);
  display.print(timeStr);
  
  // Tanggal
  display.setTextSize(1);
  int year = currentTimestamp.substring(0, 4).toInt();
  int month = currentTimestamp.substring(5, 7).toInt();
  int day = currentTimestamp.substring(8, 10).toInt();
  const char* bulanIndo[] = {"Januari", "Februari", "Maret", "April", "Mei", "Juni", 
                              "Juli", "Agustus", "September", "Oktober", "November", "Desember"};
  String tanggalFormat = String(day) + " " + String(bulanIndo[month-1]) + " " + String(year);
  int tanggalWidth = tanggalFormat.length() * 6;
  int tanggalX = (128 - tanggalWidth) / 2;
  display.setCursor(tanggalX, 40);
  display.print(tanggalFormat);
  
  // Info Sholat dan Kegiatan
  display.drawLine(0, 50, 128, 50, WHITE);
  display.setCursor(15, 55);
  display.print("S:");
  display.print(dailySholatCount);
  display.setCursor(60, 55);
  display.print("|");
  display.setCursor(75, 55);
  display.print("K:");
  display.print(dailyKegiatanCount);
  
  // WiFi indicator
  if (wifiConnected) {
    display.fillCircle(118, 6, 2, WHITE);
    display.drawLine(115, 9, 121, 9, WHITE);
    display.drawLine(112, 12, 124, 12, WHITE);
  } else {
    display.fillCircle(118, 6, 2, WHITE);
    display.drawLine(115, 9, 121, 9, WHITE);
    display.drawLine(112, 12, 124, 12, WHITE);
    display.drawLine(114, 4, 122, 8, WHITE);
    display.drawLine(114, 8, 122, 4, WHITE);
  }
  
  display.display();
}

String getDayName() {
  unsigned long epoch = timeClient.getEpochTime();
  struct tm *ptm = gmtime((time_t *)&epoch);
  int dayOfWeek = ptm->tm_wday;
  String days[] = {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"};
  return days[dayOfWeek];
}

void showConnectingScreen() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(20, 15);
  display.print("Menghubungkan");
  display.setCursor(35, 25);
  display.print("ke WiFi");
  for(int i = 0; i < 3; i++) {
    display.setCursor(50 + (i * 6), 35);
    display.print(".");
    display.display();
    delay(300);
  }
  display.display();
}

void showWiFiProgress(int attempts) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(25, 20);
  display.print("WiFi Connect");
  display.setCursor(30, 35);
  int progress = (attempts * 100) / 30;
  display.print(progress);
  display.print("%");
  display.drawRect(20, 45, 88, 8, WHITE);
  display.fillRect(22, 47, (progress * 84) / 100, 4, WHITE);
  display.display();
}

void showWiFiSuccess() {
  display.clearDisplay();
  display.fillRect(0, 0, 128, 64, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(2);
  display.setCursor(25, 15);
  display.print("WiFi OK");
  display.setTextSize(1);
  display.setCursor(15, 40);
  display.print(WiFi.localIP().toString());
  display.display();
}

void showOfflineMode() {
  display.clearDisplay();
  display.fillRect(0, 0, 128, 64, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  display.setCursor(25, 20);
  display.print("MODE OFFLINE");
  display.setCursor(20, 35);
  display.print("Terbatas");
  display.display();
}

void showLoadingScreen(String line1, String line2) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(25, 15);
  display.print(line1);
  display.setCursor(25, 25);
  display.print(line2);
  static int spinner = 0;
  const char* spinnerChars = "|/-\\";
  display.setCursor(60, 40);
  display.print(spinnerChars[spinner]);
  spinner = (spinner + 1) % 4;
  display.display();
}

void showDataLoaded(String info) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(25, 20);
  display.print("Data Dimuat");
  display.setCursor(25, 35);
  display.print(info);
  display.display();
}

void showProcessing() {
  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, WHITE);
  display.setTextSize(2);
  display.setCursor(30, 5);
  display.print("PROSES");
  display.setTextSize(1);
  display.setCursor(25, 25);
  display.print("Mohon tunggu");
  
  int centerX = 64;
  int centerY = 45;
  int radius = 10;
  display.drawCircle(centerX, centerY, radius, WHITE);
  
  static int angle = 0;
  float rad = angle * 3.14159 / 180;
  int dotX = centerX + radius * cos(rad);
  int dotY = centerY + radius * sin(rad);
  display.fillCircle(dotX, dotY, 2, WHITE);
  angle = (angle + 45) % 360;
  
  display.display();
}

void showInvalidCard() {
  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, WHITE);
  display.setTextSize(1);
  display.setCursor(30, 15);
  display.print("KARTU");
  display.setTextSize(2);
  display.setCursor(25, 28);
  display.print("INVALID");
  display.setTextSize(1);
  display.setCursor(25, 50);
  display.print("Coba lagi");
  display.display();
}

// ============================================
// SHOW SUCCESS SHOLAT PRO - UPDATED dengan TOTAL POIN
// ============================================
void showSuccessSholatPro(String nama, String sholat, String status, int poin, int total_poin) {
  // Slide 1: Nama dan Aktivitas (TIDAK BERUBAH)
  display.clearDisplay();
  display.fillRect(0, 0, 128, 14, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  int berhasilX = (128 - (8 * 6)) / 2;
  display.setCursor(berhasilX, 3);
  display.print("BERHASIL");
  display.setTextColor(WHITE);
  display.drawLine(0, 15, 128, 15, WHITE);
  
  // Nama (ukuran besar)
  display.setTextSize(2);
  display.setCursor(10, 20);
  display.print(nama.substring(0, 10));
  
  // Aktivitas/Sholat (ukuran besar)
  display.setTextSize(2);
  display.setCursor(10, 40);
  display.print(sholat.substring(0, 8));
  
  display.display();
  successTone();
  delay(2000);
  
  // ============================================
  // Slide 2: Status + Poin Didapat dan TOTAL POIN
  // ============================================
  display.clearDisplay();
  
  // Header "STATUS" dengan latar putih
  display.fillRect(0, 0, 128, 14, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  int statusHeaderX = (128 - (6 * 6)) / 2;
  display.setCursor(statusHeaderX, 3);
  display.print("STATUS");
  display.setTextColor(WHITE);
  display.drawLine(0, 15, 128, 15, WHITE);
  
  // Baris 1: Status dan Poin Didapat (seperti sebelumnya)
  display.setTextSize(2);
  
  String statusTampil = "";
  if (status == "tepat_waktu" || status == "Tepat Waktu" || status == "tepat") {
    statusTampil = "TEPAT";
  } 
  else if (status == "telat" || status == "Terlambat" || status == "terlambat") {
    statusTampil = "TELAT";
  } 
  else if (status == "terlalu_cepat" || status == "cepat" || status == "Terlalu Cepat") {
    statusTampil = "CEPAT";
  } 
  else {
    statusTampil = status.substring(0, 8);
  }
  
  // Format: "TEPAT +5"
  String baris1 = statusTampil + " +" + String(poin);
  int baris1Width = baris1.length() * 12;
  int baris1X = (128 - baris1Width) / 2;
  display.setCursor(baris1X, 22);
  display.print(baris1);
  
  // Baris 2: TOTAL POIN (BARU)
  display.setTextSize(1);
  String baris2 = "Poin : ";
  if (total_poin >= 0) {
    baris2 += String(total_poin);
  } else {
    baris2 += "--"; // Jika gagal ambil total poin
  }
  
  int baris2Width = baris2.length() * 6;
  int baris2X = (128 - baris2Width) / 2;
  display.setCursor(baris2X, 45);
  display.print(baris2);
  
  display.display();
  delay(2000);
}


// ============================================
// SHOW SUCCESS KEGIATAN PRO - UPDATED dengan TOTAL POIN
// ============================================
void showSuccessKegiatanPro(String nama, String kegiatan, String status, int poin, String waktuMulai, String waktuSelesai, int total_poin) {
  // Slide 1: Nama dan Aktivitas (TIDAK BERUBAH)
  display.clearDisplay();
  display.fillRect(0, 0, 128, 14, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  int berhasilX = (128 - (8 * 6)) / 2;
  display.setCursor(berhasilX, 3);
  display.print("BERHASIL");
  display.setTextColor(WHITE);
  display.drawLine(0, 15, 128, 15, WHITE);
  
  // Nama (ukuran besar)
  display.setTextSize(2);
  display.setCursor(10, 20);
  display.print(nama.substring(0, 10));
  
  // Kegiatan (ukuran besar)
  display.setTextSize(2);
  display.setCursor(10, 40);
  if (kegiatan.length() > 8) {
    display.print(kegiatan.substring(0, 8));
  } else {
    display.print(kegiatan);
  }
  
  display.display();
  successTone();
  delay(2000);
  
  // ============================================
  // Slide 2: Status + Poin Didapat dan TOTAL POIN
  // ============================================
  display.clearDisplay();
  display.fillRect(0, 0, 128, 14, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  int statusHeaderX = (128 - (6 * 6)) / 2;
  display.setCursor(statusHeaderX, 3);
  display.print("STATUS");
  display.setTextColor(WHITE);
  display.drawLine(0, 15, 128, 15, WHITE);
  
  // Baris 1: Status dan Poin Didapat
  display.setTextSize(2);
  
  String statusTampil = "";
  if (status == "hadir" || status == "Hadir") {
    statusTampil = "HADIR";
  } else if (status == "terlambat" || status == "Terlambat") {
    statusTampil = "TERLAMBAT";
  } else {
    statusTampil = status.substring(0, 8);
  }
  
  // Format: "HADIR +5" (poin sesuai setting kegiatan)
  String baris1 = statusTampil + " +" + String(poin);
  int baris1Width = baris1.length() * 12;
  int baris1X = (128 - baris1Width) / 2;
  display.setCursor(baris1X, 22);
  display.print(baris1);
  
  // Baris 2: TOTAL POIN (BARU)
  display.setTextSize(1);
  String baris2 = "Poin : ";
  if (total_poin >= 0) {
    baris2 += String(total_poin);
  } else {
    baris2 += "--"; // Jika gagal ambil total poin
  }
  
  int baris2Width = baris2.length() * 6;
  int baris2X = (128 - baris2Width) / 2;
  display.setCursor(baris2X, 45);
  display.print(baris2);
  
  // Waktu kegiatan (kecil, opsional) - TETAP ADA
  if (waktuMulai.length() > 0) {
    display.setTextSize(1);
    display.setCursor(5, 5);
    display.print(waktuMulai.substring(11, 16) + "-" + waktuSelesai.substring(11, 16));
  }
  
  display.display();
  delay(2000);
}

void showDuplicateSholatPro(String sholat, String waktu) {
  display.clearDisplay();
  display.fillRect(0, 0, 128, 64, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  display.setCursor(35, 5);
  display.print("SUDAH ABSEN");
  display.drawLine(0, 15, 128, 15, BLACK);
  display.setCursor(10, 22);
  display.print("Sholat: ");
  display.print(sholat);
  display.setCursor(10, 35);
  display.print("Terakhir absen pukul ");
  if (waktu.length() > 10) display.print(waktu.substring(11, 16));
  else display.print("--:--");
  display.display();
  duplicateTone();
  delay(3000);
}

void showDuplicateKegiatanPro(String kegiatan, String waktu) {
  display.clearDisplay();
  display.fillRect(0, 0, 128, 64, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(1);
  display.setCursor(35, 5);
  display.print("SUDAH ABSEN");
  display.drawLine(0, 15, 128, 15, BLACK);
  display.setCursor(10, 22);
  display.print("Kegiatan: ");
  display.print(kegiatan.substring(0, 14));
  display.setCursor(10, 38);
  display.print("Terakhir absen pukul ");
  if (waktu.length() > 10) display.print(waktu.substring(11, 16));
  else display.print("--:--");
  display.display();
  duplicateTone();
  delay(3000);
}

void showDitolakPro(String sholat, String kegiatan, String keterangan, String displayMsg) {
  display.clearDisplay();
  display.fillRect(0, 0, 128, 64, WHITE);
  display.setTextColor(BLACK);
  display.setTextSize(2);
  display.setCursor(25, 5);
  display.print("DITOLAK");
  display.drawLine(0, 25, 128, 25, BLACK);
  display.setTextSize(1);
  
  if (kegiatan.length() > 0) {
    display.setCursor(10, 30);
    display.print("Kegiatan:");
    display.setCursor(10, 40);
    display.print(kegiatan.substring(0, 14));
  } else if (sholat.length() > 0) {
    display.setCursor(10, 30);
    display.print("Sholat:");
    display.setCursor(10, 40);
    display.print(sholat);
  } else {
    display.setCursor(10, 35);
    display.print(keterangan.substring(0, 16));
  }
  
  if (displayMsg.length() > 0) {
    display.setCursor(10, 52);
    display.print(displayMsg.substring(0, 14));
  }
  
  display.display();
  warningTone();
  delay(3000);
}

void showErrorPro(String title, String msg) {
  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, WHITE);
  display.setTextSize(1);
  display.setCursor(45, 10);
  display.print(title);
  display.drawLine(20, 20, 108, 20, WHITE);
  display.setCursor(20, 30);
  display.print(msg);
  display.drawLine(100, 5, 115, 20, WHITE);
  display.drawLine(100, 20, 115, 5, WHITE);
  display.display();
}

// ============================================
// FUNGSI UTILITY
// ============================================
void updateTime() {
  String timeStr = timeClient.getFormattedTime();
  unsigned long epochTime = timeClient.getEpochTime();
  struct tm *ptm = gmtime((time_t *)&epochTime);
  
  int year = ptm->tm_year + 1900;
  int month = ptm->tm_mon + 1;
  int day = ptm->tm_mday;
  
  char buffer[20];
  sprintf(buffer, "%04d-%02d-%02d %s", year, month, day, timeStr.c_str());
  currentTimestamp = String(buffer);
}

void beep(int times) {
  for (int i = 0; i < times; i++) {
    playBuzzerTone(NOTE_C5, 100);
    digitalWrite(LED_PIN, HIGH);
    delay(100);
    digitalWrite(LED_PIN, LOW);
    if (i < times - 1) delay(100);
  }
}

// ============================================
// FUNGSI AMBIL TOTAL POIN SANTRI
// ============================================
int getTotalPoinSantri(int peserta_id) {
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi offline, tidak bisa ambil total poin");
        return -1;
    }
    
    String url = String("https://zidcreative.com/sishopint/api/get_total_poin.php?peserta_id=") + String(peserta_id);
    
    HTTPClient http;
    http.begin(url);
    http.setTimeout(3000);
    
    int httpCode = http.GET();
    int total_poin = -1;
    
    if (httpCode == 200) {
        String response = http.getString();
        JsonDocument doc;
        DeserializationError error = deserializeJson(doc, response);
        
        if (!error && doc["success"].as<bool>()) {
            total_poin = doc["total_poin"].as<int>();
            Serial.print("Total poin santri: ");
            Serial.println(total_poin);
        } else {
            Serial.println("Gagal parse response total poin");
        }
    } else {
        Serial.print("HTTP Error get total poin: ");
        Serial.println(httpCode);
    }
    
    http.end();
    return total_poin;
}