main_user_Id_LCD_I2C.ino INO FILE
// ============================================
// KODE ESP32 - Si-ShoPint (LCD 16x2 I2C VERSION)
// ============================================
// UI UPDATE: Tampilan sederhana dengan format:
// - Home: Nama Lembaga + Jam + WiFi
// - Success: BERHASIL! + Poin di kanan
// - Ditolak: DITOLAK! + Alasan jelas
// - Error: ERROR! + Keterangan
// - Splash: Si-ShoPint + by Innoiti
// ============================================
// PERANGKAT:
// - ESP32 30 pin (standar)
// - LCD 16x2 I2C (address 0x27 atau 0x3F)
// - MFRC522 RFID Module
// - Buzzer pasif dengan PWM
// ============================================

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

// ============================================
// LCD CONFIG
// ============================================
#define LCD_COLUMNS 16
#define LCD_ROWS 2
#define LCD_ADDRESS 0x27  // Coba 0x27, jika tidak 0x3F
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);

// ============================================
// HARDWARE PINS (ESP32 30 pin - AMAN)
// ============================================
#define RST_PIN 27        // RFID RST
#define SS_PIN 5          // RFID SS (SPI)
#define BUZZER_PIN 4      // Buzzer PWM
#define LED_PIN 16        // LED Indicator

// I2C untuk LCD: SDA=21, SCL=22 (default)
// SPI untuk RFID: MOSI=23, MISO=19, SCK=18 (default)

// ============================================
// BUZZER FREQUENCIES (Hz)
// ============================================
#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

// PWM Configuration
#define BUZZER_CHANNEL 0
#define BUZZER_RESOLUTION 8

// ============================================
// 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;

// ============================================
// VARIABEL
// ============================================
String currentTimestamp = "";
int dailySholatCount = 0;
int dailyKegiatanCount = 0;
bool wifiConnected = false;
unsigned long lastDisplayUpdate = 0;
int displayPage = 0;
bool cardDetected = false;
unsigned long cardProcessTime = 0;

// ============================================
// VARIABEL UNTUK SCROLLING
// ============================================
unsigned long lastScrollUpdate = 0;
int scrollPosition = 0;
const int SCROLL_INTERVAL = 300; // ms per gerakan
String displayText = "";

// ============================================
// FUNGSI PWM BUZZER
// ============================================

void initBuzzerPWM() {
  ledcSetup(BUZZER_CHANNEL, 1000, BUZZER_RESOLUTION);
  ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
  ledcWrite(BUZZER_CHANNEL, 0);
}

void playBuzzerTone(int frequency, int duration) {
  if (frequency <= 0) {
    ledcWrite(BUZZER_CHANNEL, 0);
    return;
  }
  
  ledcSetup(BUZZER_CHANNEL, frequency, BUZZER_RESOLUTION);
  ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
  
  int dutyCycle = (1 << (BUZZER_RESOLUTION - 1)) - 1;
  ledcWrite(BUZZER_CHANNEL, dutyCycle);
  
  delay(duration);
  ledcWrite(BUZZER_CHANNEL, 0);
}

// ============================================
// TONE FUNCTIONS
// ============================================

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);
  }
}

void cardDetectedTone() {
  playBuzzerTone(NOTE_A5, 60);
  delay(30);
  playBuzzerTone(NOTE_A5, 60);
  delay(30);
}

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);
  }
}

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);
  }
}

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);
  }
}

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);
  }
}

void wifiSuccessTone() {
  playBuzzerTone(NOTE_E4, 100);
  delay(20);
  playBuzzerTone(NOTE_G4, 100);
  delay(20);
  playBuzzerTone(NOTE_C5, 200);
  delay(20);
}

void wifiFailedTone() {
  playBuzzerTone(NOTE_C4, 250);
  delay(30);
  playBuzzerTone(NOTE_C4, 250);
  delay(30);
}

void duplicateTone() {
  playBuzzerTone(NOTE_F4, 200);
  delay(30);
  playBuzzerTone(NOTE_F4, 200);
  delay(30);
}

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 DISPLAY LCD 16x2 - UI BARU
// ============================================

// ----- UTILITY FUNCTIONS -----
String truncateText(String text, int maxLen = 10) {
  if (text.length() <= maxLen) return text;
  return text.substring(0, maxLen - 3) + "...";
}

