Uji Coba PyScript
Menjalankan Python di Browser Web
Di bawah ini adalah hasil eksekusi dari kode Python:
import sqlite3
import csv
import re
import time
import smtplib
import webbrowser
import urllib.parse
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, scrolledtext
from email.mime.text import MIMEText
from datetime import datetimetry:
import requests
REQUESTS_AVAILABLE = True
except ImportError:
REQUESTS_AVAILABLE = FalseDB_NAME = "absensi_siswa.db"
BATAS_JAM_MASUK_DEFAULT = "07:00"# ==========================================
# 1. DATABASE INITIALIZATION
# ==========================================
def init_db():
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()# Tabel Siswa
cursor.execute('''
CREATE TABLE IF NOT EXISTS siswa (
barcode TEXT PRIMARY KEY,
nama TEXT NOT NULL,
kelas TEXT NOT NULL,
kontak TEXT
)
''')# Migrasi: tambahkan kolom email jika belum ada (untuk database lama)
try:
cursor.execute("ALTER TABLE siswa ADD COLUMN email TEXT")
except sqlite3.OperationalError:
pass # kolom sudah ada# Tabel Absensi (hasil scan barcode - HADIR / TERLAMBAT)
cursor.execute('''
CREATE TABLE IF NOT EXISTS absensi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
barcode_siswa TEXT NOT NULL,
tanggal TEXT NOT NULL,
jam_scan TEXT NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY (barcode_siswa) REFERENCES siswa(barcode)
)
''')# Tabel Ketidakhadiran (Sakit / Izin / Alpa - diinput manual)
cursor.execute('''
CREATE TABLE IF NOT EXISTS ketidakhadiran (
id INTEGER PRIMARY KEY AUTOINCREMENT,
barcode_siswa TEXT NOT NULL,
tanggal TEXT NOT NULL,
keterangan TEXT NOT NULL,
catatan TEXT,
FOREIGN KEY (barcode_siswa) REFERENCES siswa(barcode)
)
''')# Tabel Pelanggaran
cursor.execute('''
CREATE TABLE IF NOT EXISTS pelanggaran (
id INTEGER PRIMARY KEY AUTOINCREMENT,
barcode_siswa TEXT NOT NULL,
tanggal TEXT NOT NULL,
jenis_pelanggaran TEXT NOT NULL,
poin INTEGER NOT NULL DEFAULT 0,
catatan TEXT,
FOREIGN KEY (barcode_siswa) REFERENCES siswa(barcode)
)
''')# Tabel Prestasi
cursor.execute('''
CREATE TABLE IF NOT EXISTS prestasi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
barcode_siswa TEXT NOT NULL,
tanggal TEXT NOT NULL,
jenis_prestasi TEXT NOT NULL,
tingkat TEXT NOT NULL,
keterangan TEXT,
FOREIGN KEY (barcode_siswa) REFERENCES siswa(barcode)
)
''')# Tabel Pengaturan (menyimpan token Fonnte, konfigurasi SMTP, dll)
cursor.execute('''
CREATE TABLE IF NOT EXISTS pengaturan (
key TEXT PRIMARY KEY,
value TEXT
)
''')conn.commit()
conn.close()def format_nomor_wa(nomor):
"""Normalisasi nomor HP Indonesia ke format internasional tanpa '+' (untuk wa.me / Fonnte)."""
if not nomor:
return ""
nomor = re.sub(r"[^0-9+]", "", nomor)
nomor = nomor.lstrip("+")
if nomor.startswith("0"):
nomor = "62" + nomor[1:]
elif not nomor.startswith("62"):
nomor = "62" + nomor
return nomor# ==========================================
# 2. GUI APPLICATION
# ==========================================
class AbsensiApp:
def __init__(self, root):
self.root = root
self.root.title("Sistem Absensi Siswa - Barcode Scanner")
self.root.geometry("1100x760")self.style = ttk.Style()
self.style.theme_use("clam")self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill="both", expand=True, padx=10, pady=10)self.tab_absensi = ttk.Frame(self.notebook)
self.tab_siswa = ttk.Frame(self.notebook)
self.tab_tidak_hadir = ttk.Frame(self.notebook)
self.tab_pelanggaran = ttk.Frame(self.notebook)
self.tab_prestasi = ttk.Frame(self.notebook)
self.tab_laporan = ttk.Frame(self.notebook)
self.tab_kirim = ttk.Frame(self.notebook)self.notebook.add(self.tab_absensi, text=" Absensi (Scan Barcode) ")
self.notebook.add(self.tab_siswa, text=" Kelola Siswa ")
self.notebook.add(self.tab_tidak_hadir, text=" Tidak Hadir ")
self.notebook.add(self.tab_pelanggaran, text=" Pelanggaran ")
self.notebook.add(self.tab_prestasi, text=" Prestasi ")
self.notebook.add(self.tab_laporan, text=" Laporan & Rekap ")
self.notebook.add(self.tab_kirim, text=" Kirim ke Orang Tua ")# Menyimpan siswa yang sedang ditampilkan rekapnya (dipakai oleh tombol download)
self.rekap_siswa_aktif = None
# Menyimpan siswa yang sedang disiapkan rekap kirimannya
self.kirim_siswa_aktif = Noneself.setup_absensi()
self.setup_siswa()
self.setup_tidak_hadir()
self.setup_pelanggaran()
self.setup_prestasi()
self.setup_laporan()
self.setup_kirim()self.refresh_all_tables()def get_db(self):
return sqlite3.connect(DB_NAME)def cari_siswa(self, barcode):
conn = self.get_db()
cursor = conn.cursor()
cursor.execute("SELECT barcode, nama, kelas, kontak, email FROM siswa WHERE barcode = ?", (barcode,))
row = cursor.fetchone()
conn.close()
return row# --------------------------------------------------
# PENGATURAN (key-value, dipakai untuk token Fonnte & SMTP)
# --------------------------------------------------
def get_setting(self, key, default=""):
conn = self.get_db()
cursor = conn.cursor()
cursor.execute("SELECT value FROM pengaturan WHERE key = ?", (key,))
row = cursor.fetchone()
conn.close()
return row[0] if row and row[0] is not None else defaultdef set_setting(self, key, value):
conn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO pengaturan (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
''', (key, value))
conn.commit()
conn.close()# ==================================================
# TAB 1: ABSENSI (SCAN BARCODE)
# ==================================================
def setup_absensi(self):
f_setting = ttk.LabelFrame(self.tab_absensi, text=" Pengaturan Jam Masuk ")
f_setting.pack(fill="x", padx=10, pady=10)ttk.Label(f_setting, text="Batas Jam Masuk (HH:MM):").grid(row=0, column=0, padx=10, pady=8, sticky="w")
self.ent_batas_jam = ttk.Entry(f_setting, width=10)
self.ent_batas_jam.insert(0, BATAS_JAM_MASUK_DEFAULT)
self.ent_batas_jam.grid(row=0, column=1, padx=10, pady=8, sticky="w")
ttk.Label(f_setting, text="Scan setelah jam ini otomatis berstatus TERLAMBAT",
font=("Arial", 9, "italic")).grid(row=0, column=2, padx=10, pady=8, sticky="w")f_scan = ttk.LabelFrame(self.tab_absensi, text=" Scan Barcode Kartu Siswa ")
f_scan.pack(fill="x", padx=10, pady=10)ttk.Label(f_scan, text="Scan Barcode:").grid(row=0, column=0, padx=10, pady=10, sticky="w")
self.ent_absen_barcode = ttk.Entry(f_scan, width=35, font=("Arial", 12))
self.ent_absen_barcode.grid(row=0, column=1, padx=10, pady=10)
self.ent_absen_barcode.focus_set()
self.ent_absen_barcode.bind("
", lambda e: self.proses_absensi())btn_absen = ttk.Button(f_scan, text="Proses Absen", command=self.proses_absensi)
btn_absen.grid(row=0, column=2, padx=10, pady=10)self.lbl_absen_info = ttk.Label(f_scan, text="Menunggu scan barcode...",
font=("Arial", 11, "bold"))
self.lbl_absen_info.grid(row=1, column=0, columnspan=3, padx=10, pady=(0, 10), sticky="w")f_list = ttk.LabelFrame(self.tab_absensi, text=" Absensi Hari Ini ")
f_list.pack(fill="both", expand=True, padx=10, pady=10)toolbar_absen = ttk.Frame(f_list)
toolbar_absen.pack(fill="x", padx=5, pady=5)
ttk.Button(toolbar_absen, text="Download Riwayat Hari Ini (CSV)",
command=self.download_absensi_hari_ini).pack(side="left")cols = ("barcode", "nama", "kelas", "jam", "status")
self.tree_absensi_hari_ini = ttk.Treeview(f_list, columns=cols, show="headings")
for col, text in zip(cols, ["Barcode", "Nama", "Kelas", "Jam Scan", "Status"]):
self.tree_absensi_hari_ini.heading(col, text=text)
self.tree_absensi_hari_ini.pack(fill="both", expand=True, padx=5, pady=(0, 5))def proses_absensi(self):
barcode = self.ent_absen_barcode.get().strip()
if not barcode:
returnsiswa = self.cari_siswa(barcode)
if not siswa:
messagebox.showerror("Gagal Absen", f"Siswa dengan barcode '{barcode}' tidak ditemukan!")
self.lbl_absen_info.config(text=f"Barcode '{barcode}' tidak terdaftar.", foreground="red")
self.ent_absen_barcode.delete(0, tk.END)
return_, nama, kelas, _kontak, _email = siswa
sekarang = datetime.now()
tanggal = sekarang.strftime("%Y-%m-%d")
jam_scan = sekarang.strftime("%H:%M:%S")conn = self.get_db()
cursor = conn.cursor()# Cek sudah absen hari ini atau belum
cursor.execute(
"SELECT jam_scan FROM absensi WHERE barcode_siswa = ? AND tanggal = ?",
(barcode, tanggal)
)
sudah = cursor.fetchone()
if sudah:
messagebox.showwarning("Sudah Absen", f"{nama} sudah absen hari ini pada jam {sudah[0]}.")
self.lbl_absen_info.config(text=f"{nama} ({kelas}) sudah absen jam {sudah[0]}.", foreground="orange")
conn.close()
self.ent_absen_barcode.delete(0, tk.END)
returnbatas_jam = self.ent_batas_jam.get().strip() or BATAS_JAM_MASUK_DEFAULT
try:
batas_dt = datetime.strptime(batas_jam, "%H:%M").time()
except ValueError:
batas_dt = datetime.strptime(BATAS_JAM_MASUK_DEFAULT, "%H:%M").time()status = "TERLAMBAT" if sekarang.time() > batas_dt else "HADIR"cursor.execute('''
INSERT INTO absensi (barcode_siswa, tanggal, jam_scan, status)
VALUES (?, ?, ?, ?)
''', (barcode, tanggal, jam_scan, status))
conn.commit()
conn.close()warna = "green" if status == "HADIR" else "red"
self.lbl_absen_info.config(
text=f"{nama} ({kelas}) - {status} pada jam {jam_scan}",
foreground=warna
)self.ent_absen_barcode.delete(0, tk.END)
self.refresh_all_tables()def refresh_absensi_hari_ini(self):
for item in self.tree_absensi_hari_ini.get_children():
self.tree_absensi_hari_ini.delete(item)tanggal = datetime.now().strftime("%Y-%m-%d")
conn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT a.barcode_siswa, s.nama, s.kelas, a.jam_scan, a.status
FROM absensi a
JOIN siswa s ON a.barcode_siswa = s.barcode
WHERE a.tanggal = ?
ORDER BY a.jam_scan ASC
''', (tanggal,))
for row in cursor.fetchall():
self.tree_absensi_hari_ini.insert("", "end", values=row)
conn.close()def download_absensi_hari_ini(self):
"""Unduh riwayat absensi (hasil scan barcode) hari ini ke file CSV."""
tanggal = datetime.now().strftime("%Y-%m-%d")
self._download_tree_csv(
self.tree_absensi_hari_ini,
("barcode", "nama", "kelas", "jam", "status"),
["Barcode", "Nama", "Kelas", "Jam Scan", "Status"],
f"absensi_{tanggal}.csv"
)# ==================================================
# TAB 2: KELOLA SISWA
# ==================================================
def setup_siswa(self):
f_input = ttk.LabelFrame(self.tab_siswa, text=" Form Data Siswa ")
f_input.pack(fill="x", padx=10, pady=5)ttk.Label(f_input, text="Barcode Kartu:").grid(row=0, column=0, padx=5, pady=5)
self.e_s_bc = ttk.Entry(f_input)
self.e_s_bc.grid(row=0, column=1, padx=5, pady=5)ttk.Label(f_input, text="Nama Siswa:").grid(row=0, column=2, padx=5, pady=5)
self.e_s_nama = ttk.Entry(f_input)
self.e_s_nama.grid(row=0, column=3, padx=5, pady=5)ttk.Label(f_input, text="Kelas:").grid(row=1, column=0, padx=5, pady=5)
self.e_s_kelas = ttk.Entry(f_input)
self.e_s_kelas.grid(row=1, column=1, padx=5, pady=5)ttk.Label(f_input, text="No. WA Ortu/Wali:").grid(row=1, column=2, padx=5, pady=5)
self.e_s_kontak = ttk.Entry(f_input)
self.e_s_kontak.grid(row=1, column=3, padx=5, pady=5)ttk.Label(f_input, text="Email Ortu/Wali:").grid(row=2, column=0, padx=5, pady=5)
self.e_s_email = ttk.Entry(f_input)
self.e_s_email.grid(row=2, column=1, padx=5, pady=5)btn_simpan = ttk.Button(f_input, text="Tambah Siswa", command=self.simpan_siswa)
btn_simpan.grid(row=3, column=1, pady=10)btn_hapus = ttk.Button(f_input, text="Hapus Siswa Terpilih", command=self.hapus_siswa)
btn_hapus.grid(row=3, column=2, pady=10)btn_import = ttk.Button(f_input, text="Import Siswa (CSV)", command=self.import_siswa)
btn_import.grid(row=3, column=3, pady=10)btn_template = ttk.Button(f_input, text="Unduh Template CSV", command=self.download_template_siswa)
btn_template.grid(row=3, column=4, pady=10, padx=(5, 0))cols = ("barcode", "nama", "kelas", "kontak", "email")
self.tree_siswa = ttk.Treeview(self.tab_siswa, columns=cols, show="headings")
for col, text in zip(cols, ["Barcode", "Nama", "Kelas", "No. WA Ortu", "Email Ortu"]):
self.tree_siswa.heading(col, text=text)
self.tree_siswa.column(col, width=150)
self.tree_siswa.pack(fill="both", expand=True, padx=10, pady=5)def simpan_siswa(self):
bc = self.e_s_bc.get().strip()
nama = self.e_s_nama.get().strip()
kelas = self.e_s_kelas.get().strip()
kontak = self.e_s_kontak.get().strip()
email = self.e_s_email.get().strip()if not all([bc, nama, kelas]):
messagebox.showwarning("Peringatan", "Barcode, Nama, dan Kelas wajib diisi!")
returnconn = self.get_db()
cursor = conn.cursor()
try:
cursor.execute("INSERT INTO siswa (barcode, nama, kelas, kontak, email) VALUES (?, ?, ?, ?, ?)",
(bc, nama, kelas, kontak, email))
conn.commit()
messagebox.showinfo("Sukses", f"Siswa '{nama}' berhasil ditambahkan.")
self.e_s_bc.delete(0, tk.END)
self.e_s_nama.delete(0, tk.END)
self.e_s_kelas.delete(0, tk.END)
self.e_s_kontak.delete(0, tk.END)
self.e_s_email.delete(0, tk.END)
except sqlite3.IntegrityError:
messagebox.showerror("Gagal", f"Barcode '{bc}' sudah terdaftar!")
finally:
conn.close()
self.refresh_all_tables()def hapus_siswa(self):
selected = self.tree_siswa.selection()
if not selected:
messagebox.showwarning("Peringatan", "Pilih data siswa yang ingin dihapus.")
returnbc = self.tree_siswa.item(selected[0])['values'][0]
nama = self.tree_siswa.item(selected[0])['values'][1]konfirmasi = messagebox.askyesno(
"Konfirmasi Hapus",
f"Yakin ingin menghapus siswa '{nama}'?\n"
"Seluruh riwayat absensi, ketidakhadiran, pelanggaran, dan prestasi siswa ini "
"TIDAK ikut terhapus otomatis, namun tidak akan tertaut ke data siswa lagi."
)
if not konfirmasi:
returnconn = self.get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM siswa WHERE barcode = ?", (str(bc),))
conn.commit()
conn.close()
messagebox.showinfo("Sukses", "Data siswa telah dihapus.")
self.refresh_all_tables()def download_template_siswa(self):
"""Unduh file template CSV kosong untuk import data siswa."""
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV File", "*.csv"), ("Semua File", "*.*")],
initialfile="template_siswa.csv"
)
if not path:
return
try:
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["barcode", "nama", "kelas", "kontak", "email"])
writer.writerow(["1234567890", "Contoh Nama Siswa", "7A", "08123456789", "ortu@contoh.com"])
messagebox.showinfo("Sukses", f"Template berhasil diunduh ke:\n{path}")
except Exception as e:
messagebox.showerror("Gagal", f"Gagal menyimpan file:\n{e}")def import_siswa(self):
"""Import data siswa secara massal dari file CSV.Format CSV yang diharapkan (header wajib ada, tidak case-sensitive):
barcode, nama, kelas, kontak, email (kontak & email boleh kosong)
"""
path = filedialog.askopenfilename(
title="Pilih File CSV Data Siswa",
filetypes=[("CSV File", "*.csv"), ("Semua File", "*.*")]
)
if not path:
returntry:
with open(path, "r", newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
if reader.fieldnames is None:
messagebox.showerror("Gagal", "File CSV kosong atau tidak memiliki header.")
return# Normalisasi nama kolom (huruf kecil, tanpa spasi) agar tidak case-sensitive
field_map = {(name or "").strip().lower(): name for name in reader.fieldnames}
kolom_wajib = ["barcode", "nama", "kelas"]
kolom_hilang = [k for k in kolom_wajib if k not in field_map]
if kolom_hilang:
messagebox.showerror(
"Format Salah",
"Header CSV wajib memiliki kolom: barcode, nama, kelas, kontak, email "
"(kontak & email opsional).\n"
f"Kolom yang hilang: {', '.join(kolom_hilang)}"
)
returnkolom_kontak = field_map.get("kontak")
kolom_email = field_map.get("email")baris_data = []
for i, row in enumerate(reader, start=2): # baris 1 = header
bc = (row.get(field_map["barcode"]) or "").strip()
nama = (row.get(field_map["nama"]) or "").strip()
kelas = (row.get(field_map["kelas"]) or "").strip()
kontak = (row.get(kolom_kontak) or "").strip() if kolom_kontak else ""
email = (row.get(kolom_email) or "").strip() if kolom_email else ""if not bc or not nama or not kelas:
baris_data.append((i, None)) # tandai baris tidak valid
continue
baris_data.append((i, (bc, nama, kelas, kontak, email)))
except Exception as e:
messagebox.showerror("Gagal", f"Gagal membaca file CSV:\n{e}")
returnif not baris_data:
messagebox.showwarning("Peringatan", "Tidak ada data pada file CSV.")
returnkonfirmasi = messagebox.askyesno(
"Konfirmasi Import",
f"Ditemukan {len(baris_data)} baris data pada file.\n"
"Lanjutkan import ke database siswa?"
)
if not konfirmasi:
returnberhasil = 0
duplikat = []
tidak_valid = []conn = self.get_db()
cursor = conn.cursor()
for nomor_baris, data in baris_data:
if data is None:
tidak_valid.append(nomor_baris)
continue
bc, nama, kelas, kontak, email = data
try:
cursor.execute("INSERT INTO siswa (barcode, nama, kelas, kontak, email) VALUES (?, ?, ?, ?, ?)",
(bc, nama, kelas, kontak, email))
berhasil += 1
except sqlite3.IntegrityError:
duplikat.append(bc)
conn.commit()
conn.close()self.refresh_all_tables()ringkasan = f"Import selesai.\n\nBerhasil ditambahkan: {berhasil} siswa."
if duplikat:
contoh = ", ".join(duplikat[:10]) + ("..." if len(duplikat) > 10 else "")
ringkasan += f"\nDilewati (barcode sudah terdaftar): {len(duplikat)} ({contoh})"
if tidak_valid:
ringkasan += (f"\nBaris tidak valid (barcode/nama/kelas kosong): "
f"{len(tidak_valid)} → baris {', '.join(map(str, tidak_valid[:10]))}"
f"{'...' if len(tidak_valid) > 10 else ''}")messagebox.showinfo("Hasil Import", ringkasan)# ==================================================
# TAB 3: TIDAK HADIR (SAKIT / IZIN / ALPA)
# ==================================================
def setup_tidak_hadir(self):
f_input = ttk.LabelFrame(self.tab_tidak_hadir, text=" Catat Siswa Tidak Hadir ")
f_input.pack(fill="x", padx=10, pady=5)ttk.Label(f_input, text="Barcode Siswa:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.e_th_bc = ttk.Entry(f_input, width=25)
self.e_th_bc.grid(row=0, column=1, padx=5, pady=5)
self.e_th_bc.bind("", lambda e: self.tampilkan_info_siswa(self.e_th_bc, self.lbl_th_info))self.lbl_th_info = ttk.Label(f_input, text="-", font=("Arial", 9, "italic"))
self.lbl_th_info.grid(row=0, column=2, columnspan=2, padx=5, pady=5, sticky="w")ttk.Label(f_input, text="Tanggal (YYYY-MM-DD):").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.e_th_tgl = ttk.Entry(f_input, width=25)
self.e_th_tgl.insert(0, datetime.now().strftime("%Y-%m-%d"))
self.e_th_tgl.grid(row=1, column=1, padx=5, pady=5)ttk.Label(f_input, text="Keterangan:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
self.cb_th_ket = ttk.Combobox(f_input, values=["SAKIT", "IZIN", "ALPA"], width=15, state="readonly")
self.cb_th_ket.current(0)
self.cb_th_ket.grid(row=1, column=3, padx=5, pady=5)ttk.Label(f_input, text="Catatan (opsional):").grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.e_th_catatan = ttk.Entry(f_input, width=60)
self.e_th_catatan.grid(row=2, column=1, columnspan=3, padx=5, pady=5, sticky="w")btn_simpan = ttk.Button(f_input, text="Simpan", command=self.simpan_tidak_hadir)
btn_simpan.grid(row=3, column=1, pady=10, sticky="w")btn_hapus = ttk.Button(f_input, text="Hapus Data Terpilih", command=self.hapus_tidak_hadir)
btn_hapus.grid(row=3, column=2, pady=10, sticky="w")cols = ("id", "barcode", "nama", "kelas", "tanggal", "keterangan", "catatan")
self.tree_tidak_hadir = ttk.Treeview(self.tab_tidak_hadir, columns=cols, show="headings")
for col, text in zip(cols, ["ID", "Barcode", "Nama", "Kelas", "Tanggal", "Keterangan", "Catatan"]):
self.tree_tidak_hadir.heading(col, text=text)
self.tree_tidak_hadir.pack(fill="both", expand=True, padx=10, pady=5)def tampilkan_info_siswa(self, entry_widget, label_widget):
bc = entry_widget.get().strip()
siswa = self.cari_siswa(bc) if bc else None
if siswa:
label_widget.config(text=f"{siswa[1]} - Kelas {siswa[2]}", foreground="green")
else:
label_widget.config(text="Siswa tidak ditemukan" if bc else "-", foreground="red" if bc else "black")def simpan_tidak_hadir(self):
bc = self.e_th_bc.get().strip()
tanggal = self.e_th_tgl.get().strip()
keterangan = self.cb_th_ket.get().strip()
catatan = self.e_th_catatan.get().strip()if not all([bc, tanggal, keterangan]):
messagebox.showwarning("Peringatan", "Barcode, Tanggal, dan Keterangan wajib diisi!")
returnsiswa = self.cari_siswa(bc)
if not siswa:
messagebox.showerror("Gagal", f"Siswa dengan barcode '{bc}' tidak ditemukan!")
returntry:
datetime.strptime(tanggal, "%Y-%m-%d")
except ValueError:
messagebox.showerror("Format Salah", "Format tanggal harus YYYY-MM-DD, contoh: 2026-09-03")
returnconn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO ketidakhadiran (barcode_siswa, tanggal, keterangan, catatan)
VALUES (?, ?, ?, ?)
''', (bc, tanggal, keterangan, catatan))
conn.commit()
conn.close()messagebox.showinfo("Sukses", f"Ketidakhadiran '{siswa[1]}' ({keterangan}) berhasil dicatat.")
self.e_th_bc.delete(0, tk.END)
self.e_th_catatan.delete(0, tk.END)
self.lbl_th_info.config(text="-", foreground="black")
self.refresh_all_tables()def hapus_tidak_hadir(self):
selected = self.tree_tidak_hadir.selection()
if not selected:
messagebox.showwarning("Peringatan", "Pilih data yang ingin dihapus.")
return
id_data = self.tree_tidak_hadir.item(selected[0])['values'][0]
conn = self.get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM ketidakhadiran WHERE id = ?", (id_data,))
conn.commit()
conn.close()
messagebox.showinfo("Sukses", "Data ketidakhadiran telah dihapus.")
self.refresh_all_tables()# ==================================================
# TAB 4: PELANGGARAN
# ==================================================
def setup_pelanggaran(self):
f_input = ttk.LabelFrame(self.tab_pelanggaran, text=" Catat Pelanggaran Siswa ")
f_input.pack(fill="x", padx=10, pady=5)ttk.Label(f_input, text="Barcode Siswa:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.e_pl_bc = ttk.Entry(f_input, width=25)
self.e_pl_bc.grid(row=0, column=1, padx=5, pady=5)
self.e_pl_bc.bind("", lambda e: self.tampilkan_info_siswa(self.e_pl_bc, self.lbl_pl_info))self.lbl_pl_info = ttk.Label(f_input, text="-", font=("Arial", 9, "italic"))
self.lbl_pl_info.grid(row=0, column=2, columnspan=2, padx=5, pady=5, sticky="w")ttk.Label(f_input, text="Tanggal (YYYY-MM-DD):").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.e_pl_tgl = ttk.Entry(f_input, width=25)
self.e_pl_tgl.insert(0, datetime.now().strftime("%Y-%m-%d"))
self.e_pl_tgl.grid(row=1, column=1, padx=5, pady=5)ttk.Label(f_input, text="Jenis Pelanggaran:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
self.cb_pl_jenis = ttk.Combobox(f_input, width=30, values=[
"Terlambat Masuk Sekolah",
"Tidak Memakai Atribut Lengkap",
"Tidak Mengerjakan Tugas",
"Membolos",
"Berkelahi",
"Merokok",
"Lainnya"
])
self.cb_pl_jenis.grid(row=1, column=3, padx=5, pady=5)ttk.Label(f_input, text="Poin Pelanggaran:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.e_pl_poin = ttk.Entry(f_input, width=25)
self.e_pl_poin.grid(row=2, column=1, padx=5, pady=5)ttk.Label(f_input, text="Catatan:").grid(row=2, column=2, padx=5, pady=5, sticky="w")
self.e_pl_catatan = ttk.Entry(f_input, width=30)
self.e_pl_catatan.grid(row=2, column=3, padx=5, pady=5)btn_simpan = ttk.Button(f_input, text="Simpan", command=self.simpan_pelanggaran)
btn_simpan.grid(row=3, column=1, pady=10, sticky="w")btn_hapus = ttk.Button(f_input, text="Hapus Data Terpilih", command=self.hapus_pelanggaran)
btn_hapus.grid(row=3, column=2, pady=10, sticky="w")cols = ("id", "barcode", "nama", "kelas", "tanggal", "jenis", "poin", "catatan")
self.tree_pelanggaran = ttk.Treeview(self.tab_pelanggaran, columns=cols, show="headings")
for col, text in zip(cols, ["ID", "Barcode", "Nama", "Kelas", "Tanggal", "Jenis Pelanggaran", "Poin", "Catatan"]):
self.tree_pelanggaran.heading(col, text=text)
self.tree_pelanggaran.pack(fill="both", expand=True, padx=10, pady=5)def simpan_pelanggaran(self):
bc = self.e_pl_bc.get().strip()
tanggal = self.e_pl_tgl.get().strip()
jenis = self.cb_pl_jenis.get().strip()
poin = self.e_pl_poin.get().strip()
catatan = self.e_pl_catatan.get().strip()if not all([bc, tanggal, jenis, poin]):
messagebox.showwarning("Peringatan", "Barcode, Tanggal, Jenis, dan Poin wajib diisi!")
returnsiswa = self.cari_siswa(bc)
if not siswa:
messagebox.showerror("Gagal", f"Siswa dengan barcode '{bc}' tidak ditemukan!")
returntry:
datetime.strptime(tanggal, "%Y-%m-%d")
poin_num = int(poin)
except ValueError:
messagebox.showerror("Format Salah", "Cek kembali format Tanggal (YYYY-MM-DD) dan Poin (angka)!")
returnconn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO pelanggaran (barcode_siswa, tanggal, jenis_pelanggaran, poin, catatan)
VALUES (?, ?, ?, ?, ?)
''', (bc, tanggal, jenis, poin_num, catatan))
conn.commit()
conn.close()messagebox.showinfo("Sukses", f"Pelanggaran '{siswa[1]}' berhasil dicatat ({poin_num} poin).")
self.e_pl_bc.delete(0, tk.END)
self.cb_pl_jenis.set("")
self.e_pl_poin.delete(0, tk.END)
self.e_pl_catatan.delete(0, tk.END)
self.lbl_pl_info.config(text="-", foreground="black")
self.refresh_all_tables()def hapus_pelanggaran(self):
selected = self.tree_pelanggaran.selection()
if not selected:
messagebox.showwarning("Peringatan", "Pilih data yang ingin dihapus.")
return
id_data = self.tree_pelanggaran.item(selected[0])['values'][0]
conn = self.get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM pelanggaran WHERE id = ?", (id_data,))
conn.commit()
conn.close()
messagebox.showinfo("Sukses", "Data pelanggaran telah dihapus.")
self.refresh_all_tables()# ==================================================
# TAB 5: PRESTASI
# ==================================================
def setup_prestasi(self):
f_input = ttk.LabelFrame(self.tab_prestasi, text=" Catat Prestasi Siswa ")
f_input.pack(fill="x", padx=10, pady=5)ttk.Label(f_input, text="Barcode Siswa:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.e_pr_bc = ttk.Entry(f_input, width=25)
self.e_pr_bc.grid(row=0, column=1, padx=5, pady=5)
self.e_pr_bc.bind("", lambda e: self.tampilkan_info_siswa(self.e_pr_bc, self.lbl_pr_info))self.lbl_pr_info = ttk.Label(f_input, text="-", font=("Arial", 9, "italic"))
self.lbl_pr_info.grid(row=0, column=2, columnspan=2, padx=5, pady=5, sticky="w")ttk.Label(f_input, text="Tanggal (YYYY-MM-DD):").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.e_pr_tgl = ttk.Entry(f_input, width=25)
self.e_pr_tgl.insert(0, datetime.now().strftime("%Y-%m-%d"))
self.e_pr_tgl.grid(row=1, column=1, padx=5, pady=5)ttk.Label(f_input, text="Jenis Prestasi:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
self.e_pr_jenis = ttk.Entry(f_input, width=30)
self.e_pr_jenis.grid(row=1, column=3, padx=5, pady=5)ttk.Label(f_input, text="Tingkat:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.cb_pr_tingkat = ttk.Combobox(f_input, width=22, state="readonly", values=[
"Sekolah", "Kecamatan", "Kabupaten/Kota", "Provinsi", "Nasional", "Internasional"
])
self.cb_pr_tingkat.current(0)
self.cb_pr_tingkat.grid(row=2, column=1, padx=5, pady=5)ttk.Label(f_input, text="Keterangan:").grid(row=2, column=2, padx=5, pady=5, sticky="w")
self.e_pr_ket = ttk.Entry(f_input, width=30)
self.e_pr_ket.grid(row=2, column=3, padx=5, pady=5)btn_simpan = ttk.Button(f_input, text="Simpan", command=self.simpan_prestasi)
btn_simpan.grid(row=3, column=1, pady=10, sticky="w")btn_hapus = ttk.Button(f_input, text="Hapus Data Terpilih", command=self.hapus_prestasi)
btn_hapus.grid(row=3, column=2, pady=10, sticky="w")cols = ("id", "barcode", "nama", "kelas", "tanggal", "jenis", "tingkat", "keterangan")
self.tree_prestasi = ttk.Treeview(self.tab_prestasi, columns=cols, show="headings")
for col, text in zip(cols, ["ID", "Barcode", "Nama", "Kelas", "Tanggal", "Jenis Prestasi", "Tingkat", "Keterangan"]):
self.tree_prestasi.heading(col, text=text)
self.tree_prestasi.pack(fill="both", expand=True, padx=10, pady=5)def simpan_prestasi(self):
bc = self.e_pr_bc.get().strip()
tanggal = self.e_pr_tgl.get().strip()
jenis = self.e_pr_jenis.get().strip()
tingkat = self.cb_pr_tingkat.get().strip()
keterangan = self.e_pr_ket.get().strip()if not all([bc, tanggal, jenis, tingkat]):
messagebox.showwarning("Peringatan", "Barcode, Tanggal, Jenis, dan Tingkat wajib diisi!")
returnsiswa = self.cari_siswa(bc)
if not siswa:
messagebox.showerror("Gagal", f"Siswa dengan barcode '{bc}' tidak ditemukan!")
returntry:
datetime.strptime(tanggal, "%Y-%m-%d")
except ValueError:
messagebox.showerror("Format Salah", "Format tanggal harus YYYY-MM-DD, contoh: 2026-09-03")
returnconn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO prestasi (barcode_siswa, tanggal, jenis_prestasi, tingkat, keterangan)
VALUES (?, ?, ?, ?, ?)
''', (bc, tanggal, jenis, tingkat, keterangan))
conn.commit()
conn.close()messagebox.showinfo("Sukses", f"Prestasi '{siswa[1]}' berhasil dicatat.")
self.e_pr_bc.delete(0, tk.END)
self.e_pr_jenis.delete(0, tk.END)
self.e_pr_ket.delete(0, tk.END)
self.lbl_pr_info.config(text="-", foreground="black")
self.refresh_all_tables()def hapus_prestasi(self):
selected = self.tree_prestasi.selection()
if not selected:
messagebox.showwarning("Peringatan", "Pilih data yang ingin dihapus.")
return
id_data = self.tree_prestasi.item(selected[0])['values'][0]
conn = self.get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM prestasi WHERE id = ?", (id_data,))
conn.commit()
conn.close()
messagebox.showinfo("Sukses", "Data prestasi telah dihapus.")
self.refresh_all_tables()# ==================================================
# TAB 6: LAPORAN & REKAP
# ==================================================
def setup_laporan(self):
f_cari = ttk.LabelFrame(self.tab_laporan, text=" Rekap Per Siswa ")
f_cari.pack(fill="x", padx=10, pady=5)ttk.Label(f_cari, text="Barcode Siswa:").pack(side="left", padx=10, pady=8)
self.e_lap_bc = ttk.Entry(f_cari, width=25)
self.e_lap_bc.pack(side="left", padx=5, pady=8)
btn_rekap = ttk.Button(f_cari, text="Tampilkan Rekap", command=self.tampilkan_rekap_siswa)
btn_rekap.pack(side="left", padx=5, pady=8)btn_download_rekap = ttk.Button(f_cari, text="Download Rekap (CSV)", command=self.download_rekap_siswa)
btn_download_rekap.pack(side="left", padx=5, pady=8)self.lbl_rekap = ttk.Label(f_cari, text="-", font=("Arial", 10, "bold"))
self.lbl_rekap.pack(side="left", padx=15, pady=8)f_cek = ttk.LabelFrame(self.tab_laporan, text=" Cek Siswa Belum Tercatat Hari Ini ")
f_cek.pack(fill="x", padx=10, pady=5)ttk.Label(f_cek, text="Tanggal (YYYY-MM-DD):").pack(side="left", padx=10, pady=8)
self.e_cek_tgl = ttk.Entry(f_cek, width=15)
self.e_cek_tgl.insert(0, datetime.now().strftime("%Y-%m-%d"))
self.e_cek_tgl.pack(side="left", padx=5, pady=8)
btn_cek = ttk.Button(f_cek, text="Cek Sekarang", command=self.cek_siswa_belum_tercatat)
btn_cek.pack(side="left", padx=5, pady=8)sub_nb = ttk.Notebook(self.tab_laporan)
sub_nb.pack(fill="both", expand=True, padx=10, pady=5)f_rekap_pl = ttk.Frame(sub_nb)
f_rekap_pr = ttk.Frame(sub_nb)
f_rekap_th = ttk.Frame(sub_nb)
f_belum = ttk.Frame(sub_nb)sub_nb.add(f_rekap_th, text=" Riwayat Ketidakhadiran ")
sub_nb.add(f_rekap_pl, text=" Riwayat Pelanggaran ")
sub_nb.add(f_rekap_pr, text=" Riwayat Prestasi ")
sub_nb.add(f_belum, text=" Belum Tercatat Hari Ini ")# --- Riwayat Ketidakhadiran ---
toolbar_th = ttk.Frame(f_rekap_th)
toolbar_th.pack(fill="x", padx=5, pady=5)
ttk.Button(toolbar_th, text="Hapus Riwayat Terpilih",
command=self.hapus_riwayat_th).pack(side="left", padx=3)
ttk.Button(toolbar_th, text="Download Riwayat (CSV)",
command=self.download_riwayat_th).pack(side="left", padx=3)cols_th = ("id", "tanggal", "keterangan", "catatan")
self.tree_rekap_th = ttk.Treeview(f_rekap_th, columns=cols_th, show="headings",
displaycolumns=("tanggal", "keterangan", "catatan"))
for col, text in zip(cols_th, ["ID", "Tanggal", "Keterangan", "Catatan"]):
self.tree_rekap_th.heading(col, text=text)
self.tree_rekap_th.pack(fill="both", expand=True, padx=5, pady=(0, 5))# --- Riwayat Pelanggaran ---
toolbar_pl = ttk.Frame(f_rekap_pl)
toolbar_pl.pack(fill="x", padx=5, pady=5)
ttk.Button(toolbar_pl, text="Hapus Riwayat Terpilih",
command=self.hapus_riwayat_pl).pack(side="left", padx=3)
ttk.Button(toolbar_pl, text="Download Riwayat (CSV)",
command=self.download_riwayat_pl).pack(side="left", padx=3)cols_pl = ("id", "tanggal", "jenis", "poin", "catatan")
self.tree_rekap_pl = ttk.Treeview(f_rekap_pl, columns=cols_pl, show="headings",
displaycolumns=("tanggal", "jenis", "poin", "catatan"))
for col, text in zip(cols_pl, ["ID", "Tanggal", "Jenis Pelanggaran", "Poin", "Catatan"]):
self.tree_rekap_pl.heading(col, text=text)
self.tree_rekap_pl.pack(fill="both", expand=True, padx=5, pady=(0, 5))# --- Riwayat Prestasi ---
toolbar_pr = ttk.Frame(f_rekap_pr)
toolbar_pr.pack(fill="x", padx=5, pady=5)
ttk.Button(toolbar_pr, text="Hapus Riwayat Terpilih",
command=self.hapus_riwayat_pr).pack(side="left", padx=3)
ttk.Button(toolbar_pr, text="Download Riwayat (CSV)",
command=self.download_riwayat_pr).pack(side="left", padx=3)cols_pr = ("id", "tanggal", "jenis", "tingkat", "keterangan")
self.tree_rekap_pr = ttk.Treeview(f_rekap_pr, columns=cols_pr, show="headings",
displaycolumns=("tanggal", "jenis", "tingkat", "keterangan"))
for col, text in zip(cols_pr, ["ID", "Tanggal", "Jenis Prestasi", "Tingkat", "Keterangan"]):
self.tree_rekap_pr.heading(col, text=text)
self.tree_rekap_pr.pack(fill="both", expand=True, padx=5, pady=(0, 5))# --- Belum Tercatat ---
cols_belum = ("barcode", "nama", "kelas")
self.tree_belum = ttk.Treeview(f_belum, columns=cols_belum, show="headings")
for col, text in zip(cols_belum, ["Barcode", "Nama", "Kelas"]):
self.tree_belum.heading(col, text=text)
self.tree_belum.pack(fill="both", expand=True)def tampilkan_rekap_siswa(self):
bc = self.e_lap_bc.get().strip()
if not bc:
messagebox.showwarning("Peringatan", "Masukkan barcode siswa terlebih dahulu.")
returnsiswa = self.cari_siswa(bc)
if not siswa:
messagebox.showerror("Gagal", f"Siswa dengan barcode '{bc}' tidak ditemukan!")
self.rekap_siswa_aktif = None
returnself.rekap_siswa_aktif = siswaconn = self.get_db()
cursor = conn.cursor()cursor.execute("SELECT COUNT(*) FROM absensi WHERE barcode_siswa = ? AND status = 'HADIR'", (bc,))
total_hadir = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM absensi WHERE barcode_siswa = ? AND status = 'TERLAMBAT'", (bc,))
total_terlambat = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ketidakhadiran WHERE barcode_siswa = ? AND keterangan = 'SAKIT'", (bc,))
total_sakit = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ketidakhadiran WHERE barcode_siswa = ? AND keterangan = 'IZIN'", (bc,))
total_izin = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ketidakhadiran WHERE barcode_siswa = ? AND keterangan = 'ALPA'", (bc,))
total_alpa = cursor.fetchone()[0]
cursor.execute("SELECT COALESCE(SUM(poin), 0) FROM pelanggaran WHERE barcode_siswa = ?", (bc,))
total_poin = cursor.fetchone()[0]self.lbl_rekap.config(
text=(f"{siswa[1]} ({siswa[2]}) | Hadir: {total_hadir} Terlambat: {total_terlambat} "
f"Sakit: {total_sakit} Izin: {total_izin} Alpa: {total_alpa} "
f"Total Poin Pelanggaran: {total_poin}")
)for item in self.tree_rekap_th.get_children():
self.tree_rekap_th.delete(item)
cursor.execute(
"SELECT id, tanggal, keterangan, catatan FROM ketidakhadiran WHERE barcode_siswa = ? ORDER BY tanggal DESC",
(bc,)
)
for row in cursor.fetchall():
self.tree_rekap_th.insert("", "end", values=row)for item in self.tree_rekap_pl.get_children():
self.tree_rekap_pl.delete(item)
cursor.execute(
"SELECT id, tanggal, jenis_pelanggaran, poin, catatan FROM pelanggaran WHERE barcode_siswa = ? ORDER BY tanggal DESC",
(bc,)
)
for row in cursor.fetchall():
self.tree_rekap_pl.insert("", "end", values=row)for item in self.tree_rekap_pr.get_children():
self.tree_rekap_pr.delete(item)
cursor.execute(
"SELECT id, tanggal, jenis_prestasi, tingkat, keterangan FROM prestasi WHERE barcode_siswa = ? ORDER BY tanggal DESC",
(bc,)
)
for row in cursor.fetchall():
self.tree_rekap_pr.insert("", "end", values=row)conn.close()def cek_siswa_belum_tercatat(self):
tanggal = self.e_cek_tgl.get().strip()
try:
datetime.strptime(tanggal, "%Y-%m-%d")
except ValueError:
messagebox.showerror("Format Salah", "Format tanggal harus YYYY-MM-DD, contoh: 2026-09-03")
returnfor item in self.tree_belum.get_children():
self.tree_belum.delete(item)conn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT barcode, nama, kelas FROM siswa
WHERE barcode NOT IN (SELECT barcode_siswa FROM absensi WHERE tanggal = ?)
AND barcode NOT IN (SELECT barcode_siswa FROM ketidakhadiran WHERE tanggal = ?)
ORDER BY kelas, nama
''', (tanggal, tanggal))
for row in cursor.fetchall():
self.tree_belum.insert("", "end", values=row)
conn.close()# --------------------------------------------------
# HAPUS RIWAYAT (dari tab Laporan & Rekap)
# --------------------------------------------------
def _hapus_riwayat_generik(self, tree, table_name, label):
"""Hapus satu baris riwayat terpilih pada treeview rekap, lalu refresh tampilan."""
selected = tree.selection()
if not selected:
messagebox.showwarning("Peringatan", f"Pilih data {label} yang ingin dihapus.")
returnkonfirmasi = messagebox.askyesno(
"Konfirmasi Hapus",
f"Yakin ingin menghapus riwayat {label} yang dipilih? Tindakan ini tidak dapat dibatalkan."
)
if not konfirmasi:
returnid_data = tree.item(selected[0])['values'][0]
conn = self.get_db()
cursor = conn.cursor()
cursor.execute(f"DELETE FROM {table_name} WHERE id = ?", (id_data,))
conn.commit()
conn.close()messagebox.showinfo("Sukses", f"Riwayat {label} telah dihapus.")# Refresh treeview terkait di tab lain & rekap yang sedang tampil
self.refresh_all_tables()
if self.rekap_siswa_aktif:
self.tampilkan_rekap_siswa()def hapus_riwayat_th(self):
self._hapus_riwayat_generik(self.tree_rekap_th, "ketidakhadiran", "ketidakhadiran")def hapus_riwayat_pl(self):
self._hapus_riwayat_generik(self.tree_rekap_pl, "pelanggaran", "pelanggaran")def hapus_riwayat_pr(self):
self._hapus_riwayat_generik(self.tree_rekap_pr, "prestasi", "prestasi")# --------------------------------------------------
# DOWNLOAD RIWAYAT / REKAP (CSV)
# --------------------------------------------------
def _download_tree_csv(self, tree, display_cols, headers, nama_default):
"""Ekspor isi sebuah treeview (hanya kolom yang ditampilkan) ke file CSV."""
if not tree.get_children():
messagebox.showwarning("Peringatan", "Tidak ada data untuk diunduh.")
returnpath = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV File", "*.csv"), ("Semua File", "*.*")],
initialfile=nama_default
)
if not path:
returntry:
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(headers)
for item in tree.get_children():
full_values = tree.item(item)['values']
# Ambil hanya kolom yang sesuai display_cols (lewati kolom id tersembunyi)
all_cols = tree["columns"]
row = [full_values[all_cols.index(c)] for c in display_cols]
writer.writerow(row)
messagebox.showinfo("Sukses", f"Data berhasil diunduh ke:\n{path}")
except Exception as e:
messagebox.showerror("Gagal", f"Gagal menyimpan file:\n{e}")def download_riwayat_th(self):
nama_file = "riwayat_ketidakhadiran.csv"
if self.rekap_siswa_aktif:
nama_file = f"riwayat_ketidakhadiran_{self.rekap_siswa_aktif[1]}.csv"
self._download_tree_csv(
self.tree_rekap_th,
("tanggal", "keterangan", "catatan"),
["Tanggal", "Keterangan", "Catatan"],
nama_file
)def download_riwayat_pl(self):
nama_file = "riwayat_pelanggaran.csv"
if self.rekap_siswa_aktif:
nama_file = f"riwayat_pelanggaran_{self.rekap_siswa_aktif[1]}.csv"
self._download_tree_csv(
self.tree_rekap_pl,
("tanggal", "jenis", "poin", "catatan"),
["Tanggal", "Jenis Pelanggaran", "Poin", "Catatan"],
nama_file
)def download_riwayat_pr(self):
nama_file = "riwayat_prestasi.csv"
if self.rekap_siswa_aktif:
nama_file = f"riwayat_prestasi_{self.rekap_siswa_aktif[1]}.csv"
self._download_tree_csv(
self.tree_rekap_pr,
("tanggal", "jenis", "tingkat", "keterangan"),
["Tanggal", "Jenis Prestasi", "Tingkat", "Keterangan"],
nama_file
)def download_rekap_siswa(self):
"""Unduh rekap lengkap (ringkasan + 3 riwayat) untuk siswa yang sedang ditampilkan."""
if not self.rekap_siswa_aktif:
messagebox.showwarning("Peringatan", "Tampilkan rekap siswa terlebih dahulu sebelum diunduh.")
returnbarcode, nama, kelas, _kontak, _email = self.rekap_siswa_aktifpath = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV File", "*.csv"), ("Semua File", "*.*")],
initialfile=f"rekap_{nama}_{kelas}.csv"
)
if not path:
returnconn = self.get_db()
cursor = conn.cursor()cursor.execute("SELECT COUNT(*) FROM absensi WHERE barcode_siswa = ? AND status = 'HADIR'", (barcode,))
total_hadir = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM absensi WHERE barcode_siswa = ? AND status = 'TERLAMBAT'", (barcode,))
total_terlambat = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ketidakhadiran WHERE barcode_siswa = ? AND keterangan = 'SAKIT'", (barcode,))
total_sakit = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ketidakhadiran WHERE barcode_siswa = ? AND keterangan = 'IZIN'", (barcode,))
total_izin = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ketidakhadiran WHERE barcode_siswa = ? AND keterangan = 'ALPA'", (barcode,))
total_alpa = cursor.fetchone()[0]
cursor.execute("SELECT COALESCE(SUM(poin), 0) FROM pelanggaran WHERE barcode_siswa = ?", (barcode,))
total_poin = cursor.fetchone()[0]cursor.execute(
"SELECT tanggal, keterangan, catatan FROM ketidakhadiran WHERE barcode_siswa = ? ORDER BY tanggal DESC",
(barcode,)
)
rows_th = cursor.fetchall()cursor.execute(
"SELECT tanggal, jenis_pelanggaran, poin, catatan FROM pelanggaran WHERE barcode_siswa = ? ORDER BY tanggal DESC",
(barcode,)
)
rows_pl = cursor.fetchall()cursor.execute(
"SELECT tanggal, jenis_prestasi, tingkat, keterangan FROM prestasi WHERE barcode_siswa = ? ORDER BY tanggal DESC",
(barcode,)
)
rows_pr = cursor.fetchall()conn.close()try:
with open(path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["REKAP SISWA"])
writer.writerow(["Nama", nama])
writer.writerow(["Kelas", kelas])
writer.writerow(["Barcode", barcode])
writer.writerow([])
writer.writerow(["Ringkasan"])
writer.writerow(["Hadir", total_hadir])
writer.writerow(["Terlambat", total_terlambat])
writer.writerow(["Sakit", total_sakit])
writer.writerow(["Izin", total_izin])
writer.writerow(["Alpa", total_alpa])
writer.writerow(["Total Poin Pelanggaran", total_poin])
writer.writerow([])writer.writerow(["Riwayat Ketidakhadiran"])
writer.writerow(["Tanggal", "Keterangan", "Catatan"])
writer.writerows(rows_th)
writer.writerow([])writer.writerow(["Riwayat Pelanggaran"])
writer.writerow(["Tanggal", "Jenis Pelanggaran", "Poin", "Catatan"])
writer.writerows(rows_pl)
writer.writerow([])writer.writerow(["Riwayat Prestasi"])
writer.writerow(["Tanggal", "Jenis Prestasi", "Tingkat", "Keterangan"])
writer.writerows(rows_pr)messagebox.showinfo("Sukses", f"Rekap lengkap berhasil diunduh ke:\n{path}")
except Exception as e:
messagebox.showerror("Gagal", f"Gagal menyimpan file:\n{e}")# ==================================================
# TAB 7: KIRIM KE ORANG TUA (WA Link / WA API / Email)
# ==================================================
def setup_kirim(self):
# ---------- Pengaturan Pengiriman ----------
f_setting = ttk.LabelFrame(self.tab_kirim, text=" Pengaturan Pengiriman ")
f_setting.pack(fill="x", padx=10, pady=8)ttk.Label(f_setting, text="Token Fonnte (untuk WhatsApp API):").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.e_set_fonnte = ttk.Entry(f_setting, width=40, show="*")
self.e_set_fonnte.grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(f_setting, text="(daftar gratis di fonnte.com, hubungkan WA, salin token)",
font=("Arial", 8, "italic")).grid(row=0, column=2, padx=5, pady=5, sticky="w")ttk.Label(f_setting, text="SMTP Host:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.e_set_smtp_host = ttk.Entry(f_setting, width=25)
self.e_set_smtp_host.insert(0, "smtp.gmail.com")
self.e_set_smtp_host.grid(row=1, column=1, padx=5, pady=5, sticky="w")ttk.Label(f_setting, text="SMTP Port:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
self.e_set_smtp_port = ttk.Entry(f_setting, width=10)
self.e_set_smtp_port.insert(0, "587")
self.e_set_smtp_port.grid(row=1, column=3, padx=5, pady=5, sticky="w")ttk.Label(f_setting, text="Email Pengirim:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.e_set_smtp_user = ttk.Entry(f_setting, width=25)
self.e_set_smtp_user.grid(row=2, column=1, padx=5, pady=5, sticky="w")ttk.Label(f_setting, text="App Password:").grid(row=2, column=2, padx=5, pady=5, sticky="w")
self.e_set_smtp_pass = ttk.Entry(f_setting, width=20, show="*")
self.e_set_smtp_pass.grid(row=2, column=3, padx=5, pady=5, sticky="w")ttk.Label(f_setting, text="Nama Pengirim (di WA/Email):").grid(row=3, column=0, padx=5, pady=5, sticky="w")
self.e_set_nama_sekolah = ttk.Entry(f_setting, width=25)
self.e_set_nama_sekolah.insert(0, "Sekolah")
self.e_set_nama_sekolah.grid(row=3, column=1, padx=5, pady=5, sticky="w")ttk.Button(f_setting, text="Simpan Pengaturan", command=self.simpan_pengaturan_kirim).grid(
row=3, column=3, padx=5, pady=5, sticky="w")self._muat_pengaturan_kirim()if not REQUESTS_AVAILABLE:
ttk.Label(f_setting, text="Catatan: modul 'requests' belum terpasang. "
"Jalankan 'pip install requests' agar WhatsApp API (Fonnte) bisa dipakai.",
foreground="red", font=("Arial", 8, "italic")).grid(
row=4, column=0, columnspan=4, padx=5, pady=(0, 5), sticky="w")# ---------- Buat & Kirim Rekap per Siswa ----------
f_siswa = ttk.LabelFrame(self.tab_kirim, text=" Kirim Rekap ke Satu Siswa ")
f_siswa.pack(fill="x", padx=10, pady=8)ttk.Label(f_siswa, text="Barcode Siswa:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.e_kr_bc = ttk.Entry(f_siswa, width=20)
self.e_kr_bc.grid(row=0, column=1, padx=5, pady=5, sticky="w")
self.e_kr_bc.bind("", lambda e: self.tampilkan_info_siswa(self.e_kr_bc, self.lbl_kr_info))self.lbl_kr_info = ttk.Label(f_siswa, text="-", font=("Arial", 9, "italic"))
self.lbl_kr_info.grid(row=0, column=2, columnspan=2, padx=5, pady=5, sticky="w")ttk.Label(f_siswa, text="Dari Tanggal:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.e_kr_dari = ttk.Entry(f_siswa, width=15)
self.e_kr_dari.grid(row=1, column=1, padx=5, pady=5, sticky="w")ttk.Label(f_siswa, text="Sampai Tanggal:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
self.e_kr_sampai = ttk.Entry(f_siswa, width=15)
self.e_kr_sampai.grid(row=1, column=3, padx=5, pady=5, sticky="w")ttk.Label(f_siswa, text="(kosongkan Dari & Sampai untuk seluruh periode; format YYYY-MM-DD)",
font=("Arial", 8, "italic")).grid(row=1, column=4, padx=5, pady=5, sticky="w")ttk.Button(f_siswa, text="Buat Rekap", command=self.buat_rekap_preview).grid(
row=2, column=1, padx=5, pady=8, sticky="w")self.txt_preview = scrolledtext.ScrolledText(f_siswa, width=90, height=12, wrap="word")
self.txt_preview.grid(row=3, column=0, columnspan=5, padx=5, pady=5, sticky="we")f_tombol_kirim = ttk.Frame(f_siswa)
f_tombol_kirim.grid(row=4, column=0, columnspan=5, padx=5, pady=5, sticky="w")ttk.Button(f_tombol_kirim, text="Kirim via WhatsApp (Link)",
command=self.kirim_via_wa_link).pack(side="left", padx=3)
ttk.Button(f_tombol_kirim, text="Kirim via WhatsApp API (Fonnte)",
command=self.kirim_via_wa_fonnte).pack(side="left", padx=3)
ttk.Button(f_tombol_kirim, text="Kirim via Email",
command=self.kirim_via_email).pack(side="left", padx=3)# ---------- Kirim Massal ----------
f_massal = ttk.LabelFrame(self.tab_kirim, text=" Kirim Rekap ke Banyak Siswa Sekaligus ")
f_massal.pack(fill="both", expand=True, padx=10, pady=8)ttk.Label(f_massal, text="Kelas (kosongkan untuk semua kelas):").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.e_massal_kelas = ttk.Entry(f_massal, width=15)
self.e_massal_kelas.grid(row=0, column=1, padx=5, pady=5, sticky="w")ttk.Label(f_massal, text="Dari Tanggal:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
self.e_massal_dari = ttk.Entry(f_massal, width=15)
self.e_massal_dari.grid(row=0, column=3, padx=5, pady=5, sticky="w")ttk.Label(f_massal, text="Sampai Tanggal:").grid(row=0, column=4, padx=5, pady=5, sticky="w")
self.e_massal_sampai = ttk.Entry(f_massal, width=15)
self.e_massal_sampai.grid(row=0, column=5, padx=5, pady=5, sticky="w")f_tombol_massal = ttk.Frame(f_massal)
f_tombol_massal.grid(row=1, column=0, columnspan=6, padx=5, pady=10, sticky="w")ttk.Button(f_tombol_massal, text="Kirim ke Semua (WhatsApp API - Fonnte)",
command=lambda: self.kirim_massal("fonnte")).pack(side="left", padx=3)
ttk.Button(f_tombol_massal, text="Kirim ke Semua (Email)",
command=lambda: self.kirim_massal("email")).pack(side="left", padx=3)ttk.Label(f_massal,
text="Siswa tanpa No. WA / Email terdaftar akan dilewati otomatis. "
"Mode WhatsApp Link tidak tersedia untuk kirim massal karena perlu klik manual satu per satu.",
font=("Arial", 8, "italic"), wraplength=850, justify="left").grid(
row=2, column=0, columnspan=6, padx=5, pady=(0, 5), sticky="w")def _muat_pengaturan_kirim(self):
self.e_set_fonnte.insert(0, self.get_setting("fonnte_token", ""))
smtp_host = self.get_setting("smtp_host", "")
if smtp_host:
self.e_set_smtp_host.delete(0, tk.END)
self.e_set_smtp_host.insert(0, smtp_host)
smtp_port = self.get_setting("smtp_port", "")
if smtp_port:
self.e_set_smtp_port.delete(0, tk.END)
self.e_set_smtp_port.insert(0, smtp_port)
self.e_set_smtp_user.insert(0, self.get_setting("smtp_user", ""))
self.e_set_smtp_pass.insert(0, self.get_setting("smtp_pass", ""))
nama_sekolah = self.get_setting("nama_sekolah", "")
if nama_sekolah:
self.e_set_nama_sekolah.delete(0, tk.END)
self.e_set_nama_sekolah.insert(0, nama_sekolah)def simpan_pengaturan_kirim(self):
self.set_setting("fonnte_token", self.e_set_fonnte.get().strip())
self.set_setting("smtp_host", self.e_set_smtp_host.get().strip())
self.set_setting("smtp_port", self.e_set_smtp_port.get().strip())
self.set_setting("smtp_user", self.e_set_smtp_user.get().strip())
self.set_setting("smtp_pass", self.e_set_smtp_pass.get().strip())
self.set_setting("nama_sekolah", self.e_set_nama_sekolah.get().strip())
messagebox.showinfo("Sukses", "Pengaturan pengiriman telah disimpan.")def bangun_teks_rekap(self, barcode, tgl_dari="", tgl_sampai=""):
"""Membuat teks rekap (absensi + ketidakhadiran + prestasi) untuk satu siswa."""
siswa = self.cari_siswa(barcode)
if not siswa:
return None, None_bc, nama, kelas, kontak, email = siswafilter_sql = ""
params_extra = []
if tgl_dari and tgl_sampai:
filter_sql = " AND tanggal BETWEEN ? AND ?"
params_extra = [tgl_dari, tgl_sampai]conn = self.get_db()
cursor = conn.cursor()cursor.execute(f"SELECT COUNT(*) FROM absensi WHERE barcode_siswa = ? AND status='HADIR'{filter_sql}",
(barcode, *params_extra))
total_hadir = cursor.fetchone()[0]cursor.execute(f"SELECT COUNT(*) FROM absensi WHERE barcode_siswa = ? AND status='TERLAMBAT'{filter_sql}",
(barcode, *params_extra))
total_terlambat = cursor.fetchone()[0]cursor.execute(
f"SELECT tanggal, keterangan, catatan FROM ketidakhadiran WHERE barcode_siswa = ?{filter_sql} "
"ORDER BY tanggal", (barcode, *params_extra))
rows_th = cursor.fetchall()cursor.execute(
f"SELECT tanggal, jenis_prestasi, tingkat, keterangan FROM prestasi WHERE barcode_siswa = ?{filter_sql} "
"ORDER BY tanggal", (barcode, *params_extra))
rows_pr = cursor.fetchall()conn.close()sakit = sum(1 for r in rows_th if r[1] == "SAKIT")
izin = sum(1 for r in rows_th if r[1] == "IZIN")
alpa = sum(1 for r in rows_th if r[1] == "ALPA")periode = f"{tgl_dari} s/d {tgl_sampai}" if (tgl_dari and tgl_sampai) else "Seluruh periode"
nama_sekolah = self.get_setting("nama_sekolah", "Sekolah")lines = []
lines.append("Rekap Kehadiran & Prestasi Siswa")
lines.append(f"Nama: {nama}")
lines.append(f"Kelas: {kelas}")
lines.append(f"Periode: {periode}")
lines.append("")
lines.append("Ringkasan:")
lines.append(f"- Hadir: {total_hadir}")
lines.append(f"- Terlambat: {total_terlambat}")
lines.append(f"- Sakit: {sakit}")
lines.append(f"- Izin: {izin}")
lines.append(f"- Alpa: {alpa}")
lines.append(f"- Prestasi: {len(rows_pr)}")if rows_th:
lines.append("")
lines.append("Detail Ketidakhadiran:")
for tgl, ket, catatan in rows_th:
teks = f"- {tgl}: {ket}"
if catatan:
teks += f" ({catatan})"
lines.append(teks)if rows_pr:
lines.append("")
lines.append("Detail Prestasi:")
for tgl, jenis, tingkat, ket in rows_pr:
teks = f"- {tgl}: {jenis} (Tingkat {tingkat})"
if ket:
teks += f" - {ket}"
lines.append(teks)lines.append("")
lines.append(f"Terima kasih atas perhatiannya.\nSalam,\n{nama_sekolah}")teks = "\n".join(lines)
return teks, siswadef buat_rekap_preview(self):
bc = self.e_kr_bc.get().strip()
if not bc:
messagebox.showwarning("Peringatan", "Masukkan barcode siswa terlebih dahulu.")
returntgl_dari = self.e_kr_dari.get().strip()
tgl_sampai = self.e_kr_sampai.get().strip()if (tgl_dari and not tgl_sampai) or (tgl_sampai and not tgl_dari):
messagebox.showwarning("Peringatan", "Isi Dari Tanggal dan Sampai Tanggal berdua, atau kosongkan berdua.")
returnif tgl_dari and tgl_sampai:
try:
datetime.strptime(tgl_dari, "%Y-%m-%d")
datetime.strptime(tgl_sampai, "%Y-%m-%d")
except ValueError:
messagebox.showerror("Format Salah", "Format tanggal harus YYYY-MM-DD.")
returnteks, siswa = self.bangun_teks_rekap(bc, tgl_dari, tgl_sampai)
if teks is None:
messagebox.showerror("Gagal", f"Siswa dengan barcode '{bc}' tidak ditemukan!")
self.kirim_siswa_aktif = None
returnself.kirim_siswa_aktif = siswa
self.txt_preview.delete("1.0", tk.END)
self.txt_preview.insert("1.0", teks)def kirim_via_wa_link(self):
if not self.kirim_siswa_aktif:
messagebox.showwarning("Peringatan", "Buat rekap terlebih dahulu sebelum mengirim.")
returnpesan = self.txt_preview.get("1.0", tk.END).strip()
if not pesan:
messagebox.showwarning("Peringatan", "Isi rekap masih kosong.")
return_bc, nama, _kelas, kontak, _email = self.kirim_siswa_aktif
nomor = format_nomor_wa(kontak)
if not nomor:
messagebox.showerror("Gagal", f"No. WA orang tua '{nama}' belum diisi. Lengkapi di tab Kelola Siswa.")
returnurl = f"https://wa.me/{nomor}?text={urllib.parse.quote(pesan)}"
webbrowser.open(url)def kirim_via_wa_fonnte(self):
if not self.kirim_siswa_aktif:
messagebox.showwarning("Peringatan", "Buat rekap terlebih dahulu sebelum mengirim.")
returnif not REQUESTS_AVAILABLE:
messagebox.showerror("Gagal", "Modul 'requests' belum terpasang. Jalankan: pip install requests")
returntoken = self.get_setting("fonnte_token", "").strip()
if not token:
messagebox.showerror("Gagal", "Token Fonnte belum diisi. Isi dulu di bagian Pengaturan Pengiriman.")
returnpesan = self.txt_preview.get("1.0", tk.END).strip()
if not pesan:
messagebox.showwarning("Peringatan", "Isi rekap masih kosong.")
return_bc, nama, _kelas, kontak, _email = self.kirim_siswa_aktif
nomor = format_nomor_wa(kontak)
if not nomor:
messagebox.showerror("Gagal", f"No. WA orang tua '{nama}' belum diisi. Lengkapi di tab Kelola Siswa.")
returntry:
resp = requests.post(
"https://api.fonnte.com/send",
headers={"Authorization": token},
data={"target": nomor, "message": pesan},
timeout=20
)
if resp.status_code == 200:
messagebox.showinfo("Sukses", f"Rekap berhasil dikirim ke WA orang tua {nama}.")
else:
messagebox.showerror("Gagal", f"Gagal mengirim (status {resp.status_code}):\n{resp.text}")
except Exception as e:
messagebox.showerror("Gagal", f"Gagal mengirim via Fonnte:\n{e}")def kirim_via_email(self):
if not self.kirim_siswa_aktif:
messagebox.showwarning("Peringatan", "Buat rekap terlebih dahulu sebelum mengirim.")
returnsmtp_host = self.get_setting("smtp_host", "").strip()
smtp_port = self.get_setting("smtp_port", "").strip()
smtp_user = self.get_setting("smtp_user", "").strip()
smtp_pass = self.get_setting("smtp_pass", "").strip()
nama_sekolah = self.get_setting("nama_sekolah", "Sekolah").strip()if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
messagebox.showerror("Gagal", "Pengaturan SMTP belum lengkap. Isi dulu di bagian Pengaturan Pengiriman.")
returnpesan = self.txt_preview.get("1.0", tk.END).strip()
if not pesan:
messagebox.showwarning("Peringatan", "Isi rekap masih kosong.")
return_bc, nama, _kelas, _kontak, email = self.kirim_siswa_aktif
email = (email or "").strip()
if not email:
messagebox.showerror("Gagal", f"Email orang tua '{nama}' belum diisi. Lengkapi di tab Kelola Siswa.")
returntry:
self._kirim_email_tunggal(smtp_host, int(smtp_port), smtp_user, smtp_pass,
nama_sekolah, email, f"Rekap Kehadiran Siswa - {nama}", pesan)
messagebox.showinfo("Sukses", f"Rekap berhasil dikirim via email ke orang tua {nama}.")
except Exception as e:
messagebox.showerror("Gagal", f"Gagal mengirim email:\n{e}")def _kirim_email_tunggal(self, host, port, user, password, nama_pengirim, tujuan, subjek, isi):
msg = MIMEText(isi, "plain", "utf-8")
msg["Subject"] = subjek
msg["From"] = f"{nama_pengirim} <{user}>"
msg["To"] = tujuanwith smtplib.SMTP(host, port, timeout=20) as server:
server.starttls()
server.login(user, password)
server.sendmail(user, [tujuan], msg.as_string())def kirim_massal(self, metode):
kelas = self.e_massal_kelas.get().strip()
tgl_dari = self.e_massal_dari.get().strip()
tgl_sampai = self.e_massal_sampai.get().strip()if (tgl_dari and not tgl_sampai) or (tgl_sampai and not tgl_dari):
messagebox.showwarning("Peringatan", "Isi Dari Tanggal dan Sampai Tanggal berdua, atau kosongkan berdua.")
returnif tgl_dari and tgl_sampai:
try:
datetime.strptime(tgl_dari, "%Y-%m-%d")
datetime.strptime(tgl_sampai, "%Y-%m-%d")
except ValueError:
messagebox.showerror("Format Salah", "Format tanggal harus YYYY-MM-DD.")
returnif metode == "fonnte":
if not REQUESTS_AVAILABLE:
messagebox.showerror("Gagal", "Modul 'requests' belum terpasang. Jalankan: pip install requests")
return
token = self.get_setting("fonnte_token", "").strip()
if not token:
messagebox.showerror("Gagal", "Token Fonnte belum diisi. Isi dulu di bagian Pengaturan Pengiriman.")
return
elif metode == "email":
smtp_host = self.get_setting("smtp_host", "").strip()
smtp_port = self.get_setting("smtp_port", "").strip()
smtp_user = self.get_setting("smtp_user", "").strip()
smtp_pass = self.get_setting("smtp_pass", "").strip()
nama_sekolah = self.get_setting("nama_sekolah", "Sekolah").strip()
if not all([smtp_host, smtp_port, smtp_user, smtp_pass]):
messagebox.showerror("Gagal", "Pengaturan SMTP belum lengkap. Isi dulu di bagian Pengaturan Pengiriman.")
returnconn = self.get_db()
cursor = conn.cursor()
if kelas:
cursor.execute("SELECT barcode FROM siswa WHERE kelas = ? ORDER BY nama", (kelas,))
else:
cursor.execute("SELECT barcode FROM siswa ORDER BY kelas, nama")
daftar_barcode = [row[0] for row in cursor.fetchall()]
conn.close()if not daftar_barcode:
messagebox.showwarning("Peringatan", "Tidak ada siswa yang cocok dengan kelas tersebut.")
returnkonfirmasi = messagebox.askyesno(
"Konfirmasi Kirim Massal",
f"Akan mengirim rekap ke {len(daftar_barcode)} siswa melalui "
f"{'WhatsApp API (Fonnte)' if metode == 'fonnte' else 'Email'}.\nLanjutkan?"
)
if not konfirmasi:
returnberhasil = 0
dilewati = []
gagal = []for bc in daftar_barcode:
teks, siswa = self.bangun_teks_rekap(bc, tgl_dari, tgl_sampai)
if teks is None or siswa is None:
continue
_bc, nama, _kelas, kontak, email = siswaif metode == "fonnte":
nomor = format_nomor_wa(kontak)
if not nomor:
dilewati.append(nama)
continue
try:
resp = requests.post(
"https://api.fonnte.com/send",
headers={"Authorization": token},
data={"target": nomor, "message": teks},
timeout=20
)
if resp.status_code == 200:
berhasil += 1
else:
gagal.append(nama)
time.sleep(1) # jeda singkat agar tidak melebihi rate limit
except Exception:
gagal.append(nama)elif metode == "email":
email = (email or "").strip()
if not email:
dilewati.append(nama)
continue
try:
self._kirim_email_tunggal(smtp_host, int(smtp_port), smtp_user, smtp_pass,
nama_sekolah, email, f"Rekap Kehadiran Siswa - {nama}", teks)
berhasil += 1
except Exception:
gagal.append(nama)ringkasan = f"Pengiriman selesai.\n\nBerhasil: {berhasil}"
if dilewati:
contoh = ", ".join(dilewati[:10]) + ("..." if len(dilewati) > 10 else "")
ringkasan += f"\nDilewati (kontak/email kosong): {len(dilewati)} ({contoh})"
if gagal:
contoh = ", ".join(gagal[:10]) + ("..." if len(gagal) > 10 else "")
ringkasan += f"\nGagal terkirim: {len(gagal)} ({contoh})"messagebox.showinfo("Hasil Pengiriman Massal", ringkasan)# ==================================================
# DATA REFRESHERS
# ==================================================
def refresh_all_tables(self):
self.refresh_absensi_hari_ini()
self.refresh_siswa()
self.refresh_tidak_hadir()
self.refresh_pelanggaran()
self.refresh_prestasi()def refresh_siswa(self):
for item in self.tree_siswa.get_children():
self.tree_siswa.delete(item)
conn = self.get_db()
cursor = conn.cursor()
cursor.execute("SELECT barcode, nama, kelas, kontak, email FROM siswa")
for row in cursor.fetchall():
self.tree_siswa.insert("", "end", values=row)
conn.close()def refresh_tidak_hadir(self):
for item in self.tree_tidak_hadir.get_children():
self.tree_tidak_hadir.delete(item)
conn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT k.id, k.barcode_siswa, s.nama, s.kelas, k.tanggal, k.keterangan, k.catatan
FROM ketidakhadiran k
JOIN siswa s ON k.barcode_siswa = s.barcode
ORDER BY k.tanggal DESC
''')
for row in cursor.fetchall():
self.tree_tidak_hadir.insert("", "end", values=row)
conn.close()def refresh_pelanggaran(self):
for item in self.tree_pelanggaran.get_children():
self.tree_pelanggaran.delete(item)
conn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT p.id, p.barcode_siswa, s.nama, s.kelas, p.tanggal, p.jenis_pelanggaran, p.poin, p.catatan
FROM pelanggaran p
JOIN siswa s ON p.barcode_siswa = s.barcode
ORDER BY p.tanggal DESC
''')
for row in cursor.fetchall():
self.tree_pelanggaran.insert("", "end", values=row)
conn.close()def refresh_prestasi(self):
for item in self.tree_prestasi.get_children():
self.tree_prestasi.delete(item)
conn = self.get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT pr.id, pr.barcode_siswa, s.nama, s.kelas, pr.tanggal, pr.jenis_prestasi, pr.tingkat, pr.keterangan
FROM prestasi pr
JOIN siswa s ON pr.barcode_siswa = s.barcode
ORDER BY pr.tanggal DESC
''')
for row in cursor.fetchall():
self.tree_prestasi.insert("", "end", values=row)
conn.close()# ==========================================
# MAIN EXECUTION
# ==========================================
if __name__ == "__main__":
init_db()
root = tk.Tk()
app = AbsensiApp(root)
root.mainloop()