Manajemen data siswa yang rapi adalah kunci suksesnya pelayanan di sekolah. Postingan kali ini akan membagikan Source Code Aplikasi Guru BK yang siap pakai. Tinggal edit nama, sekolah, dan logo, aplikasi langsung bisa digunakan!
Apa itu Bimbingan Konseling?
Bimbingan Konseling (BK) adalah proses interaksi berkelanjutan antara konselor (guru BK) dan konseli (siswa), baik secara langsung maupun tidak langsung. Tujuannya adalah membantu siswa dalam memahami diri sendiri, menemukan potensi terbaik, serta memecahkan berbagai masalah yang mungkin menghambat proses belajar dan perkembangan karakter mereka di lingkungan sekolah.
Apa itu Google Apps Script?
Google Apps Script (GAS) adalah platform pembuatan skrip berbasis cloud dari Google yang dikembangkan menggunakan bahasa JavaScript. Platform ini memungkinkan kita untuk mengotomatisasi tugas-tugas di seluruh produk Google, seperti Google Sheets, Docs, Drive, dan Gmail, serta membangun aplikasi web yang kuat tanpa perlu menyewa hosting atau database eksternal.
Manfaat Menggunakan AppScript untuk Laporan Guru BK
- 100% Gratis & Tanpa Hosting: Semua data dan sistem berjalan di atas infrastruktur Google yang aman.
- Database Real-time: Menggunakan Google Sheets sebagai database, sehingga data sangat mudah diedit, di-backup, atau diunduh oleh guru.
- Akses Kapan Saja: Aplikasi berbasis web ini dapat diakses melalui laptop, tablet, maupun smartphone selama ada koneksi internet.
- Kustomisasi Mudah: Karena menggunakan HTML, CSS, dan JavaScript murni, antarmuka aplikasi sangat mudah disesuaikan dengan kebutuhan sekolah.
@yefri_haryanto Cara Membuat Aplikasi Guru BK Menggunakan Google Appscript. Source Code silakan ambil di laman www.yefriharyanto.id Jika ingin menambah fitur dan menu silahkan kembangkan sendiri menggunakan AI. #aplikasigurubk #aplikasibimbingankonseling #aplikasigurubkmenggunakanappscript #aplikasibkonline #aplijasibimbinganbk ♬ suara asli - Guru Kito
Petunjuk Instalasi & Penggunaan
Langkah 1: Persiapan Database
- Buka Google Sheets.
- Buat Spreadsheet Kosong Baru.
- Beri nama file, misalnya: "Database Guru BK".
- Biarkan sheet tetap kosong (jangan diisi apapun).
Langkah 2: Membuka Editor Kode
- Di menu Spreadsheet, klik Ekstensi (Extensions) > Apps Script.
- Akan terbuka tab baru berisi editor kode.
Langkah 3: Masukkan Kode Backend (Code.gs)
- Hapus semua kode bawaan yang ada di file
Code.gs. - Salin kode di bawah ini, lalu tempelkan (Paste) ke editor tersebut.
/* BACKEND - SIP BK INTEGRATED CONFIG (FINAL STABLE) */
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('SIP-BK Integrated')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
/* --- API UTAMA --- */
function getInitialData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
setupDatabase(ss);
return {
config: getConfig(ss), // Load Konfigurasi
students: getSheetData(ss, "Data_Siswa"),
violations: getSheetData(ss, "Data_Pelanggaran"),
counselings: getSheetData(ss, "Data_Konseling"),
achievements: getSheetData(ss, "Data_Prestasi"),
homeVisits: getSheetData(ss, "Data_HomeVisit")
};
}
/* --- CONFIGURATION HANDLER --- */
function getConfig(ss) {
const sheet = ss.getSheetByName("Data_Config");
const data = sheet.getDataRange().getValues();
let config = {};
// Convert Rows to Object
// Skip header (row 0)
for(let i=1; i<data.length; i++){
config[data[i][0]] = data[i][1];
}
return config;
}
function saveConfig(newConfig) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Data_Config");
// Clear old data except header
if(sheet.getLastRow() > 1) {
sheet.getRange(2, 1, sheet.getLastRow()-1, 2).clearContent();
}
// Prepare data array
let rows = [];
for (const [key, value] of Object.entries(newConfig)) {
rows.push([key, value]);
}
if(rows.length > 1) { // Fixed: Changed from >0 to handle potential empty array correctly in logic
// But logic above assumes rows populated.
if(rows.length > 0) {
sheet.getRange(2, 1, rows.length, 2).setValues(rows);
}
} else if (rows.length > 0) { // Fallback if singular
sheet.getRange(2, 1, rows.length, 2).setValues(rows);
}
return getConfig(ss); // Return updated config
}
/* Single Insert */
function addDataToSheet(sheetName, dataObj) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const rowData = headers.map(header => {
if (header === 'id') return Date.now();
let val = dataObj[header];
return (val === undefined || val === null) ? "" : val;
});
sheet.appendRow(rowData);
return getSheetData(ss, sheetName);
}
/* BULK INSERT (Import Excel) */
function importBulkStudents(dataArray) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Data_Siswa");
if (!dataArray || dataArray.length === 0) return getSheetData(ss, "Data_Siswa");
const rows = dataArray.map((row, index) => {
return [
Date.now() + index,
row.name || "",
row.class || "",
row.nis || "",
"Aktif",
0
];
});
const lastRow = sheet.getLastRow();
sheet.getRange(lastRow + 1, 1, rows.length, rows[0].length).setValues(rows);
return getSheetData(ss, "Data_Siswa");
}
function deleteDataFromSheet(sheetName, id) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
const data = sheet.getDataRange().getValues();
const idIndex = data[0].indexOf('id');
if (idIndex === -1) return;
for (let i = 1; i < data.length; i++) {
if (data[i][idIndex] == id) {
sheet.deleteRow(i + 1);
break;
}
}
return getSheetData(ss, sheetName);
}
/* HELPER */
function getSheetData(ss, sheetName) {
const sheet = ss.getSheetByName(sheetName);
if (!sheet || sheet.getLastRow() < 2) return [];
const rawData = sheet.getDataRange().getValues();
const headers = rawData.shift();
return rawData.map(row => {
let obj = {};
headers.forEach((header, index) => {
let value = row[index];
if (Object.prototype.toString.call(value) === '[object Date]') {
value = Utilities.formatDate(value, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
obj[header] = value;
});
return obj;
});
}
function setupDatabase(ss) {
// Schema Tabel Data
const schema = {
"Data_Siswa": ["id", "name", "class", "nis", "status", "points"],
"Data_Pelanggaran": ["id", "studentId", "studentName", "date", "type", "points", "note"],
"Data_Konseling": ["id", "studentId", "studentName", "date", "time", "status", "topic"],
"Data_Prestasi": ["id", "studentId", "studentName", "date", "title", "level", "note"],
"Data_HomeVisit": ["id", "studentId", "studentName", "date", "parentName", "result", "petugas"]
};
for (let sheetName in schema) {
if (!ss.getSheetByName(sheetName)) {
let s = ss.insertSheet(sheetName);
s.appendRow(schema[sheetName]);
}
}
// Schema Konfigurasi (Key-Value Store)
if (!ss.getSheetByName("Data_Config")) {
let s = ss.insertSheet("Data_Config");
s.appendRow(["Key", "Value"]);
// Default Values
const defaults = [
["appPassword", "123456"],
["schoolName", "SMP NEGERI 3 KERINCI"],
["schoolAddress", "Jl. Lempur Tengah, Kec. Gunung Raya, Kab. Kerinci"],
["guruName", "Reni Emelisa, S. Pd., Gr."],
["guruNip", "199310112024212043"],
["kepsekName", "Hamdani, S. Pd."],
["kepsekNip", "197108142005021005"],
["logoDaerah", "https://drive.google.com/thumbnail?id=1uZHh5ReKYrAL6ycC6nYXtnb6zhETXQlX"],
["logoSekolah", "https://drive.google.com/thumbnail?id=1QOvUSLtFM9-s0asnaZSCPXxsGOZnBUim"]
];
s.getRange(2, 1, defaults.length, 2).setValues(defaults);
}
}
Langkah 4: Masukkan Kode Frontend (Index.html)
- Di menu sebelah kiri, tambahkan file baru berupa HTML dengan mengeklik ikon (+).
- Beri nama
Index(Perhatikan huruf kapital "I"). - Salin semua kode di bawah ini, hapus kode bawaan, lalu paste ke editor.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SIP-BK Integrated System</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.29/jspdf.plugin.autotable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
/* --- CSS SYSTEM --- */
.print-only { display: none; }
/* STYLE KERTAS SURAT (PREVIEW DI LAYAR) */
.paper-preview {
background: white;
padding: 2cm;
min-height: 29.7cm; /* Tinggi A4 */
width: 100%;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
font-family: 'Times New Roman', serif;
color: black;
line-height: 1.5;
font-size: 12pt;
}
/* CSS KHUSUS TABEL SURAT (Agar Tidak Ada Border) */
.surat-table { width: 100%; border-collapse: collapse; margin-bottom: 0; }
.surat-table td { border: none !important; padding: 2px 5px 2px 0; vertical-align: top; }
.label-col { width: 130px; }
.sep-col { width: 10px; }
@media print {
@page { size: A4 portrait; margin: 1.5cm; }
/* PAKSA WARNA */
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
background: white !important;
color: black !important;
}
/* HEADER TABEL DATA BERWARNA (Hanya tabel data laporan, bukan surat) */
.data-table thead tr th {
background-color: #d1d5db !important;
color: black !important;
border: 1px solid black !important;
}
.data-table tbody tr td {
border: 1px solid black !important;
}
/* LOGO BERWARNA */
img {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* BADGE JADI TEKS BIASA */
.bg-blue-100, .bg-red-100, .bg-green-100, .bg-orange-500, .bg-red-500, .bg-blue-500 {
background-color: transparent !important;
color: black !important;
border: none !important;
padding: 0 !important;
font-weight: normal !important;
}
/* RESET LAYOUT */
html, body { height: auto; margin: 0 !important; padding: 0 !important; overflow: visible !important; }
.no-print, nav, header, button, input, select, textarea, .scrollbar-hide { display: none !important; }
.print-only { display: flex !important; }
.print-block { display: block !important; }
/* KHUSUS SURAT SAAT PRINT */
.letter-container {
display: block !important;
width: 100% !important;
margin: 0 !important; padding: 0 !important;
}
.paper-preview {
box-shadow: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
min-height: auto !important;
}
/* TABEL CETAK UMUM */
table { width: 100% !important; border-collapse: collapse !important; font-size: 10pt !important; }
th, td { padding: 4px !important; }
thead { display: table-header-group; }
tr { break-inside: avoid; }
/* HILANGKAN BORDER KHUSUS DI SURAT SAAT PRINT */
.surat-table, .surat-table tr, .surat-table td {
border: none !important;
}
/* TANDA TANGAN SEJAJAR */
.signature-wrapper {
width: 100% !important;
margin-top: 1.5cm !important;
display: flex !important;
justify-content: space-between !important;
align-items: flex-start !important;
page-break-inside: avoid !important;
font-family: 'Times New Roman', serif !important;
font-size: 11pt !important;
}
.signature-col { width: 40% !important; text-align: center !important; }
.card-box { border: none !important; shadow: none !important; box-shadow: none !important; padding: 0 !important; }
}
/* Typography Kop Surat */
.kop-text h4 { font-size: 12pt; margin: 0; font-weight: 500; text-transform: uppercase; font-family: 'Times New Roman', serif; }
.kop-text h3 { font-size: 14pt; margin: 0; font-weight: 700; text-transform: uppercase; font-family: 'Times New Roman', serif; }
.kop-text h1 { font-size: 18pt; margin: 0; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; font-family: 'Times New Roman', serif; }
.kop-text p { font-size: 10pt; margin: 0; font-style: italic; margin-top: 2px; font-family: 'Times New Roman', serif; }
</style>
</head>
<body class="bg-slate-100 text-slate-900 font-sans">
<div id="root"></div>
<div id="hidden-kop-container" style="position: absolute; top: -9999px; left: -9999px; width: 800px; background: white; padding: 20px;">
<div style="display: flex; align-items: center; justify-content: space-between; border-bottom: 4px double black; padding-bottom: 10px; margin-bottom: 10px;">
<div style="width: 100px; display: flex; justify-content: center; align-items: center;">
<img id="kop-logo-1" src="" style="width: 80px; height: auto; object-fit: contain;"/>
</div>
<div style="flex: 1; text-align: center; color: black; font-family: 'Times New Roman', serif;">
<h4 style="font-size: 14pt; margin: 0; font-weight: 500; text-transform: uppercase;">PEMERINTAH KABUPATEN</h4>
<h3 style="font-size: 16pt; margin: 0; font-weight: 700; text-transform: uppercase;">DINAS PENDIDIKAN</h3>
<h1 id="kop-school-name" style="font-size: 22pt; margin: 0; font-weight: 800; text-transform: uppercase; letter-spacing: 1px;"></h1>
<p id="kop-school-address" style="font-size: 11pt; margin: 0; font-style: italic; margin-top: 2px;"></p>
</div>
<div style="width: 100px; display: flex; justify-content: center; align-items: center;">
<img id="kop-logo-2" src="" style="width: 80px; height: auto; object-fit: contain;"/>
</div>
</div>
</div>
<script type="text/babel">
const { useState, useEffect, useMemo } = React;
const { jsPDF } = window.jspdf;
const Icon = ({ name, size = 20, className = "" }) => <i data-lucide={name} style={{ width: size, height: size }} className={className}></i>;
// --- KOP SURAT (Untuk Tampilan HTML) ---
const FormalHeader = ({ config, title }) => (
<div className="w-full text-black font-serif mb-6">
<div className="flex items-center justify-between border-b-4 border-double border-black pb-4 mb-4">
<div className="w-24 flex justify-center items-center">
{config.logoDaerah && <img src={config.logoDaerah} className="w-20 h-auto object-contain"/>}
</div>
<div className="flex-1 text-center kop-text px-2">
<h4>PEMERINTAH KABUPATEN</h4>
<h3>DINAS PENDIDIKAN</h3>
<h1>{config.schoolName}</h1>
<p>{config.schoolAddress}</p>
</div>
<div className="w-24 flex justify-center items-center">
{config.logoSekolah && <img src={config.logoSekolah} className="w-20 h-auto object-contain"/>}
</div>
</div>
{title && (
<div className="text-center mb-6">
<h2 className="text-xl font-bold uppercase underline decoration-2 underline-offset-4">{title}</h2>
</div>
)}
</div>
);
// --- TANDA TANGAN ---
const SignatureSection = ({ config, dateStr }) => {
const today = dateStr
? new Date(dateStr).toLocaleDateString('id-ID', {day: 'numeric', month: 'long', year: 'numeric'})
: new Date().toLocaleDateString('id-ID', {day: 'numeric', month: 'long', year: 'numeric'});
return (
<div className="signature-wrapper flex justify-between mt-12 font-serif text-black">
<div className="signature-col text-center w-64">
<p>Mengetahui,</p>
<p>Kepala Sekolah</p>
<br/><br/><br/><br/>
<p className="font-bold underline">{config.kepsekName}</p>
<p>NIP. {config.kepsekNip}</p>
</div>
<div className="signature-col text-center w-64">
<p>Kerinci, {today}</p>
<p>Guru Bimbingan Konseling</p>
<br/><br/><br/><br/>
<p className="font-bold underline">{config.guruName}</p>
<p>NIP. {config.guruNip}</p>
</div>
</div>
);
};
// --- LOGIN VIEW ---
const LoginView = ({ onLogin, config }) => {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
const inputPwd = String(password).trim();
const dbPwd = String(config.appPassword || "123456").trim();
if (inputPwd === dbPwd) onLogin(true);
else { setError("Password salah."); setPassword(""); }
};
return (
<div className="min-h-screen flex items-center justify-center bg-slate-900 p-4">
<div className="bg-white p-8 rounded-xl shadow-2xl w-full max-w-md text-center">
<div className="w-16 h-16 bg-blue-600 rounded-xl mx-auto flex items-center justify-center text-white mb-4"><Icon name="lock" size={32} /></div>
<h1 className="text-2xl font-bold text-slate-800">Aplikasi Guru BK</h1>
<p className="text-slate-500 text-sm mt-1">{config.schoolName || 'Memuat Data...'}</p>
<form onSubmit={handleSubmit} className="space-y-6 mt-6">
<input type="password" required className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500" placeholder="Password Aplikasi" value={password} onChange={(e) => {setPassword(e.target.value); setError("");}}/>
{error && <p className="text-red-500 text-xs text-left flex items-center gap-1"><Icon name="alert-circle" size={12}/> {error}</p>}
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg flex justify-center gap-2"><Icon name="log-in" size={20} /> Masuk</button>
</form>
<div className="mt-4 text-xs text-gray-400 border-t pt-2">Default Password: 123456</div>
</div>
</div>
);
};
// --- CONFIG VIEW ---
const ConfigView = ({ config, onSave }) => {
const [formData, setFormData] = useState(config);
const [saved, setSaved] = useState(false);
const handleChange = (e) => setFormData({...formData, [e.target.name]: e.target.value});
const handleSave = (e) => { e.preventDefault(); onSave(formData); setSaved(true); setTimeout(() => setSaved(false), 3000); };
return (
<div className="max-w-4xl mx-auto bg-white p-6 md:p-8 rounded-xl shadow border card-box">
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2"><Icon name="settings" size={24}/> Konfigurasi Aplikasi</h2>
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<h3 className="font-bold text-blue-600 border-b pb-2">Identitas Sekolah</h3>
<div><label className="block text-sm font-medium">Nama Sekolah</label><input name="schoolName" value={formData.schoolName||''} onChange={handleChange} className="w-full p-2 border rounded"/></div>
<div><label className="block text-sm font-medium">Alamat Lengkap</label><textarea name="schoolAddress" value={formData.schoolAddress||''} onChange={handleChange} className="w-full p-2 border rounded h-20"></textarea></div>
<div><label className="block text-sm font-medium">Link Logo Daerah</label><input name="logoDaerah" value={formData.logoDaerah||''} onChange={handleChange} className="w-full p-2 border rounded text-xs"/></div>
<div><label className="block text-sm font-medium">Link Logo Sekolah</label><input name="logoSekolah" value={formData.logoSekolah||''} onChange={handleChange} className="w-full p-2 border rounded text-xs"/></div>
</div>
<div className="space-y-4">
<h3 className="font-bold text-blue-600 border-b pb-2">Pejabat & Keamanan</h3>
<div><label className="block text-sm font-medium">Nama Kepala Sekolah</label><input name="kepsekName" value={formData.kepsekName||''} onChange={handleChange} className="w-full p-2 border rounded"/></div>
<div><label className="block text-sm font-medium">NIP Kepala Sekolah</label><input name="kepsekNip" value={formData.kepsekNip||''} onChange={handleChange} className="w-full p-2 border rounded"/></div>
<div><label className="block text-sm font-medium">Nama Guru BK</label><input name="guruName" value={formData.guruName||''} onChange={handleChange} className="w-full p-2 border rounded"/></div>
<div><label className="block text-sm font-medium">NIP Guru BK</label><input name="guruNip" value={formData.guruNip||''} onChange={handleChange} className="w-full p-2 border rounded"/></div>
<div className="bg-red-50 p-4 rounded border border-red-200"><label className="block text-sm font-bold text-red-700">Password Aplikasi</label><input name="appPassword" value={formData.appPassword||''} onChange={handleChange} className="w-full p-2 border rounded border-red-300"/></div>
</div>
<div className="md:col-span-2 pt-4 border-t">
<button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded-lg font-bold hover:bg-blue-700 w-full md:w-auto flex items-center justify-center gap-2"><Icon name="save" size={18}/> {saved ? "Tersimpan!" : "Simpan Konfigurasi"}
</button>
</div>
</form>
</div>
);
}
// --- STUDENTS VIEW (MANUAL INPUT CLASS) ---
const StudentsView = ({ students, onAddStudent, onDeleteStudent, onBulkImport, config }) => {
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [newStudent, setNewStudent] = useState({ name: '', class: '', nis: '' });
const filtered = (students||[]).filter(s => s.name.toLowerCase().includes(search.toLowerCase()) || s.nis.includes(search));
const handleSubmit = (e) => { e.preventDefault(); onAddStudent(newStudent); setNewStudent({ name: '', class: '', nis: '' }); setShowModal(false); };
const downloadTemplate = () => { const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([{ "Nama Lengkap": "", "NIS": "", "Kelas": "" }]), "Template"); XLSX.writeFile(wb, "Template_Siswa.xlsx"); }
const handleFileImport = (e) => {
const file = e.target.files[0]; if(!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
const wb = XLSX.read(evt.target.result, { type: 'binary' });
const ws = wb.Sheets[wb.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(ws);
const formattedData = data.map(row => ({ name: row["Nama Lengkap"] || row["Nama"], nis: row["NIS"], class: row["Kelas"] })).filter(i => i.name && i.class);
if(formattedData.length > 0 && confirm(`Import ${formattedData.length} data?`)) { onBulkImport(formattedData); setShowImportModal(false); }
}; reader.readAsBinaryString(file);
};
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 no-print">
<h2 className="text-2xl font-bold text-slate-800">Data Siswa</h2>
<div className="flex flex-wrap gap-2 justify-center">
<button onClick={() => window.print()} className="bg-slate-800 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-slate-900"><Icon name="printer" size={18}/> Cetak</button>
<button onClick={() => setShowImportModal(true)} className="bg-green-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-green-700"><Icon name="file-spreadsheet" size={18}/> Import</button>
<button onClick={() => setShowModal(true)} className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700"><Icon name="user-plus" size={18}/> Tambah</button>
</div>
</div>
<div className="print-block hidden"><FormalHeader config={config} title="DATA SISWA" /></div>
<div className="no-print"><input className="w-full p-3 border rounded-lg" placeholder="Cari nama atau NIS..." value={search} onChange={e=>setSearch(e.target.value)}/></div>
<div className="bg-white rounded-xl shadow overflow-hidden card-box">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left data-table">
<thead className="bg-slate-100 uppercase"><tr><th className="p-4">NIS</th><th className="p-4">Nama</th><th className="p-4">Kelas</th><th className="p-4 no-print">Aksi</th></tr></thead>
<tbody>{filtered.map(s => (<tr key={s.id} className="border-b hover:bg-slate-50"><td className="p-4">{s.nis}</td><td className="p-4 font-bold">{s.name}</td><td className="p-4"><span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs print:bg-white print:text-black print:px-0">{s.class}</span></td><td className="p-4 no-print"><button onClick={()=>onDeleteStudent(s.id)} className="text-red-500"><Icon name="trash-2" size={16}/></button></td></tr>))}</tbody>
</table>
</div>
</div>
<div className="print-block hidden"><SignatureSection config={config} /></div>
{/* Modals - Input Kelas Manual */}
{showModal && <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print"><div className="bg-white p-6 rounded-xl w-96 mx-4"><h3 className="text-xl font-bold mb-4">Tambah Siswa</h3><form onSubmit={handleSubmit} className="space-y-4"><input required placeholder="Nama" className="w-full p-2 border rounded" value={newStudent.name} onChange={e=>setNewStudent({...newStudent, name:e.target.value})}/><input required placeholder="NIS" className="w-full p-2 border rounded" value={newStudent.nis} onChange={e=>setNewStudent({...newStudent, nis:e.target.value})}/><input required placeholder="Kelas (Manual)" className="w-full p-2 border rounded" value={newStudent.class} onChange={e=>setNewStudent({...newStudent, class:e.target.value})}/><div className="flex justify-end gap-2"><button type="button" onClick={()=>setShowModal(false)} className="px-4 py-2 text-slate-500">Batal</button><button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">Simpan</button></div></form></div></div>}
{showImportModal && <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print"><div className="bg-white p-6 rounded-xl w-96 mx-4"><h3 className="font-bold mb-4">Import Excel</h3><button onClick={downloadTemplate} className="text-blue-600 text-sm mb-4 block underline">Download Template</button><input type="file" accept=".xlsx" onChange={handleFileImport} className="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"/><button onClick={()=>setShowImportModal(false)} className="mt-4 w-full text-slate-500">Batal</button></div></div>}
</div>
)
};
// --- GENERIC VIEW (Prestasi, Konseling, Home Visit) ---
const GenericFormView = ({ title, type, dataList, students, onAdd, config }) => {
const [showModal, setShowModal] = useState(false);
const [form, setForm] = useState({});
const handleSubmit = (e) => { e.preventDefault(); const s = (students||[]).find(s=>String(s.id)===String(form.studentId)); if(s){ onAdd({...form, studentName: s.name}); setShowModal(false); setForm({}); }};
// Fix judul Laporan Home Visit
const reportTitle = type === 'homevisit' ? 'LAPORAN HOME VISIT' : `LAPORAN ${title.toUpperCase()}`;
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 no-print">
<h2 className="text-2xl font-bold text-slate-800">{title}</h2>
<div className="flex gap-2">
<button onClick={() => window.print()} className="bg-slate-800 text-white px-4 py-2 rounded-lg flex items-center gap-2"><Icon name="printer" size={18}/> Cetak</button>
<button onClick={()=>setShowModal(true)} className="bg-blue-600 text-white px-4 py-2 rounded-lg flex gap-2"><Icon name="plus" size={18}/> Tambah</button>
</div>
</div>
<div className="print-block hidden"><FormalHeader config={config} title={reportTitle} /></div>
<div className="bg-white rounded-xl shadow overflow-hidden card-box">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left data-table">
<thead className="bg-slate-100 uppercase"><tr><th className="p-4">Tanggal</th><th className="p-4">Nama</th><th className="p-4">Keterangan</th>{type==='violation' && <th className="p-4">Poin</th>}</tr></thead>
<tbody>{(dataList||[]).map((d,i) => (<tr key={i} className="border-b"><td className="p-4 w-32">{d.date}</td><td className="p-4 font-bold">{d.studentName}</td><td className="p-4">{d.type || d.title || d.topic || d.result} <span className="text-xs text-gray-500 block">{d.note}</span></td>{type==='violation' && <td className="p-4 text-red-600 font-bold">+{d.points}</td>}</tr>))}</tbody>
</table>
</div>
</div>
<div className="print-block hidden"><SignatureSection config={config} /></div>
{showModal && <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print"><div className="bg-white p-6 rounded-xl w-96 mx-4"><h3 className="font-bold text-lg mb-4">Input Data</h3><form onSubmit={handleSubmit} className="space-y-3"><select required className="w-full p-2 border rounded" onChange={e=>setForm({...form, studentId:e.target.value})}><option value="">Pilih Siswa</option>{(students||[]).map(s=><option key={s.id} value={s.id}>{s.name} - {s.class}</option>)}</select><input type="date" required className="w-full p-2 border rounded" onChange={e=>setForm({...form, date:e.target.value})}/>{type==='achievement' && <><input placeholder="Prestasi" className="w-full p-2 border rounded" onChange={e=>setForm({...form, title:e.target.value})}/><select className="w-full p-2 border rounded" onChange={e=>setForm({...form, level:e.target.value})}><option value="Sekolah">Sekolah</option><option value="Kabupaten">Kabupaten</option><option value="Provinsi">Provinsi</option></select></>}{type==='counseling' && <><input placeholder="Topik" className="w-full p-2 border rounded" onChange={e=>setForm({...form, topic:e.target.value})}/><input type="time" className="w-full p-2 border rounded" onChange={e=>setForm({...form, time:e.target.value})}/></>}{type==='homevisit' && <><input placeholder="Nama Wali" className="w-full p-2 border rounded" onChange={e=>setForm({...form, parentName:e.target.value})}/><textarea placeholder="Hasil" className="w-full p-2 border rounded" onChange={e=>setForm({...form, result:e.target.value})}/></>}<input placeholder="Catatan" className="w-full p-2 border rounded" onChange={e=>setForm({...form, note:e.target.value})}/><div className="flex justify-end gap-2 mt-4"><button type="button" onClick={()=>setShowModal(false)} className="text-gray-500">Batal</button><button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">Simpan</button></div></form></div></div>}
</div>
)
}
// --- VIOLATIONS VIEW ---
const ViolationsView = ({ violations, students, onAddViolation, config }) => {
const [showModal, setShowModal] = useState(false);
const [form, setForm] = useState({ studentId: '', type: '', category: '', points: 0, note: '', date: new Date().toISOString().split('T')[0] });
const handleCategoryChange = (e) => {
const cat = e.target.value;
let pts = 0;
if(cat === 'Berat') pts = 75; else if(cat === 'Sedang') pts = 20; else if(cat === 'Ringan') pts = 5;
setForm({...form, category: cat, points: pts, type: cat});
};
const handleSubmit = (e) => {
e.preventDefault();
const s = (students||[]).find(s=>String(s.id)===String(form.studentId));
if(s){
onAddViolation({...form, studentName: s.name, points: Number(form.points)});
setShowModal(false);
setForm({ studentId: '', type: '', category: '', points: 0, note: '', date: new Date().toISOString().split('T')[0] });
}
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center no-print">
<h2 className="text-2xl font-bold">Buku Kasus</h2>
<div className="flex gap-2">
<button onClick={() => window.print()} className="bg-slate-800 text-white px-4 py-2 rounded-lg flex items-center gap-2"><Icon name="printer" size={18}/> Cetak</button>
<button onClick={()=>setShowModal(true)} className="bg-red-600 text-white px-4 py-2 rounded flex gap-2"><Icon name="plus" size={18}/> Catat</button>
</div>
</div>
<div className="print-block hidden"><FormalHeader config={config} title="BUKU KASUS PELANGGARAN" /></div>
<div className="bg-white shadow rounded overflow-hidden card-box">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left data-table"><thead className="bg-slate-100"><tr><th className="p-4">Tanggal</th><th className="p-4">Nama</th><th className="p-4">Kategori</th><th className="p-4">Catatan</th><th className="p-4">Poin</th></tr></thead><tbody>{(violations||[]).map((v,i)=>(<tr key={i} className="border-b"><td className="p-4 w-32">{v.date}</td><td className="p-4 font-bold">{v.studentName}</td><td className="p-4 font-semibold">{v.type}</td><td className="p-4 text-gray-600">{v.note}</td><td className="p-4 text-red-600 font-bold">+{v.points}</td></tr>))}</tbody></table>
</div>
</div>
<div className="print-block hidden"><SignatureSection config={config} /></div>
{showModal && <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 no-print"><div className="bg-white p-6 rounded w-96 mx-4"><h3 className="font-bold mb-4">Catat Kasus</h3><form onSubmit={handleSubmit} className="space-y-3"><select required className="w-full p-2 border rounded" onChange={e=>setForm({...form, studentId:e.target.value})}><option value="">Pilih Siswa</option>{(students||[]).map(s=><option key={s.id} value={s.id}>{s.name}</option>)}</select><input type="date" className="w-full p-2 border rounded" value={form.date} onChange={e=>setForm({...form, date:e.target.value})}/><select required className="w-full p-2 border rounded" value={form.category} onChange={handleCategoryChange}><option value="">-- Pilih Jenis --</option><option value="Berat">Berat (75 Poin)</option><option value="Sedang">Sedang (20 Poin)</option><option value="Ringan">Ringan (5 Poin)</option></select><input type="number" className="w-full p-2 border rounded bg-gray-100" value={form.points} placeholder="Poin Otomatis"/><textarea placeholder="Catatan Kejadian (Detail)" className="w-full p-2 border rounded" onChange={e=>setForm({...form, note:e.target.value})}></textarea><div className="flex justify-end gap-2 mt-4"><button type="button" onClick={()=>setShowModal(false)} className="text-gray-500">Batal</button><button type="submit" className="bg-red-600 text-white px-4 py-2 rounded">Simpan</button></div></form></div></div>}
</div>
)
}
const MonitoringView = ({ students, violations, config }) => {
const calculatedStudents = useMemo(() => {
return (students || []).map(student => {
const studentViolations = (violations || []).filter(v => String(v.studentId) === String(student.id));
const totalPoints = studentViolations.reduce((sum, v) => sum + Number(v.points || 0), 0);
return { ...student, points: totalPoints, violationHistory: studentViolations };
}).filter(s => s.points > 0).sort((a, b) => b.points - a.points);
}, [students, violations]);
const getStatusColor = (p) => p >= 50 ? 'bg-red-500' : p >= 20 ? 'bg-orange-500' : 'bg-blue-500';
const getStatusText = (p) => p >= 50 ? 'BAHAYA (SP)' : p >= 20 ? 'PERLU PERHATIAN' : 'PEMBINAAN';
return (
<div className="space-y-6">
<div className="flex justify-between items-center no-print">
<h2 className="text-2xl font-bold text-slate-800">Pantauan Siswa</h2>
<button onClick={() => window.print()} className="bg-slate-800 text-white px-4 py-2 rounded-lg flex items-center gap-2"><Icon name="printer" size={18}/> Cetak</button>
</div>
<div className="print-block hidden"><FormalHeader config={config} title="DAFTAR SISWA DALAM PENGAWASAN" /></div>
<div className="grid grid-cols-1 gap-6 no-print">{calculatedStudents.map((s) => (<div key={s.id} className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex flex-col md:flex-row"><div className="p-6 md:w-1/3 bg-slate-50 flex flex-col items-center justify-center text-center border-r"><div className={`w-20 h-20 rounded-full ${getStatusColor(s.points)} text-white flex items-center justify-center text-3xl font-bold mb-4 shadow-lg`}>{s.points}</div><h3 className="font-bold text-xl text-slate-800">{s.name}</h3><p className="text-slate-500 mb-2">Kelas {s.class} | NIS: {s.nis}</p><span className={`px-3 py-1 rounded-full text-xs font-bold text-white ${getStatusColor(s.points)}`}>{getStatusText(s.points)}</span></div><div className="p-6 md:w-2/3"><h4 className="font-bold text-slate-700 mb-4 flex items-center gap-2"><Icon name="file-text" size={18} /> Riwayat Pelanggaran</h4><div className="space-y-3 max-h-60 overflow-y-auto">{s.violationHistory.map((v, idx) => (<div key={idx} className="flex justify-between text-sm p-3 bg-white border rounded-lg"><div><p className="font-semibold">{v.type}</p><p className="text-xs text-slate-500">{v.note}</p></div><div className="text-right"><span className="text-red-600 font-bold">+{v.points}</span><p className="text-xs text-slate-400">{v.date}</p></div></div>))}</div></div></div>))}</div>
<div className="print-block hidden"><table className="w-full data-table"><thead><tr><th>No</th><th>Nama</th><th>Kelas</th><th>Total Poin</th><th>Status</th></tr></thead><tbody>{calculatedStudents.map((s,i)=>(<tr key={i}><td className="text-center">{i+1}</td><td>{s.name}</td><td className="text-center">{s.class}</td><td className="text-center font-bold">{s.points}</td><td>{getStatusText(s.points)}</td></tr>))}</tbody></table></div>
{calculatedStudents.length === 0 && (<div className="text-center p-12 bg-white rounded-xl border border-slate-200 no-print"><Icon name="award" size={48} className="mx-auto text-green-500 mb-4"/><h3 className="text-xl font-bold text-slate-800">Aman</h3><p className="text-slate-500">Tidak ada siswa bermasalah.</p></div>)}
</div>
);
};
const DashboardView = ({ students, violations, counselings, achievements, config }) => {
const stats = [{ label: 'Total Siswa', value: (students||[]).length, icon: 'users', color: 'bg-blue-500' }, { label: 'Pelanggaran', value: (violations||[]).length, icon: 'alert-triangle', color: 'bg-red-500' }, { label: 'Prestasi', value: (achievements||[]).length, icon: 'award', color: 'bg-yellow-500' }, { label: 'Konseling', value: (counselings||[]).length, icon: 'calendar', color: 'bg-green-500' }];
const guides = [{ icon: 'layout-dashboard', title: 'Dashboard', desc: 'Halaman utama ringkasan data statistik.' }, { icon: 'eye', title: 'Pantauan Siswa', desc: 'Melihat siswa yang poin pelanggarannya mencapai batas peringatan.' }, { icon: 'users', title: 'Data Siswa', desc: 'Input, import, dan kelola data master siswa.' }, { icon: 'alert-triangle', title: 'Pelanggaran', desc: 'Mencatat kasus pelanggaran siswa (Ringan, Sedang, Berat).' }, { icon: 'mail', title: 'Surat Panggilan', desc: 'Cetak surat panggilan orang tua otomatis.' }, { icon: 'award', title: 'Prestasi', desc: 'Mencatat pencapaian akademik/non-akademik siswa.' }, { icon: 'calendar', title: 'Konseling', desc: 'Jadwal dan rekam jejak bimbingan konseling.' }, { icon: 'home', title: 'Home Visit', desc: 'Laporan kunjungan rumah.' }, { icon: 'file-text', title: 'Laporan', desc: 'Cetak laporan rekapitulasi data.' }, { icon: 'settings', title: 'Pengaturan', desc: 'Ubah identitas sekolah, logo, dan password.' }];
return (
<div className="space-y-8">
{/* WELCOME BANNER */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-xl p-8 text-white shadow-lg relative overflow-hidden">
<div className="relative z-10">
<h1 className="text-3xl font-bold mb-2">Selamat Datang, {config.guruName || 'Guru BK'}</h1>
<p className="text-blue-100 text-lg">{config.schoolName || 'SIP-BK System'}</p>
<div className="mt-4 inline-flex items-center bg-white/20 px-4 py-2 rounded-full text-sm">
<Icon name="calendar" size={16} className="mr-2"/>
{new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</div>
</div>
<div className="absolute right-0 bottom-0 opacity-10 transform translate-x-10 translate-y-10"><Icon name="book-open" size={200} /></div>
</div>
{/* STATS GRID */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, idx) => (
<div key={idx} className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex items-center gap-4 hover:shadow-md transition-shadow">
<div className={`${stat.color} p-4 rounded-lg text-white`}><Icon name={stat.icon} size={24}/></div>
<div><p className="text-sm text-slate-500 font-medium">{stat.label}</p><p className="text-2xl font-bold text-slate-800">{stat.value}</p></div>
</div>
))}
</div>
{/* USER GUIDE */}
<div>
<h3 className="text-xl font-bold text-slate-800 mb-4 flex items-center gap-2"><Icon name="help-circle" size={24} className="text-blue-600"/> Petunjuk Penggunaan Aplikasi</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{guides.map((guide, idx) => (
<div key={idx} className="bg-white p-5 rounded-xl border border-slate-200 hover:border-blue-400 transition-colors group">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-slate-100 rounded-lg group-hover:bg-blue-50 text-slate-600 group-hover:text-blue-600 transition-colors"><Icon name={guide.icon} size={20}/></div>
<h4 className="font-bold text-slate-700">{guide.title}</h4>
</div>
<p className="text-sm text-slate-500 leading-relaxed">{guide.desc}</p>
</div>
))}
</div>
</div>
</div>
);
};
// --- LETTERS VIEW (SURAT PANGGILAN - FIXED TABLE) ---
const LettersView = ({ students, violations, config }) => {
const [formData, setFormData] = useState({
studentId: '', letterNo: '005/BK/2026', day: 'Senin', date: new Date().toISOString().split('T')[0],
time: '09:00 WIB', place: `Ruang BK ${config.schoolName||'Sekolah'}`, reason: 'Penyelesaian Masalah Kedisiplinan'
});
const [selectedStudent, setSelectedStudent] = useState(null);
const studentsWithPoints = useMemo(() => (students || []).map(s => ({ ...s, totalPoints: (violations || []).filter(v => String(v.studentId) === String(s.id)).reduce((acc, curr) => acc + Number(curr.points || 0), 0) })), [students, violations]);
const handleStudentChange = (e) => { const val = e.target.value; setFormData({...formData, studentId: val}); if(!val) { setSelectedStudent(null); return; } setSelectedStudent(studentsWithPoints.find(s => String(s.id) === String(val))); };
return (
<div className="flex gap-6 flex-col lg:flex-row">
{/* Input Form (Kiri) - HILANG SAAT PRINT */}
<div className="lg:w-1/3 bg-white p-6 rounded-xl shadow border h-fit no-print">
<h3 className="font-bold text-lg mb-4">Buat Surat</h3>
<div className="space-y-3">
<select className="w-full p-2 border rounded" value={formData.studentId} onChange={handleStudentChange}><option value="">-- Cari Nama Siswa --</option>{studentsWithPoints.map(s => <option key={s.id} value={s.id}>{s.name} - {s.class} (Poin: {s.totalPoints})</option>)}</select>
<input className="w-full p-2 border rounded" value={formData.letterNo} onChange={e=>setFormData({...formData, letterNo:e.target.value})} placeholder="No. Surat"/>
<input type="date" className="w-full p-2 border rounded" value={formData.date} onChange={e=>setFormData({...formData, date:e.target.value})}/>
<input className="w-full p-2 border rounded" value={formData.time} onChange={e=>setFormData({...formData, time:e.target.value})} placeholder="Waktu"/>
<input className="w-full p-2 border rounded" value={formData.reason} onChange={e=>setFormData({...formData, reason:e.target.value})} placeholder="Keperluan"/>
<button onClick={()=>window.print()} disabled={!selectedStudent} className="w-full bg-blue-600 text-white p-2 rounded flex justify-center gap-2 mt-2 disabled:bg-gray-300"><Icon name="printer" size={16}/> Cetak Surat</button>
</div>
</div>
{/* Preview Surat (Kanan) - FULL PAGE SAAT PRINT */}
<div className="lg:w-2/3 letter-container">
{selectedStudent ? (
<div className="paper-preview">
<FormalHeader config={config} title="" />
<div className="leading-relaxed text-black">
<div className="flex justify-between mb-8">
<table className="surat-table"><tbody><tr><td className="label-col">Nomor</td><td className="sep-col">:</td><td>{formData.letterNo}</td></tr><tr><td>Lamp</td><td>:</td><td>-</td></tr><tr><td>Hal</td><td>:</td><td><strong>Panggilan Orang Tua</strong></td></tr></tbody></table>
<div className="text-right">Kerinci, {new Date(formData.date).toLocaleDateString('id-ID', {day:'numeric', month:'long', year:'numeric'})}</div>
</div>
<div className="mb-6"><p>Yth. Orang Tua / Wali Murid dari :</p><p className="font-bold text-lg mt-2 underline uppercase">{selectedStudent.name}</p><p>Kelas : {selectedStudent.class}</p><p className="mt-1">di Tempat</p></div>
<p className="indent-12 mb-4 text-justify">Dengan hormat,<br/>Sehubungan dengan perlunya komunikasi dan kerjasama antara pihak sekolah dengan orang tua siswa demi perkembangan pendidikan dan kedisiplinan putra/putri Bapak/Ibu, maka kami mengharap kehadiran Bapak/Ibu ke sekolah pada:</p>
<div className="ml-12 mb-8 space-y-1 font-semibold">
<table className="surat-table"><tbody>
<tr><td className="label-col">Hari / Tanggal</td><td className="sep-col">:</td><td>{formData.day}, {new Date(formData.date).toLocaleDateString('id-ID', {day:'numeric', month:'long', year:'numeric'})}</td></tr>
<tr><td>Pukul</td><td>:</td><td>{formData.time}</td></tr>
<tr><td>Tempat</td><td>:</td><td>{formData.place}</td></tr>
<tr><td>Keperluan</td><td>:</td><td>{formData.reason}</td></tr>
</tbody></table>
</div>
<p className="indent-12 mb-16 text-justify">Mengingat pentingnya hal tersebut, dimohon Bapak/Ibu dapat hadir tepat pada waktunya. Atas perhatian dan kerjasamanya kami ucapkan terima kasih.</p>
<SignatureSection config={config} dateStr={formData.date} />
</div>
</div>
) : (
<div className="flex items-center justify-center h-full bg-white border border-dashed border-gray-300 rounded-xl no-print p-12">
<p className="text-gray-400">Silakan pilih siswa di sebelah kiri untuk melihat preview surat.</p>
</div>
)}
</div>
</div>
)
};
// --- REPORTS VIEW ---
const ReportsView = ({ students, violations, counselings, achievements, homeVisits, config }) => {
const [selectedStudentId, setSelectedStudentId] = useState("");
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
const violationsByClass = (violations||[]).reduce((acc, curr) => { const student = (students||[]).find(s => s.name === curr.studentName); const cls = student ? student.class : 'Lainnya'; acc[cls] = (acc[cls] || 0) + 1; return acc; }, {});
const maxVal = Math.max(...Object.values(violationsByClass), 1);
// Fungsi Generate PDF Individu dengan KOP Surat Resmi
const exportIndividualPDF = async () => {
if (!selectedStudentId) return;
const student = students.find(s => String(s.id) === String(selectedStudentId));
if (!student) return;
setIsGeneratingPdf(true);
// Persiapkan element KOP Surat tersembunyi
const kopElement = document.getElementById("hidden-kop-container");
document.getElementById("kop-logo-1").src = config.logoDaerah || "";
document.getElementById("kop-school-name").innerText = config.schoolName || "SEKOLAH";
document.getElementById("kop-school-address").innerText = config.schoolAddress || "Alamat Sekolah";
document.getElementById("kop-logo-2").src = config.logoSekolah || "";
try {
// Konversi Kop HTML ke Gambar (Canvas)
const canvas = await html2canvas(kopElement, { scale: 2, useCORS: true, allowTaint: true });
const imgData = canvas.toDataURL('image/png');
const doc = new jsPDF();
// Tambahkan Kop Surat
doc.addImage(imgData, 'PNG', 14, 10, 180, 25);
let currentY = 45; // Mulai teks di bawah Kop
doc.setFontSize(12); doc.setFont("times", "bold");
doc.text("LAPORAN INDIVIDU BIMBINGAN KONSELING", 105, currentY, { align: "center" });
currentY += 10;
doc.setFontSize(10); doc.setFont("times", "normal");
doc.text(`Nama : ${student.name}`, 14, currentY); currentY += 6;
doc.text(`NIS : ${student.nis}`, 14, currentY); currentY += 6;
doc.text(`Kelas : ${student.class}`, 14, currentY); currentY += 10;
const studentViolations = (violations||[]).filter(v => String(v.studentId) === String(selectedStudentId));
const studentCounselings = (counselings||[]).filter(v => String(v.studentId) === String(selectedStudentId));
const studentAchievements = (achievements||[]).filter(v => String(v.studentId) === String(selectedStudentId));
const studentHomeVisits = (homeVisits||[]).filter(v => String(v.studentId) === String(selectedStudentId));
// A. Pelanggaran
doc.setFont("times", "bold"); doc.text("A. Riwayat Pelanggaran", 14, currentY); doc.setFont("times", "normal");
if (studentViolations.length > 0) {
const vBody = studentViolations.map((v, i) => [i+1, v.date, v.type, v.points, v.note || '-']);
doc.autoTable({ startY: currentY + 4, head: [['No', 'Tanggal', 'Pelanggaran', 'Poin', 'Catatan']], body: vBody, theme: 'grid', styles: { fontSize: 9, font: 'times' }, headStyles: { fillColor: [209, 213, 219], textColor: [0,0,0] } });
currentY = doc.lastAutoTable.finalY + 10;
} else { currentY += 6; doc.text("- Tidak ada catatan pelanggaran -", 14, currentY); currentY += 10; }
// B. Konseling
doc.setFont("times", "bold"); doc.text("B. Riwayat Konseling", 14, currentY); doc.setFont("times", "normal");
if (studentCounselings.length > 0) {
const cBody = studentCounselings.map((c, i) => [i+1, c.date, c.topic, c.time || '-', c.status || '-']);
doc.autoTable({ startY: currentY + 4, head: [['No', 'Tanggal', 'Topik', 'Waktu', 'Status']], body: cBody, theme: 'grid', styles: { fontSize: 9, font: 'times' }, headStyles: { fillColor: [209, 213, 219], textColor: [0,0,0] } });
currentY = doc.lastAutoTable.finalY + 10;
} else { currentY += 6; doc.text("- Tidak ada catatan konseling -", 14, currentY); currentY += 10; }
if (currentY > 230) { doc.addPage(); currentY = 20; }
// C. Prestasi
doc.setFont("times", "bold"); doc.text("C. Riwayat Prestasi", 14, currentY); doc.setFont("times", "normal");
if (studentAchievements.length > 0) {
const aBody = studentAchievements.map((a, i) => [i+1, a.date, a.title, a.level || '-']);
doc.autoTable({ startY: currentY + 4, head: [['No', 'Tanggal', 'Prestasi', 'Tingkat']], body: aBody, theme: 'grid', styles: { fontSize: 9, font: 'times' }, headStyles: { fillColor: [209, 213, 219], textColor: [0,0,0] } });
currentY = doc.lastAutoTable.finalY + 10;
} else { currentY += 6; doc.text("- Tidak ada catatan prestasi -", 14, currentY); currentY += 10; }
if (currentY > 230) { doc.addPage(); currentY = 20; }
// D. Home Visit
doc.setFont("times", "bold"); doc.text("D. Riwayat Home Visit", 14, currentY); doc.setFont("times", "normal");
if (studentHomeVisits.length > 0) {
const hBody = studentHomeVisits.map((h, i) => [i+1, h.date, h.parentName || '-', h.result || '-']);
doc.autoTable({ startY: currentY + 4, head: [['No', 'Tanggal', 'Nama Wali', 'Hasil/Catatan']], body: hBody, theme: 'grid', styles: { fontSize: 9, font: 'times' }, headStyles: { fillColor: [209, 213, 219], textColor: [0,0,0] } });
currentY = doc.lastAutoTable.finalY + 15;
} else { currentY += 6; doc.text("- Tidak ada catatan home visit -", 14, currentY); currentY += 15; }
if (currentY > 230) { doc.addPage(); currentY = 20; }
// Tanda Tangan
const today = new Date().toLocaleDateString('id-ID', {day: 'numeric', month: 'long', year: 'numeric'});
doc.text("Mengetahui,", 30, currentY);
doc.text(`Kerinci, ${today}`, 130, currentY);
doc.text("Kepala Sekolah", 30, currentY + 5);
doc.text("Guru Bimbingan Konseling", 130, currentY + 5);
doc.setFont("times", "bold");
doc.text(config.kepsekName || "", 30, currentY + 25);
doc.text(config.guruName || "", 130, currentY + 25);
doc.setFont("times", "normal");
doc.text(`NIP. ${config.kepsekNip || ""}`, 30, currentY + 30);
doc.text(`NIP. ${config.guruNip || ""}`, 130, currentY + 30);
doc.save(`Laporan_Individu_${student.name.replace(/\s+/g, '_')}.pdf`);
} catch (err) {
console.error(err);
alert("Gagal memuat logo untuk PDF. Pastikan Link Logo valid.");
}
setIsGeneratingPdf(false);
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center no-print">
<h2 className="text-2xl font-bold text-slate-800">Laporan & Statistik</h2>
<button onClick={()=>window.print()} className="bg-slate-800 text-white px-4 py-2 rounded-lg flex items-center gap-2"><Icon name="printer" size={18} /> Cetak Visual</button>
</div>
<div className="print-block hidden"><FormalHeader config={config} title="LAPORAN REKAPITULASI BK" /></div>
<div className="bg-white p-8 rounded-xl shadow-sm border border-slate-200 card-box">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="p-4 border rounded-lg">
<h3 className="font-bold mb-4">Statistik Pelanggaran per Kelas</h3>
{Object.entries(violationsByClass).map(([cls, count]) => (
<div key={cls} className="mb-2">
<div className="flex justify-between text-xs mb-1"><span>{cls}</span><span>{count}</span></div>
<div className="w-full bg-slate-100 h-2 rounded"><div className="bg-blue-600 h-2 rounded print:bg-black" style={{width: `${(count/maxVal)*100}%`}}></div></div>
</div>
))}
</div>
<div className="p-4 border rounded-lg">
<h3 className="font-bold mb-4">Ringkasan Data</h3>
<table className="w-full text-sm data-table"><tbody>
<tr className="border-b"><td className="py-2">Total Siswa</td><td className="font-bold text-right">{students.length}</td></tr>
<tr className="border-b"><td className="py-2">Total Pelanggaran</td><td className="font-bold text-right">{violations.length}</td></tr>
<tr className="border-b"><td className="py-2">Total Konseling</td><td className="font-bold text-right">{counselings.length}</td></tr>
<tr className="border-b"><td className="py-2">Prestasi Tercatat</td><td className="font-bold text-right">{achievements.length}</td></tr>
</tbody></table>
</div>
</div>
<div className="print-block hidden"><SignatureSection config={config} /></div>
</div>
{/* NEW SECTION: LAPORAN INDIVIDU */}
<div className="no-print p-6 bg-slate-50 rounded-xl border mt-6">
<h3 className="font-bold text-lg mb-4 flex gap-2"><Icon name="user" size={20}/> Download Laporan Individu</h3>
<div className="flex flex-col sm:flex-row gap-4 items-center">
<select className="w-full sm:w-2/3 p-2 border rounded" value={selectedStudentId} onChange={e=>setSelectedStudentId(e.target.value)}>
<option value="">-- Pilih Siswa --</option>
{(students||[]).map(s=><option key={s.id} value={s.id}>{s.name} - {s.class}</option>)}
</select>
<button onClick={exportIndividualPDF} disabled={!selectedStudentId || isGeneratingPdf} className="w-full sm:w-1/3 bg-blue-600 hover:bg-blue-700 text-white p-2 rounded flex justify-center gap-2 disabled:bg-gray-300">
<Icon name="download" size={18}/> {isGeneratingPdf ? 'Memproses...' : 'Download PDF'}
</button>
</div>
</div>
</div>
)
};
// --- SIDEBAR ---
const Sidebar = ({ activeTab, setActiveTab, isOpen, setIsOpen, isMobile, config }) => {
const menuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: 'layout-dashboard' },
{ id: 'monitoring', label: 'Pantauan Siswa', icon: 'eye' },
{ id: 'students', label: 'Data Siswa', icon: 'users' },
{ id: 'violations', label: 'Pelanggaran', icon: 'alert-triangle' },
{ id: 'letters', label: 'Surat Panggilan', icon: 'mail' },
{ id: 'achievements', label: 'Prestasi Siswa', icon: 'award' },
{ id: 'counseling', label: 'Jadwal Konseling', icon: 'calendar' },
{ id: 'homevisit', label: 'Home Visit', icon: 'home' },
{ id: 'reports', label: 'Laporan & Export', icon: 'file-text' },
{ id: 'config', label: 'Pengaturan', icon: 'settings' },
];
return (
<>
{isMobile && isOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-20 no-print" onClick={() => setIsOpen(false)} />}
<div className={`fixed md:static inset-y-0 left-0 z-30 w-64 bg-slate-900 text-white transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'} flex flex-col h-full shadow-xl no-print`}>
<div className="p-6 border-b border-slate-800 flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center font-bold text-xl">BK</div>
<div><h1 className="font-bold text-lg leading-tight">SIP-BK</h1><p className="text-xs text-slate-400">Integrated System</p></div>
{isMobile && <button onClick={()=>setIsOpen(false)} className="ml-auto md:hidden"><Icon name="x" size={20}/></button>}
</div>
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
{menuItems.map((item) => (<button key={item.id} onClick={() => { setActiveTab(item.id); if (isMobile) setIsOpen(false); }} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === item.id ? 'bg-blue-600 text-white shadow-md' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}><Icon name={item.icon} size={20} /> <span className="font-medium">{item.label}</span></button>))}
</nav>
<div className="p-4 border-t border-slate-800 text-center text-xs text-slate-500 truncate px-2">{config.guruName || 'Guru BK'}</div>
</div>
</>
);
};
// --- MAIN APP ---
const GuruBKApp = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [config, setConfig] = useState({});
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('dashboard');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [students, setStudents] = useState([]);
const [violations, setViolations] = useState([]);
const [counselings, setCounselings] = useState([]);
const [achievements, setAchievements] = useState([]);
const [homeVisits, setHomeVisits] = useState([]);
useEffect(() => { lucide.createIcons(); }, [activeTab, students, violations, loading, isLoggedIn, config]);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768); checkMobile(); window.addEventListener('resize', checkMobile);
google.script.run.withSuccessHandler((data) => { setConfig(data.config || {}); setStudents(data.students||[]); setViolations(data.violations||[]); setCounselings(data.counselings||[]); setAchievements(data.achievements||[]); setHomeVisits(data.homeVisits||[]); setLoading(false); }).getInitialData();
return () => window.removeEventListener('resize', checkMobile);
}, []);
const handleAddStudent = (s) => { const n={...s, status:'Aktif', points:0, id:Date.now()}; setStudents([...students,n]); google.script.run.addDataToSheet("Data_Siswa",n); };
const handleBulkImport = (data) => { google.script.run.withSuccessHandler((updated) => setStudents(updated)).importBulkStudents(data); };
const handleDeleteStudent = (id) => { if(confirm("Hapus?")) { setStudents(students.filter(s=>s.id!==id)); google.script.run.deleteDataFromSheet("Data_Siswa", id); }};
const handleAddViolation = (v) => { const n={...v, id:Date.now()}; setViolations([...violations,n]); google.script.run.addDataToSheet("Data_Pelanggaran",n); };
const handleAddGeneric = (t, d, set, list, sheet) => { const n={...d, id:Date.now()}; set([...list,n]); google.script.run.addDataToSheet(sheet,n); };
const handleSaveConfig = (newConfig) => { setConfig(newConfig); google.script.run.withSuccessHandler(updated => setConfig(updated)).saveConfig(newConfig); };
const renderContent = () => {
switch (activeTab) {
case 'dashboard': return <DashboardView students={students} violations={violations} counselings={counselings} achievements={achievements} config={config} />;
case 'config': return <ConfigView config={config} onSave={handleSaveConfig} />;
case 'students': return <StudentsView students={students} onAddStudent={handleAddStudent} onDeleteStudent={handleDeleteStudent} onBulkImport={handleBulkImport} config={config} />;
case 'violations': return <ViolationsView violations={violations} students={students} onAddViolation={handleAddViolation} config={config} />;
case 'monitoring': return <MonitoringView students={students} violations={violations} config={config} />;
case 'letters': return <LettersView students={students} violations={violations} config={config} />;
case 'reports': return <ReportsView students={students} violations={violations} counselings={counselings} achievements={achievements} homeVisits={homeVisits} config={config} />;
case 'achievements': return <GenericFormView title="Prestasi Siswa" type="achievement" dataList={achievements} students={students} onAdd={(d)=>handleAddGeneric('achievement', d, setAchievements, achievements, 'Data_Prestasi')} config={config} />;
case 'counseling': return <GenericFormView title="Jadwal Konseling" type="counseling" dataList={counselings} students={students} onAdd={(d)=>handleAddGeneric('counseling', d, setCounselings, counselings, 'Data_Konseling')} config={config} />;
case 'homevisit': return <GenericFormView title="Home Visit" type="homevisit" dataList={homeVisits} students={students} onAdd={(d)=>handleAddGeneric('homevisit', d, setHomeVisits, homeVisits, 'Data_HomeVisit')} config={config} />;
default: return <DashboardView />;
}
};
if (loading) return <div className="flex h-screen w-full items-center justify-center bg-slate-100"><div className="text-center animate-pulse"><div className="w-16 h-16 bg-slate-300 rounded-full mx-auto mb-4"></div><h2 className="text-xl font-bold text-slate-500">Memuat Data...</h2></div></div>;
if (!isLoggedIn) return <LoginView onLogin={(status) => setIsLoggedIn(status)} config={config} />;
return (
<div className="flex h-screen bg-slate-100 font-sans text-slate-900">
<Sidebar activeTab={activeTab} setActiveTab={setActiveTab} isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} isMobile={isMobile} config={config} />
<div className="flex-1 flex flex-col h-screen overflow-hidden print:overflow-visible">
<header className="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-6 z-10 print:hidden">
<div className="flex items-center gap-4">
{isMobile && (<button onClick={() => setIsSidebarOpen(true)} className="text-slate-500 hover:text-slate-800"><Icon name="menu" size={24} /></button>)}
<h2 className="text-lg font-bold text-slate-800 hidden sm:block">Aplikasi Bimbingan Konseling</h2>
</div>
<div className="flex items-center gap-3 pl-4 border-l border-slate-200">
<div className="text-right hidden sm:block"><p className="text-sm font-bold text-slate-800">{config.guruName}</p><p className="text-xs text-slate-500">Guru Pembimbing</p></div>
<div className="w-9 h-9 bg-slate-200 rounded-full flex items-center justify-center text-slate-500"><Icon name="users" size={18} /></div>
</div>
</header>
<main className="flex-1 overflow-y-auto p-4 md:p-8 print:p-0 print:overflow-visible"><div className="max-w-7xl mx-auto print:max-w-none print:mx-0">{renderContent()}</div></main>
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<GuruBKApp />);
</script>
</body>
</html>
Langkah 5: Publish Aplikasi (Deploy)
- Di pojok kanan atas Apps Script, klik tombol biru Terapkan (Deploy) > Deployment Baru.
- Klik ikon roda gigi (Pilih jenis), lalu pilih Aplikasi Web.
- Pada bagian Yang memiliki akses, pilih Siapa Saja (Anyone).
- Klik Terapkan dan berikan otorisasi izin akses (Pilih Lanjutan > Lanjutkan ke... tidak aman).
- Selesai! Aplikasi siap digunakan. Password default adalah 123456.