void lcdPrintRight(int row, String text) {
  int padding = 16 - text.length();
  if (padding < 0) padding = 0;
  lcd.setCursor(padding, row);
  lcd.print(text);
}

// ----- 1. SPLASH SCREEN -----
void showSplashScreen() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Si-ShoPint");
  lcd.setCursor(0, 1);
  lcd.print("by Innoiti");
  delay(2000);
}

// ----- 2. HOME SCREEN (STANDBY) WITH SCROLLING -----
void showHomeScreen() {
    lcd.clear();
    
    // ============================================
    // BARIS 1: Nama Lembaga (SCROLLING)
    // ============================================
    String fullText = String(LEMBAGA_NAME);
    
    // Jika teks pendek (<= 16 karakter), tampilkan statis
    if (fullText.length() <= 16) {
        lcd.setCursor(0, 0);
        lcd.print(fullText);
    } else {
        // Tambahkan spasi di awal dan akhir untuk efek marquee
        String paddedText = "   " + fullText + "   ";
        
        // Update scroll position
        unsigned long now = millis();
        if (now - lastScrollUpdate >= SCROLL_INTERVAL) {
            scrollPosition++;
            // Reset ketika sudah mencapai akhir
            if (scrollPosition > fullText.length() + 3) {
                scrollPosition = 0;
            }
            lastScrollUpdate = now;
        }
        
        // Ambil 16 karakter dari posisi scroll
        String displayText = "";
        int maxLen = paddedText.length();
        for (int i = 0; i < 16; i++) {
            int idx = scrollPosition + i;
            if (idx >= maxLen) {
                idx = idx - maxLen;
            }
            displayText += paddedText.charAt(idx);
        }
        
        lcd.setCursor(0, 0);
        lcd.print(displayText);
    }
    
    // ============================================
    // BARIS 2: Jam + WiFi Indicator (TETAP)
    // ============================================
    String timeStr = currentTimestamp.substring(11, 16); // HH:MM
    String wifiIcon = wifiConnected ? "W" : "X";
    
    String line2 = timeStr + " " + wifiIcon;
    lcd.setCursor(9, 1);
    lcd.print(line2);
}

// ----- 3. SUCCESS SHOLAT -----
void showSuccessSholatPro(String nama, String sholat, String status, int poin, int total_poin) {
    lcd.clear();
    
    // Baris 1: Nama User (kiri) + Poin didapat (kanan)
    lcd.setCursor(0, 0);
    String namaUser = truncateText(nama, 10);
    lcd.print(namaUser);
    
    // Poin di kanan (format "+5")
    String poinText = "+" + String(poin);
    lcdPrintRight(0, poinText);
    
    // Baris 2: Nama Sholat (kiri) + Total Poin (kanan)
    lcd.setCursor(0, 1);
    String sholatText = truncateText(sholat, 10);
    lcd.print(sholatText);
    
    // Total poin di kanan
    String totalText = (total_poin >= 0) ? String(total_poin) : "--";
    lcdPrintRight(1, totalText);
    
    successTone();
    delay(2500);
}

// ----- 4. SUCCESS KEGIATAN -----
void showSuccessKegiatanPro(String nama, String kegiatan, String status, int poin, String waktuMulai, String waktuSelesai, int total_poin) {
    lcd.clear();
    
    // Baris 1: Nama User (kiri) + Poin didapat (kanan)
    lcd.setCursor(0, 0);
    String namaUser = truncateText(nama, 10);
    lcd.print(namaUser);
    
    // Poin di kanan (format "+5")
    String poinText = "+" + String(poin);
    lcdPrintRight(0, poinText);
    
    // Baris 2: Nama Kegiatan (kiri) + Total Poin (kanan)
    lcd.setCursor(0, 1);
    String kegiatanText = truncateText(kegiatan, 10);
    lcd.print(kegiatanText);
    
    // Total poin di kanan
    String totalText = (total_poin >= 0) ? String(total_poin) : "--";
    lcdPrintRight(1, totalText);
    
    successTone();
    delay(2500);
}

// ----- 5. DITOLAK (UNIFIED) -----
void showDitolakPro(String sholat, String kegiatan, String keterangan, String displayMsg) {
  lcd.clear();
  
  // Baris 1: "DITOLAK!"
  lcd.setCursor(0, 0);
  lcd.print("DITOLAK!");
  
  // Baris 2: Alasan penolakan
  lcd.setCursor(0, 1);
  String reason = "";
  
  // Prioritaskan displayMsg dari server
  if (displayMsg.length() > 0) {
    reason = displayMsg;
  } 
  // Gunakan keterangan
  else if (keterangan.length() > 0) {
    reason = keterangan;
  }
  // Jika ada kegiatan/sholat, tampilkan
  else if (kegiatan.length() > 0) {
    reason = "Kegiatan: " + truncateText(kegiatan, 8);
  } else if (sholat.length() > 0) {
    reason = "Sholat: " + truncateText(sholat, 8);
  } else {
    reason = "Tidak valid";
  }
  
  // Potong jika terlalu panjang
  if (reason.length() > 16) reason = reason.substring(0, 14) + "..";
  lcd.print(reason);
  
  warningTone();
  delay(2500);
}

// ----- 6. INVALID CARD (DITOLAK) -----
void showInvalidCard() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("DITOLAK!");
  lcd.setCursor(0, 1);
  lcd.print("Kartu tidak valid");
  invalidCardTone();
  delay(2500);
}

// ----- 7. DUPLICATE SHOLAT (DITOLAK) -----
void showDuplicateSholatPro(String sholat, String waktu) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("DITOLAK!");
  
  lcd.setCursor(0, 1);
  String reason = "Sudah absen " + truncateText(sholat, 6);
  if (waktu.length() > 10) {
    reason += " " + waktu.substring(11, 16);
  }
  if (reason.length() > 16) reason = reason.substring(0, 14) + "..";
  lcd.print(reason);
  
  duplicateTone();
  delay(2500);
}

// ----- 8. DUPLICATE KEGIATAN (DITOLAK) -----
void showDuplicateKegiatanPro(String kegiatan, String waktu) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("DITOLAK!");
  
  lcd.setCursor(0, 1);
  String reason = "Sudah absen " + truncateText(kegiatan, 6);
  if (waktu.length() > 10) {
    reason += " " + waktu.substring(11, 16);
  }
  if (reason.length() > 16) reason = reason.substring(0, 14) + "..";
  lcd.print(reason);
  
  duplicateTone();
  delay(2500);
}

// ----- 9. ERROR SCREEN -----
void showErrorPro(String title, String msg) {
  lcd.clear();
  
  // Baris 1: "ERROR!"
  lcd.setCursor(0, 0);
  lcd.print("ERROR!");
  
  // Baris 2: Keterangan error
  lcd.setCursor(0, 1);
  String errorMsg = msg;
  if (errorMsg.length() > 16) errorMsg = errorMsg.substring(0, 14) + "..";
  lcd.print(errorMsg);
  
  errorTone();
  delay(2500);
}

// ----- 10. LOADING / PROCESSING -----
void showLoadingScreen(String line1, String line2) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(line1.substring(0, 16));
  lcd.setCursor(0, 1);
  static int dotCount = 0;
  String dots = "";
  for(int i = 0; i < dotCount; i++) dots += ".";
  lcd.print(line2.substring(0, 16 - dots.length()));
  lcd.print(dots);
  dotCount = (dotCount % 3) + 1;
}

void showProcessing() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Memproses...");
  lcd.setCursor(0, 1);
  static int dotCount = 0;
  for(int i = 0; i < dotCount; i++) lcd.print(".");
  dotCount = (dotCount % 3) + 1;
}

// ----- 11. WIFI CONNECTION -----
void showConnectingScreen() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Menghubungkan...");
  lcd.setCursor(0, 1);
  static int dots = 0;
  lcd.print("WiFi");
  for(int i = 0; i < dots; i++) lcd.print(".");
  dots = (dots % 3) + 1;
}

void showWiFiProgress(int attempts) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("WiFi Connect");
  lcd.setCursor(0, 1);
  int progress = (attempts * 100) / 30;
  lcd.print(progress);
  lcd.print("%");
  int barLen = (progress * 16) / 100;
  for(int i = 0; i < barLen; i++) lcd.print("=");
}

void showWiFiSuccess() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("WiFi OK!");
  lcd.setCursor(0, 1);
  lcd.print(WiFi.localIP().toString());
  delay(1500);
}

void showOfflineMode() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("MODE OFFLINE");
  lcd.setCursor(0, 1);
  lcd.print("Terbatas");
  delay(1500);
}

void showDataLoaded(String info) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Data Dimuat");
  lcd.setCursor(0, 1);
  lcd.print(info.substring(0, 16));
  delay(1000);
}

// ============================================
// 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();
  } else {
    wifiConnected = false;
    Serial.println("\nWiFi GAGAL");
    wifiFailedTone();
    showOfflineMode();
  }
}

void checkWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    if (!wifiConnected) {
      wifiConnected = true;
      Serial.println("WiFi Reconnected");
      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");
      showDataLoaded(String(jumlahKegiatan) + " kegiatan");
    } else {
      Serial.println("Gagal parse jadwal kegiatan");
      showErrorPro("Parse Error", "Gagal parse");
    }
  } else {
    Serial.print("HTTP Error: ");
    Serial.println(httpCode);
    showErrorPro("HTTP Error", String(httpCode));
  }
  
  http.end();
  lastKegiatanUpdate = millis();
}

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");
    showInvalidCard();
    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");
    showErrorPro("WiFi", "Putus");
    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");
    showErrorPro("Server", "Error");
  }
  
  http.end();
}

// ============================================
// FUNGSI PROCESS RESPONSE
// ============================================

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());
    showErrorPro("Respon", "Error");
    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;
    
    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) {
        showSuccessKegiatanPro(nama, namaKegiatanRes, displayMsg, poin, waktuMulai, waktuSelesai, total_poin);
      } else {
        String statusText = (statusPresensi == "hadir") ? "Hadir" : "Terlambat";
        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) {
        showSuccessSholatPro(nama, sholat, displayMsg, poin, total_poin);
      } else {
        showSuccessSholatPro(nama, sholat, statusPresensi, poin, total_poin);
      }
    }
    
  } else {
    handleErrorPro(errorCode, doc, namaKegiatan);
  }
}

// ============================================
// 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") {
    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") {
    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") {
    showErrorPro("Kegiatan", "Tidak ditemukan");
    
  } else if (errorCode == "KEGIATAN_INACTIVE") {
    showErrorPro("Kegiatan", "Non-aktif");
    
  } else {
    String msg = doc["message"] | "Error";
    showErrorPro("Error", msg.substring(0, 12));
  }
}

// ============================================
// 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);
    }
  }
}

void kontrolBuzzerPro(String buzzerType) {
  if (buzzerType == "SHORT_BEEP") {
    playBuzzerTone(NOTE_A5, 100);
  } else if (buzzerType == "LONG_BEEP") {
    playBuzzerTone(NOTE_G4, 600);
  } else if (buzzerType == "DOUBLE_BEEP") {
    playBuzzerTone(NOTE_C5, 100);
    delay(20);
    playBuzzerTone(NOTE_E5, 100);
  } else if (buzzerType == "CONTINUOUS") {
    for(int i = 0; i < 3; i++) {
      playBuzzerTone(NOTE_C5, 200);
      delay(20);
      playBuzzerTone(NOTE_G5, 200);
      delay(20);
    }
  }
}

// ============================================
// 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;
}

// ============================================
// 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);
}

// ============================================
// SETUP
// ============================================

void setup() {
  Serial.begin(115200);
  Serial.println("\n=== Si-ShoPint v2.0 (LCD 16x2 Version) ===");
  
  // Init LCD
  lcd.init();
  lcd.backlight();
  lcd.clear();
  
  // 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();
  
  showSplashScreen();
  
  // Connect WiFi
  connectWiFi();
  
  // Init time
  timeClient.begin();
  timeClient.update();
  updateTime();
  
  // Ambil jadwal kegiatan dari server
  if (wifiConnected) {
    ambilJadwalKegiatan();
  } else {
    showOfflineMode();
  }
  
  startupTone();
}

// ============================================
// LOOP
// ============================================

void loop() {
  static unsigned long lastTimeUpdate = 0;
  static unsigned long lastWifiCheck = 0;
  static unsigned long lastScreenUpdate = 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;
  }
  
  // Update home screen setiap 200ms (untuk animasi)
  if (now - lastScreenUpdate >= 200) {
    showHomeScreen();
    lastScreenUpdate = now;
  }
  
  // Check RFID (non-blocking)
  if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
    cardDetectedTone();
    processCard();
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
  }
  
  delay(50);
}