diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..d9de4da --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,42 @@ + +body { + font-family: 'Inter', sans-serif; + background-color: #F3F4F6; +} + +.sidebar { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 250px; + background-color: #FFFFFF; + padding-top: 1rem; + border-right: 1px solid #e5e7eb; +} + +.sidebar .nav-link { + color: #4B5563; + font-weight: 500; +} + +.sidebar .nav-link.active, +.sidebar .nav-link:hover { + color: #4F46E5; + background-color: #EEF2FF; +} + +.sidebar .nav-link .feather { + margin-right: 1rem; + color: #9CA3AF; +} + +.sidebar .nav-link.active .feather, +.sidebar .nav-link:hover .feather { + color: #4F46E5; +} + +.main-content { + margin-left: 250px; + padding: 2rem; +} diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..ca064ba --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,6 @@ + +(function () { + 'use strict' + + feather.replace() +})() diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..b60cfdf --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,28 @@ + false, 'message' => 'File migrasi tidak ditemukan.']; + } + + $sql = file_get_contents($migration_file); + $pdo->exec($sql); + + return ['success' => true, 'message' => 'Migrasi tabel `murid` berhasil dijalankan.']; + } catch (PDOException $e) { + return ['success' => false, 'message' => 'Migrasi gagal: ' . $e->getMessage()]; + } +} + +// Jalankan migrasi jika file ini dieksekusi langsung dari CLI atau via include +if (basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) { + $result = run_migrations(); + echo $result['message'] . "\n"; +} + diff --git a/db/migrations/001_create_murid_table.sql b/db/migrations/001_create_murid_table.sql new file mode 100644 index 0000000..223c291 --- /dev/null +++ b/db/migrations/001_create_murid_table.sql @@ -0,0 +1,16 @@ +-- 001_create_murid_table.sql +CREATE TABLE IF NOT EXISTS `murid` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `nis` VARCHAR(20) NOT NULL UNIQUE, + `nama_lengkap` VARCHAR(100) NOT NULL, + `tempat_lahir` VARCHAR(50) DEFAULT NULL, + `tanggal_lahir` DATE DEFAULT NULL, + `jenis_kelamin` ENUM('Laki-laki', 'Perempuan') DEFAULT NULL, + `alamat` TEXT DEFAULT NULL, + `no_telepon` VARCHAR(20) DEFAULT NULL, + `email` VARCHAR(100) DEFAULT NULL, + `foto` VARCHAR(255) DEFAULT 'default.png', -- Path to photo file + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/002_add_location_and_photo_to_murid.sql b/db/migrations/002_add_location_and_photo_to_murid.sql new file mode 100644 index 0000000..26e8a50 --- /dev/null +++ b/db/migrations/002_add_location_and_photo_to_murid.sql @@ -0,0 +1,5 @@ +ALTER TABLE `murid` +ADD COLUMN `nisn` VARCHAR(20) NULL AFTER `nis`, +ADD COLUMN `foto` VARCHAR(255) NULL AFTER `alamat`, +ADD COLUMN `latitude` VARCHAR(50) NULL AFTER `foto`, +ADD COLUMN `longitude` VARCHAR(50) NULL AFTER `latitude`; diff --git a/db/migrations/003_create_guru_table.sql b/db/migrations/003_create_guru_table.sql new file mode 100644 index 0000000..49206e8 --- /dev/null +++ b/db/migrations/003_create_guru_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS guru ( + id INT AUTO_INCREMENT PRIMARY KEY, + nip VARCHAR(50) UNIQUE NOT NULL, + nama VARCHAR(100) NOT NULL, + alamat TEXT, + telepon VARCHAR(20), + foto VARCHAR(255), + latitude VARCHAR(50), + longitude VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); diff --git a/db/migrations/004_create_kelas_table.sql b/db/migrations/004_create_kelas_table.sql new file mode 100644 index 0000000..4c839eb --- /dev/null +++ b/db/migrations/004_create_kelas_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `kelas` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `nama_kelas` VARCHAR(100) NOT NULL, + `id_wali_kelas` INT, + FOREIGN KEY (`id_wali_kelas`) REFERENCES `guru`(`id`) ON DELETE SET NULL +) ENGINE=InnoDB; diff --git a/db/migrations/005_create_jadwal_table.sql b/db/migrations/005_create_jadwal_table.sql new file mode 100644 index 0000000..2295c6f --- /dev/null +++ b/db/migrations/005_create_jadwal_table.sql @@ -0,0 +1,16 @@ +-- +-- Create table for schedule (jadwal) +-- +CREATE TABLE IF NOT EXISTS `jadwal` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `id_kelas` INT NOT NULL, + `id_guru` INT NOT NULL, + `hari` VARCHAR(10) NOT NULL, + `jam_mulai` TIME NOT NULL, + `jam_selesai` TIME NOT NULL, + `mata_pelajaran` VARCHAR(100) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (`id_kelas`) REFERENCES `kelas`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`id_guru`) REFERENCES `guru`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/006_create_absensi_table.sql b/db/migrations/006_create_absensi_table.sql new file mode 100644 index 0000000..45cfda1 --- /dev/null +++ b/db/migrations/006_create_absensi_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS absensi ( + id INT AUTO_INCREMENT PRIMARY KEY, + jadwal_id INT NOT NULL, + murid_id INT NOT NULL, + tanggal DATE NOT NULL, + status ENUM('Hadir', 'Izin', 'Sakit', 'Alpa') NOT NULL, + keterangan TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (jadwal_id) REFERENCES jadwal(id) ON DELETE CASCADE, + FOREIGN KEY (murid_id) REFERENCES murid(id) ON DELETE CASCADE, + UNIQUE KEY (jadwal_id, murid_id, tanggal) +); \ No newline at end of file diff --git a/db/migrations/007_add_kelas_id_to_murid.sql b/db/migrations/007_add_kelas_id_to_murid.sql new file mode 100644 index 0000000..afe594b --- /dev/null +++ b/db/migrations/007_add_kelas_id_to_murid.sql @@ -0,0 +1,3 @@ +ALTER TABLE murid +ADD COLUMN kelas_id INT NULL AFTER email, +ADD FOREIGN KEY (kelas_id) REFERENCES kelas(id) ON DELETE SET NULL; \ No newline at end of file diff --git a/db/migrations/008_create_matapelajaran_table.sql b/db/migrations/008_create_matapelajaran_table.sql new file mode 100644 index 0000000..5542f2f --- /dev/null +++ b/db/migrations/008_create_matapelajaran_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `mata_pelajaran` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `nama_matapelajaran` VARCHAR(100) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=INNODB; diff --git a/db/migrations/009_add_matapelajaran_to_jadwal.sql b/db/migrations/009_add_matapelajaran_to_jadwal.sql new file mode 100644 index 0000000..27ab20c --- /dev/null +++ b/db/migrations/009_add_matapelajaran_to_jadwal.sql @@ -0,0 +1,6 @@ +ALTER TABLE `jadwal` +ADD COLUMN `id_matapelajaran` INT NULL AFTER `id_kelas`, +ADD CONSTRAINT `fk_jadwal_matapelajaran` + FOREIGN KEY (`id_matapelajaran`) + REFERENCES `mata_pelajaran`(`id`) + ON DELETE SET NULL; diff --git a/export_laporan.php b/export_laporan.php new file mode 100644 index 0000000..14a9c01 --- /dev/null +++ b/export_laporan.php @@ -0,0 +1,61 @@ +NIS', + 'Nama Lengkap', + 'Hadir', + 'Sakit', + 'Izin', + 'Alpa', + 'Total Jam' + ] +]; + +if (empty($rekap_data)) { + $data_for_excel[] = ['Tidak ada data absensi pada periode ini.', '', '', '', '', '', '']; +} else { + foreach ($rekap_data as $row) { + $data_for_excel[] = [ + $row['nis'], + $row['nama_lengkap'], + (int)$row['total_hadir'], + (int)$row['total_sakit'], + (int)$row['total_izin'], + (int)$row['total_alpa'], + (int)$row['total_absensi'], + ]; + } +} + +// Generate file name +$filename = "Laporan_Absensi_{$nama_kelas}_{$start_date}_to_{$end_date}.xlsx"; + +// Generate and download the Excel file +Shuchkin\SimpleXLSXGen::fromArray($data_for_excel)->downloadAs($filename); + +exit(); diff --git a/export_murid.php b/export_murid.php new file mode 100644 index 0000000..46e7909 --- /dev/null +++ b/export_murid.php @@ -0,0 +1,34 @@ +downloadAs('data_murid_'.date('Ymd').'.xlsx'); // Unduh dengan nama file dinamis diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..c088ec3 --- /dev/null +++ b/functions.php @@ -0,0 +1,658 @@ + +query("SELECT m.id, m.nis, m.nisn, m.nama_lengkap, m.jenis_kelamin, m.email, m.foto, k.nama_kelas + FROM murid m + LEFT JOIN kelas k ON m.kelas_id = k.id + ORDER BY m.nama_lengkap ASC"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_murid_by_id($id) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM murid WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} + +function handle_murid_action() { + $action = $_POST['action'] ?? $_GET['action'] ?? ''; + + // Handle Delete Action via GET first as it redirects + if ($action === 'delete' && isset($_GET['id'])) { + $id = $_GET['id']; + $murid = get_murid_by_id($id); // Get murid data to delete foto + $pdo = db(); + try { + // Delete old photo if exists + if (!empty($murid['foto'])) { + $photo_path = 'assets/uploads/murid/' . $murid['foto']; + if (file_exists($photo_path)) { + unlink($photo_path); + } + } + $stmt = $pdo->prepare("DELETE FROM murid WHERE id = ?"); + $stmt->execute([$id]); + header("Location: index.php?page=murid&status=deleted"); + exit(); + } catch (PDOException $e) { + header("Location: index.php?page=murid&status=error"); + exit(); + } + } + + // Only proceed with POST actions from here + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return null; + } + + $pdo = db(); + $id = $_POST['id'] ?? null; + + // Handle Import + if ($action === 'import') { + if (isset($_FILES['file_murid']) && $_FILES['file_murid']['error'] === UPLOAD_ERR_OK) { + $file_tmp_path = $_FILES['file_murid']['tmp_name']; + return import_murid_from_xlsx($file_tmp_path); + } else { + return ['success' => false, 'message' => 'Gagal mengunggah file. Silakan coba lagi.']; + } + } + + // Handle Add/Edit + $nis = $_POST['nis'] ?? ''; + $nisn = $_POST['nisn'] ?? ''; + $nama_lengkap = $_POST['nama_lengkap'] ?? ''; + $tempat_lahir = $_POST['tempat_lahir'] ?? ''; + $tanggal_lahir = $_POST['tanggal_lahir'] ?? ''; + $jenis_kelamin = $_POST['jenis_kelamin'] ?? ''; + $alamat = $_POST['alamat'] ?? ''; + $latitude = $_POST['latitude'] ?? ''; + $longitude = $_POST['longitude'] ?? ''; + $no_telepon = $_POST['no_telepon'] ?? ''; + $email = $_POST['email'] ?? ''; + $kelas_id = !empty($_POST['kelas_id']) ? $_POST['kelas_id'] : null; + + + $foto_filename = $_POST['foto_existing'] ?? null; + + // Handle file upload + if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) { + $upload_dir = 'assets/uploads/murid/'; + if (!is_dir($upload_dir)) { + mkdir($upload_dir, 0777, true); + } + + $tmp_name = $_FILES['foto']['tmp_name']; + $original_name = basename($_FILES['foto']['name']); + $file_ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION)); + $safe_filename = preg_replace('/[^A-Za-z0-9_.-]/', '_', pathinfo($original_name, PATHINFO_FILENAME)); + $foto_filename = uniqid() . '_' . $safe_filename . '.' . $file_ext; + + // Delete old photo if a new one is uploaded during edit + if ($action === 'edit' && !empty($_POST['foto_existing'])) { + $old_photo_path = $upload_dir . $_POST['foto_existing']; + if (file_exists($old_photo_path)) { + unlink($old_photo_path); + } + } + + if (!move_uploaded_file($tmp_name, $upload_dir . $foto_filename)) { + return ['success' => false, 'message' => 'Gagal memindahkan file foto.']; + } + } + + try { + if ($action === 'add') { + $stmt = $pdo->prepare( + "INSERT INTO murid (nis, nisn, nama_lengkap, tempat_lahir, tanggal_lahir, jenis_kelamin, alamat, foto, latitude, longitude, no_telepon, email, kelas_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + ); + $stmt->execute([$nis, $nisn, $nama_lengkap, $tempat_lahir, $tanggal_lahir, $jenis_kelamin, $alamat, $foto_filename, $latitude, $longitude, $no_telepon, $email, $kelas_id]); + return ['success' => true, 'message' => 'Data murid berhasil ditambahkan.']; + } elseif ($action === 'edit' && $id) { + $stmt = $pdo->prepare( + "UPDATE murid SET nis = ?, nisn = ?, nama_lengkap = ?, tempat_lahir = ?, tanggal_lahir = ?, + jenis_kelamin = ?, alamat = ?, foto = ?, latitude = ?, longitude = ?, no_telepon = ?, email = ?, kelas_id = ? WHERE id = ?" + ); + $stmt->execute([$nis, $nisn, $nama_lengkap, $tempat_lahir, $tanggal_lahir, $jenis_kelamin, $alamat, $foto_filename, $latitude, $longitude, $no_telepon, $email, $kelas_id, $id]); + return ['success' => true, 'message' => 'Data murid berhasil diperbarui.']; + } + } catch (PDOException $e) { + if ($e->getCode() == 23000) { // Integrity constraint violation + return ['success' => false, 'message' => 'Gagal menyimpan data. NIS atau NISN sudah terdaftar.']; + } + return ['success' => false, 'message' => 'Terjadi kesalahan database: ' . $e->getMessage()]; + } + + return null; +} + +function import_murid_from_xlsx($filepath) { + // Include the parser library + require_once 'libs/SimpleXLSX.php'; + + // Check if the file exists and is readable + if (!file_exists($filepath) || !is_readable($filepath)) { + return ['success' => false, 'message' => 'File tidak ditemukan atau tidak bisa dibaca.']; + } + + // Try to parse the XLSX file + if ($xlsx = Shuchkin\SimpleXLSX::parse($filepath)) { + $pdo = db(); + $success_count = 0; + $fail_count = 0; + $errors = []; + + // Get rows as array + $rows = $xlsx->rows(); + + // Skip header row (the first row) + $is_header = true; + + foreach ($rows as $row) { + if ($is_header) { + $is_header = false; + continue; + } + + // Assign columns to variables - assuming order: NIS, NISN, Nama, Tgl Lahir, Alamat + $nis = $row[0] ?? ''; + $nisn = $row[1] ?? ''; + $nama_lengkap = $row[2] ?? ''; + $tanggal_lahir = $row[3] ?? ''; + $alamat = $row[4] ?? ''; + + // Basic validation: skip if NIS or name is empty + if (empty($nis) || empty($nama_lengkap)) { + $fail_count++; + $errors[] = "Baris data dengan NIS '.htmlspecialchars($nis).' dan Nama '.htmlspecialchars($nama_lengkap).' dilewati karena data tidak lengkap."; + continue; + } + + try { + // Using the existing add function logic + $stmt = $pdo->prepare( + "INSERT INTO murid (nis, nisn, nama_lengkap, tanggal_lahir, alamat) + VALUES (?, ?, ?, ?, ?)" + ); + $stmt->execute([$nis, $nisn, $nama_lengkap, $tanggal_lahir, $alamat]); + $success_count++; + } catch (PDOException $e) { + $fail_count++; + if ($e->getCode() == 23000) { + $errors[] = "NIS atau NISN '.htmlspecialchars($nis).' sudah ada di database."; + } else { + $errors[] = "Gagal memasukkan data untuk NIS '.htmlspecialchars($nis).': " . $e->getMessage(); + } + } + } + + $message = "Impor selesai. Berhasil: {$success_count}, Gagal: {$fail_count}."; + if (!empty($errors)) { + $message .= "
Detail kegagalan:
" . implode("
", $errors); + } + + return ['success' => ($fail_count === 0), 'message' => $message]; + + } else { + return ['success' => false, 'message' => 'Gagal memproses file XLSX: ' . Shuchkin\SimpleXLSX::parseError()]; + } +} + +function get_all_guru() { + $pdo = db(); + $stmt = $pdo->query("SELECT id, nip, nama, foto FROM guru ORDER BY nama ASC"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_guru_by_id($id) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM guru WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} + +function handle_guru_action() { + $action = $_POST['action'] ?? $_GET['action'] ?? ''; + + if ($action === 'delete' && isset($_GET['id'])) { + $id = $_GET['id']; + $guru = get_guru_by_id($id); + $pdo = db(); + try { + if (!empty($guru['foto'])) { + $photo_path = 'assets/uploads/guru/' . $guru['foto']; + if (file_exists($photo_path)) { + unlink($photo_path); + } + } + $stmt = $pdo->prepare("DELETE FROM guru WHERE id = ?"); + $stmt->execute([$id]); + header("Location: index.php?page=teachers&status=deleted"); + exit(); + } catch (PDOException $e) { + header("Location: index.php?page=teachers&status=error"); + exit(); + } + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return null; + } + + $pdo = db(); + $id = $_POST['id'] ?? null; + $nip = $_POST['nip'] ?? ''; + $nama = $_POST['nama'] ?? ''; + $alamat = $_POST['alamat'] ?? ''; + $telepon = $_POST['telepon'] ?? ''; + $latitude = $_POST['latitude'] ?? ''; + $longitude = $_POST['longitude'] ?? ''; + + $foto_filename = $_POST['foto_existing'] ?? null; + + if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) { + $upload_dir = 'assets/uploads/guru/'; + if (!is_dir($upload_dir)) { + mkdir($upload_dir, 0777, true); + } + + $tmp_name = $_FILES['foto']['tmp_name']; + $original_name = basename($_FILES['foto']['name']); + $file_ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION)); + $safe_filename = preg_replace('/[^A-Za-z0-9_.-]/', '_', pathinfo($original_name, PATHINFO_FILENAME)); + $foto_filename = uniqid() . '_' . $safe_filename . '.' . $file_ext; + + if ($action === 'edit' && !empty($_POST['foto_existing'])) { + $old_photo_path = $upload_dir . $_POST['foto_existing']; + if (file_exists($old_photo_path)) { + unlink($old_photo_path); + } + } + + if (!move_uploaded_file($tmp_name, $upload_dir . $foto_filename)) { + return ['success' => false, 'message' => 'Gagal memindahkan file foto.']; + } + } + + try { + if ($action === 'add') { + $stmt = $pdo->prepare( + "INSERT INTO guru (nip, nama, alamat, telepon, foto, latitude, longitude) + VALUES (?, ?, ?, ?, ?, ?, ?)" + ); + $stmt->execute([$nip, $nama, $alamat, $telepon, $foto_filename, $latitude, $longitude]); + return ['success' => true, 'message' => 'Data guru berhasil ditambahkan.']; + } elseif ($action === 'edit' && $id) { + $stmt = $pdo->prepare( + "UPDATE guru SET nip = ?, nama = ?, alamat = ?, telepon = ?, foto = ?, latitude = ?, longitude = ? WHERE id = ?" + ); + $stmt->execute([$nip, $nama, $alamat, $telepon, $foto_filename, $latitude, $longitude, $id]); + return ['success' => true, 'message' => 'Data guru berhasil diperbarui.']; + } + } catch (PDOException $e) { + if ($e->getCode() == 23000) { // Integrity constraint violation + return ['success' => false, 'message' => 'Gagal menyimpan data. NIP sudah terdaftar.']; + } + return ['success' => false, 'message' => 'Terjadi kesalahan database: ' . $e->getMessage()]; + } + + return null; +} + +function get_all_kelas() { + $pdo = db(); + $stmt = $pdo->query(" + SELECT k.id, k.nama_kelas, g.nama as nama_wali_kelas + FROM kelas k + LEFT JOIN guru g ON k.id_wali_kelas = g.id + ORDER BY k.nama_kelas ASC + "); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_kelas_by_id($id) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM kelas WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} + +function handle_kelas_action() { + $action = $_POST['action'] ?? $_GET['action'] ?? ''; + + if ($action === 'delete' && isset($_GET['id'])) { + $id = $_GET['id']; + $pdo = db(); + try { + $stmt = $pdo->prepare("DELETE FROM kelas WHERE id = ?"); + $stmt->execute([$id]); + header("Location: index.php?page=classes&status=deleted"); + exit(); + } catch (PDOException $e) { + // Foreign key constraint might fail, though ON DELETE SET NULL should prevent it. + // Good to have a generic error handler. + header("Location: index.php?page=classes&status=error"); + exit(); + } + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return null; + } + + $pdo = db(); + $id = $_POST['id'] ?? null; + $nama_kelas = $_POST['nama_kelas'] ?? ''; + // Use 'none' or null for the wali kelas if not selected + $id_wali_kelas = !empty($_POST['id_wali_kelas']) ? $_POST['id_wali_kelas'] : null; + + try { + if ($action === 'add') { + $stmt = $pdo->prepare( + "INSERT INTO kelas (nama_kelas, id_wali_kelas) VALUES (?, ?)" + ); + $stmt->execute([$nama_kelas, $id_wali_kelas]); + return ['success' => true, 'message' => 'Data kelas berhasil ditambahkan.']; + } elseif ($action === 'edit' && $id) { + $stmt = $pdo->prepare( + "UPDATE kelas SET nama_kelas = ?, id_wali_kelas = ? WHERE id = ?" + ); + $stmt->execute([$nama_kelas, $id_wali_kelas, $id]); + return ['success' => true, 'message' => 'Data kelas berhasil diperbarui.']; + } + } catch (PDOException $e) { + if ($e->getCode() == 23000) { + return ['success' => false, 'message' => 'Gagal menyimpan data. Nama kelas mungkin sudah ada.']; + } + return ['success' => false, 'message' => 'Terjadi kesalahan database: ' . $e->getMessage()]; + } + + return null; +} + + +function get_all_jadwal() { + $pdo = db(); + $stmt = $pdo->query(" + SELECT + j.id, + j.hari, + j.jam_mulai, + j.jam_selesai, + mp.nama_matapelajaran, + k.nama_kelas, + g.nama as nama_guru + FROM jadwal j + JOIN kelas k ON j.id_kelas = k.id + JOIN guru g ON j.id_guru = g.id + LEFT JOIN mata_pelajaran mp ON j.id_matapelajaran = mp.id + ORDER BY k.nama_kelas, FIELD(j.hari, 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'), j.jam_mulai + "); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_jadwal_by_id($id) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM jadwal WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} + +function handle_jadwal_action() { + $action = $_POST['action'] ?? $_GET['action'] ?? ''; + + if ($action === 'delete' && isset($_GET['id'])) { + $id = $_GET['id']; + $pdo = db(); + try { + $stmt = $pdo->prepare("DELETE FROM jadwal WHERE id = ?"); + $stmt->execute([$id]); + header("Location: index.php?page=schedule&status=deleted"); + exit(); + } catch (PDOException $e) { + header("Location: index.php?page=schedule&status=error"); + exit(); + } + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return null; + } + + $pdo = db(); + $id = $_POST['id'] ?? null; + $id_kelas = $_POST['id_kelas'] ?? ''; + $id_guru = $_POST['id_guru'] ?? ''; + $id_matapelajaran = $_POST['id_matapelajaran'] ?? ''; // New field + $hari = $_POST['hari'] ?? ''; + $jam_mulai = $_POST['jam_mulai'] ?? ''; + $jam_selesai = $_POST['jam_selesai'] ?? ''; + + // Basic validation + if (empty($id_kelas) || empty($id_guru) || empty($id_matapelajaran) || empty($hari) || empty($jam_mulai) || empty($jam_selesai)) { + return ['success' => false, 'message' => 'Semua kolom wajib diisi.']; + } + + try { + if ($action === 'add') { + $stmt = $pdo->prepare( + "INSERT INTO jadwal (id_kelas, id_guru, id_matapelajaran, hari, jam_mulai, jam_selesai) + VALUES (?, ?, ?, ?, ?, ?)" + ); + $stmt->execute([$id_kelas, $id_guru, $id_matapelajaran, $hari, $jam_mulai, $jam_selesai]); + return ['success' => true, 'message' => 'Data jadwal berhasil ditambahkan.']; + } elseif ($action === 'edit' && $id) { + $stmt = $pdo->prepare( + "UPDATE jadwal SET id_kelas = ?, id_guru = ?, id_matapelajaran = ?, hari = ?, jam_mulai = ?, jam_selesai = ? + WHERE id = ?" + ); + $stmt->execute([$id_kelas, $id_guru, $id_matapelajaran, $hari, $jam_mulai, $jam_selesai, $id]); + return ['success' => true, 'message' => 'Data jadwal berhasil diperbarui.']; + } + } catch (PDOException $e) { + return ['success' => false, 'message' => 'Terjadi kesalahan database: ' . $e->getMessage()]; + } + + return null; +} + +// Attendance Functions + +function get_murid_by_kelas($kelas_id) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT id, nama_lengkap FROM murid WHERE kelas_id = ? ORDER BY nama_lengkap ASC"); + $stmt->execute([$kelas_id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_jadwal_by_kelas_and_hari($kelas_id, $hari) { + $pdo = db(); + $stmt = $pdo->prepare(" + SELECT j.id, mp.nama_matapelajaran, j.jam_mulai, j.jam_selesai + FROM jadwal j + LEFT JOIN mata_pelajaran mp ON j.id_matapelajaran = mp.id + WHERE j.id_kelas = ? AND j.hari = ? ORDER BY j.jam_mulai ASC"); + $stmt->execute([$kelas_id, $hari]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_absensi($id_kelas, $tanggal) { + $pdo = db(); + $stmt = $pdo->prepare(" + SELECT murid_id, jadwal_id, status + FROM absensi + WHERE tanggal = ? AND jadwal_id IN (SELECT id FROM jadwal WHERE id_kelas = ?) + "); + $stmt->execute([$tanggal, $id_kelas]); + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Format the result for easier lookup in the template + $absensi_data = []; + foreach ($result as $row) { + $absensi_data[$row['murid_id']][$row['jadwal_id']] = $row['status']; + } + return $absensi_data; +} + +function handle_absensi_action() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['action']) || $_POST['action'] !== 'save_absensi') { + return null; + } + + $id_kelas = $_POST['id_kelas'] ?? ''; + $tanggal = $_POST['tanggal'] ?? ''; + $absensi = $_POST['absensi'] ?? []; + + if (empty($id_kelas) || empty($tanggal) || empty($absensi)) { + return ['success' => false, 'message' => 'Data tidak lengkap. Harap pilih kelas, tanggal, dan isi absensi.']; + } + + $pdo = db(); + try { + $pdo->beginTransaction(); + + // Prepare statement for inserting/updating attendance + $stmt = $pdo->prepare(" + INSERT INTO absensi (jadwal_id, murid_id, tanggal, status) + VALUES (:jadwal_id, :murid_id, :tanggal, :status) + ON DUPLICATE KEY UPDATE status = :status + "); + + foreach ($absensi as $murid_id => $jadwal_absensi) { + foreach ($jadwal_absensi as $jadwal_id => $status) { + if (!empty($status)) { + $stmt->execute([ + ':jadwal_id' => $jadwal_id, + ':murid_id' => $murid_id, + ':tanggal' => $tanggal, + ':status' => $status + ]); + } + } + } + + $pdo->commit(); + return ['success' => true, 'message' => 'Absensi berhasil disimpan.']; + + } catch (PDOException $e) { + $pdo->rollBack(); + return ['success' => false, 'message' => 'Gagal menyimpan absensi: ' . $e->getMessage()]; + } +} + +function get_rekap_absensi($id_kelas, $start_date, $end_date) { + $pdo = db(); + + // Validate input dates + if (empty($id_kelas) || empty($start_date) || empty($end_date)) { + return []; + } + + $sql = " + SELECT + m.id as murid_id, + m.nis, + m.nama_lengkap, + COUNT(a.id) as total_absensi, + SUM(CASE WHEN a.status = 'Hadir' THEN 1 ELSE 0 END) as total_hadir, + SUM(CASE WHEN a.status = 'Sakit' THEN 1 ELSE 0 END) as total_sakit, + SUM(CASE WHEN a.status = 'Izin' THEN 1 ELSE 0 END) as total_izin, + SUM(CASE WHEN a.status = 'Alpa' THEN 1 ELSE 0 END) as total_alpa + FROM murid m + LEFT JOIN absensi a ON m.id = a.murid_id + WHERE m.kelas_id = :id_kelas + AND a.tanggal BETWEEN :start_date AND :end_date + GROUP BY m.id, m.nis, m.nama_lengkap + ORDER BY m.nama_lengkap ASC; + "; + + try { + $stmt = $pdo->prepare($sql); + $stmt->execute([ + ':id_kelas' => $id_kelas, + ':start_date' => $start_date, + ':end_date' => $end_date + ]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + // Log error or handle it as needed + error_log("Rekap Absensi Error: " . $e->getMessage()); + return []; + } +} + +// Subject (Mata Pelajaran) Functions + +function get_all_matapelajaran() { + $pdo = db(); + $stmt = $pdo->query("SELECT * FROM mata_pelajaran ORDER BY nama_matapelajaran ASC"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function get_matapelajaran_by_id($id) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM mata_pelajaran WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); +} + +function handle_matapelajaran_action() { + $action = $_POST['action'] ?? $_GET['action'] ?? ''; + + if ($action === 'delete' && isset($_GET['id'])) { + $id = $_GET['id']; + $pdo = db(); + try { + $stmt = $pdo->prepare("DELETE FROM mata_pelajaran WHERE id = ?"); + $stmt->execute([$id]); + header("Location: index.php?page=subjects&status=deleted"); + exit(); + } catch (PDOException $e) { + header("Location: index.php?page=subjects&status=error&err_msg=" . urlencode($e->getMessage())); + exit(); + } + } + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return null; + } + + $pdo = db(); + $id = $_POST['id'] ?? null; + $nama_matapelajaran = $_POST['nama_matapelajaran'] ?? ''; + + try { + if ($action === 'add') { + $stmt = $pdo->prepare("INSERT INTO mata_pelajaran (nama_matapelajaran) VALUES (?)"); + $stmt->execute([$nama_matapelajaran]); + return ['success' => true, 'message' => 'Data mata pelajaran berhasil ditambahkan.']; + } elseif ($action === 'edit' && $id) { + $stmt = $pdo->prepare("UPDATE mata_pelajaran SET nama_matapelajaran = ? WHERE id = ?"); + $stmt->execute([$nama_matapelajaran, $id]); + return ['success' => true, 'message' => 'Data mata pelajaran berhasil diperbarui.']; + } + } catch (PDOException $e) { + if ($e->getCode() == 23000) { + return ['success' => false, 'message' => 'Gagal menyimpan data. Nama mata pelajaran mungkin sudah ada.']; + } + return ['success' => false, 'message' => 'Terjadi kesalahan database: ' . $e->getMessage()]; + } + + return null; +} + +?> diff --git a/index.php b/index.php index 7205f3d..e015d34 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,34 @@ - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
- - - +$page = $_GET['page'] ?? 'dashboard'; + +switch ($page) { + case 'murid': + include 'templates/murid.php'; + break; + case 'teachers': + include 'templates/teachers.php'; + break; + case 'classes': + include 'templates/classes.php'; + break; + case 'attendance': + include 'templates/attendance.php'; + break; + case 'schedule': + include 'templates/schedule.php'; + break; + case 'reports': + include 'templates/reports.php'; + break; + case 'dashboard': + default: + include 'templates/dashboard.php'; + break; +} + +require_once 'templates/footer.php'; \ No newline at end of file diff --git a/libs/SimpleXLSX.php b/libs/SimpleXLSX.php new file mode 100644 index 0000000..292da12 --- /dev/null +++ b/libs/SimpleXLSX.php @@ -0,0 +1,1231 @@ +rows() as $r) { + * print_r( $r ); + * } + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 2: html table + * if ( $xlsx = SimpleXLSX::parse('book.xlsx') ) { + * echo $xlsx->toHTML(); + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 3: rowsEx + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * foreach ( $xlsx->rowsEx() as $r ) { + * print_r( $r ); + * } + * + * Example 4: select worksheet + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * foreach( $xlsx->rows(1) as $r ) { // second worksheet + * print_t( $r ); + * } + * + * Example 5: IDs and worksheet names + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * print_r( $xlsx->sheetNames() ); // array( 0 => 'Sheet 1', 1 => 'Catalog' ); + * + * Example 6: get sheet name by index + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * echo 'Sheet Name 2 = '.$xlsx->sheetName(1); + * + * Example 7: getCell (very slow) + * echo $xlsx->getCell(1,'D12'); // reads D12 cell from second sheet + * + * Example 8: read data + * if ( $xlsx = SimpleXLSX::parseData( file_get_contents('http://www.example.com/example.xlsx') ) ) { + * $dim = $xlsx->dimension(1); + * $num_cols = $dim[0]; + * $num_rows = $dim[1]; + * echo $xlsx->sheetName(1).':'.$num_cols.'x'.$num_rows; + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 9: old style + * $xlsx = new SimpleXLSX('book.xlsx'); + * if ( $xlsx->success() ) { + * print_r( $xlsx->rows() ); + * } else { + * echo 'xlsx error: '.$xlsx->error(); + * } + */ +class SimpleXLSX +{ + // Don't remove this string! Created by Sergey Shuchkin sergey.shuchkin@gmail.com + public static $CF = [ // Cell formats + 0 => 'General', + 1 => '0', + 2 => '0.00', + 3 => '#,##0', + 4 => '#,##0.00', + 9 => '0%', + 10 => '0.00%', + 11 => '0.00E+00', + 12 => '# ?/?', + 13 => '# ??/??', + 14 => 'mm-dd-yy', + 15 => 'd-mmm-yy', + 16 => 'd-mmm', + 17 => 'mmm-yy', + 18 => 'h:mm AM/PM', + 19 => 'h:mm:ss AM/PM', + 20 => 'h:mm', + 21 => 'h:mm:ss', + 22 => 'm/d/yy h:mm', + + 37 => '#,##0 ;(#,##0)', + 38 => '#,##0 ;[Red](#,##0)', + 39 => '#,##0.00;(#,##0.00)', + 40 => '#,##0.00;[Red](#,##0.00)', + + 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)', + 45 => 'mm:ss', + 46 => '[h]:mm:ss', + 47 => 'mmss.0', + 48 => '##0.0E+0', + 49 => '@', + + 27 => '[$-404]e/m/d', + 30 => 'm/d/yy', + 36 => '[$-404]e/m/d', + 50 => '[$-404]e/m/d', + 57 => '[$-404]e/m/d', + + 59 => 't0', + 60 => 't0.00', + 61 => 't#,##0', + 62 => 't#,##0.00', + 67 => 't0%', + 68 => 't0.00%', + 69 => 't# ?/?', + 70 => 't# ??/??', + ]; + public $nf = []; // number formats + public $cellFormats = []; // cellXfs + public $datetimeFormat = 'Y-m-d H:i:s'; + public $debug; + public $activeSheet = 0; + public $rowsExReader; + + /* @var SimpleXMLElement[] $sheets */ + public $sheets; + public $sheetFiles = []; + public $sheetMetaData = []; + public $sheetRels = []; + // scheme + public $styles; + /* @var array[] $package */ + public $package; + public $sharedstrings; + public $date1904 = 0; + + + /* + private $date_formats = array( + 0xe => "d/m/Y", + 0xf => "d-M-Y", + 0x10 => "d-M", + 0x11 => "M-Y", + 0x12 => "h:i a", + 0x13 => "h:i:s a", + 0x14 => "H:i", + 0x15 => "H:i:s", + 0x16 => "d/m/Y H:i", + 0x2d => "i:s", + 0x2e => "H:i:s", + 0x2f => "i:s.S" + ); + private $number_formats = array( + 0x1 => "%1.0f", // "0" + 0x2 => "%1.2f", // "0.00", + 0x3 => "%1.0f", //"#,##0", + 0x4 => "%1.2f", //"#,##0.00", + 0x5 => "%1.0f", //"$#,##0;($#,##0)", + 0x6 => '$%1.0f', //"$#,##0;($#,##0)", + 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x9 => '%1.0f%%', //"0%" + 0xa => '%1.2f%%', //"0.00%" + 0xb => '%1.2f', //"0.00E00", + 0x25 => '%1.0f', //"#,##0;(#,##0)", + 0x26 => '%1.0f', //"#,##0;(#,##0)", + 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x29 => '%1.0f', //"#,##0;(#,##0)", + 0x2a => '$%1.0f', //"$#,##0;($#,##0)", + 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x30 => '%1.0f'); //"##0.0E0"; + // }}} + */ + public $errno = 0; + public $error = false; + /** + * @var false|SimpleXMLElement + */ + public $theme; + + + public function __construct($filename = null, $is_data = null, $debug = null) + { + if ($debug !== null) { + $this->debug = $debug; + } + $this->package = [ + 'filename' => '', + 'mtime' => 0, + 'size' => 0, + 'comment' => '', + 'entries' => [] + ]; + if ($filename && $this->unzip($filename, $is_data)) { + $this->parseEntries(); + } + } + + public function unzip($filename, $is_data = false) + { + + if ($is_data) { + $this->package['filename'] = 'default.xlsx'; + $this->package['mtime'] = time(); + $this->package['size'] = self::strlen($filename); + + $vZ = $filename; + } else { + if (!is_readable($filename)) { + $this->error(1, 'File not found ' . $filename); + + return false; + } + + // Package information + $this->package['filename'] = $filename; + $this->package['mtime'] = filemtime($filename); + $this->package['size'] = filesize($filename); + + // Read file + $vZ = file_get_contents($filename); + } + // Cut end of central directory + /* $aE = explode("\x50\x4b\x05\x06", $vZ); + + if (count($aE) == 1) { + $this->error('Unknown format'); + return false; + } + */ + // Explode to each part + $aE = explode("\x50\x4b\x03\x04", $vZ); + array_shift($aE); + + $aEL = count($aE); + if ($aEL === 0) { + $this->error(2, 'Unknown archive format'); + + return false; + } + // Search central directory end record + $last = $aE[$aEL - 1]; + $last = explode("\x50\x4b\x05\x06", $last); + if (count($last) !== 2) { + $this->error(2, 'Unknown archive format'); + + return false; + } + // Search central directory + $last = explode("\x50\x4b\x01\x02", $last[0]); + if (count($last) < 2) { + $this->error(2, 'Unknown archive format'); + + return false; + } + $aE[$aEL - 1] = $last[0]; + + // Loop through the entries + foreach ($aE as $vZ) { + $aI = []; + $aI['E'] = 0; + $aI['EM'] = ''; + // Retrieving local file header information +// $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ); + $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ); + + // Check if data is encrypted +// $bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE; +// $bE = false; + $nF = $aP['FNL']; + $mF = $aP['EFL']; + + // Special case : value block after the compressed data + if ($aP['GPF'] & 0x0008) { + $aP1 = unpack('V1CRC/V1CS/V1UCS', self::substr($vZ, -12)); + + $aP['CRC'] = $aP1['CRC']; + $aP['CS'] = $aP1['CS']; + $aP['UCS'] = $aP1['UCS']; + // 2013-08-10 + $vZ = self::substr($vZ, 0, -12); + if (self::substr($vZ, -4) === "\x50\x4b\x07\x08") { + $vZ = self::substr($vZ, 0, -4); + } + } + + // Getting stored filename + $aI['N'] = self::substr($vZ, 26, $nF); + $aI['N'] = str_replace('\\', '/', $aI['N']); + + if (self::substr($aI['N'], -1) === '/') { + // is a directory entry - will be skipped + continue; + } + + // Truncate full filename in path and filename + $aI['P'] = dirname($aI['N']); + $aI['P'] = ($aI['P'] === '.') ? '' : $aI['P']; + $aI['N'] = basename($aI['N']); + + $vZ = self::substr($vZ, 26 + $nF + $mF); + + if ($aP['CS'] > 0 && (self::strlen($vZ) !== (int)$aP['CS'])) { // check only if availabled + $aI['E'] = 1; + $aI['EM'] = 'Compressed size is not equal with the value in header information.'; + } +// } elseif ( $bE ) { +// $aI['E'] = 5; +// $aI['EM'] = 'File is encrypted, which is not supported from this class.'; +/* } else { + switch ($aP['CM']) { + case 0: // Stored + // Here is nothing to do, the file ist flat. + break; + case 8: // Deflated + $vZ = gzinflate($vZ); + break; + case 12: // BZIP2 + if (extension_loaded('bz2')) { + $vZ = bzdecompress($vZ); + } else { + $aI['E'] = 7; + $aI['EM'] = 'PHP BZIP2 extension not available.'; + } + break; + default: + $aI['E'] = 6; + $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported."; + } + if (!$aI['E']) { + if ($vZ === false) { + $aI['E'] = 2; + $aI['EM'] = 'Decompression of data failed.'; + } elseif ($this->_strlen($vZ) !== (int)$aP['UCS']) { + $aI['E'] = 3; + $aI['EM'] = 'Uncompressed size is not equal with the value in header information.'; + } elseif (crc32($vZ) !== $aP['CRC']) { + $aI['E'] = 4; + $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.'; + } + } + } +*/ + + // DOS to UNIX timestamp + $aI['T'] = mktime( + ($aP['FT'] & 0xf800) >> 11, + ($aP['FT'] & 0x07e0) >> 5, + ($aP['FT'] & 0x001f) << 1, + ($aP['FD'] & 0x01e0) >> 5, + $aP['FD'] & 0x001f, + (($aP['FD'] & 0xfe00) >> 9) + 1980 + ); + + $this->package['entries'][] = [ + 'data' => $vZ, + 'ucs' => (int)$aP['UCS'], // ucompresses size + 'cm' => $aP['CM'], // compressed method + 'cs' => isset($aP['CS']) ? (int) $aP['CS'] : 0, // compresses size + 'crc' => $aP['CRC'], + 'error' => $aI['E'], + 'error_msg' => $aI['EM'], + 'name' => $aI['N'], + 'path' => $aI['P'], + 'time' => $aI['T'] + ]; + } // end for each entries + + return true; + } + + + public function error($num = null, $str = null) + { + if ($num) { + $this->errno = $num; + $this->error = $str; + if ($this->debug) { + trigger_error(__CLASS__ . ': ' . $this->error, E_USER_WARNING); + } + } + + return $this->error; + } + + public function parseEntries() + { + // Document data holders + $this->sharedstrings = []; + $this->sheets = []; +// $this->styles = array(); +// $m1 = 0; // memory_get_peak_usage( true ); + // Read relations and search for officeDocument + if ($relations = $this->getEntryXML('_rels/.rels')) { + foreach ($relations->Relationship as $rel) { + $rel_type = basename(trim((string)$rel['Type'])); // officeDocument + $rel_target = self::getTarget('', (string)$rel['Target']); // /xl/workbook.xml or xl/workbook.xml + + if ($rel_type === 'officeDocument' + && $workbook = $this->getEntryXML($rel_target) + ) { + $index_rId = []; // [0 => rId1] + + $index = 0; + foreach ($workbook->sheets->sheet as $s) { + $a = []; + foreach ($s->attributes() as $k => $v) { + $a[(string)$k] = (string)$v; + } + $this->sheetMetaData[$index] = $a; + $index_rId[$index] = (string)$s['id']; + $index++; + } + if ((int)$workbook->workbookPr['date1904'] === 1) { + $this->date1904 = 1; + } + + + if ($workbookRelations = $this->getEntryXML(dirname($rel_target) . '/_rels/workbook.xml.rels')) { + // Loop relations for workbook and extract sheets... + foreach ($workbookRelations->Relationship as $workbookRelation) { + $wrel_type = basename(trim((string)$workbookRelation['Type'])); // worksheet + $wrel_target = self::getTarget(dirname($rel_target), (string)$workbookRelation['Target']); + if (!$this->entryExists($wrel_target)) { + continue; + } + + if ($wrel_type === 'worksheet') { // Sheets + if ($sheet = $this->getEntryXML($wrel_target)) { + $index = array_search((string)$workbookRelation['Id'], $index_rId, true); + $this->sheets[$index] = $sheet; + $this->sheetFiles[$index] = $wrel_target; + $srel_d = dirname($wrel_target); + $srel_f = basename($wrel_target); + $srel_file = $srel_d . '/_rels/' . $srel_f . '.rels'; + if ($this->entryExists($srel_file)) { + $this->sheetRels[$index] = $this->getEntryXML($srel_file); + } + } + } elseif ($wrel_type === 'sharedStrings') { + if ($sharedStrings = $this->getEntryXML($wrel_target)) { + foreach ($sharedStrings->si as $val) { + if (isset($val->t)) { + $this->sharedstrings[] = (string)$val->t; + } elseif (isset($val->r)) { + $this->sharedstrings[] = self::parseRichText($val); + } + } + } + } elseif ($wrel_type === 'styles') { + $this->styles = $this->getEntryXML($wrel_target); + + // number formats + $this->nf = []; + if (isset($this->styles->numFmts->numFmt)) { + foreach ($this->styles->numFmts->numFmt as $v) { + $this->nf[(int)$v['numFmtId']] = (string)$v['formatCode']; + } + } + + $this->cellFormats = []; + if (isset($this->styles->cellXfs->xf)) { + foreach ($this->styles->cellXfs->xf as $v) { + $x = [ + 'format' => null + ]; + foreach ($v->attributes() as $k1 => $v1) { + $x[ $k1 ] = (int) $v1; + } + if (isset($x['numFmtId'])) { + if (isset($this->nf[$x['numFmtId']])) { + $x['format'] = $this->nf[$x['numFmtId']]; + } elseif (isset(self::$CF[$x['numFmtId']])) { + $x['format'] = self::$CF[$x['numFmtId']]; + } + } + + $this->cellFormats[] = $x; + } + } + } elseif ($wrel_type === 'theme') { + $this->theme = $this->getEntryXML($wrel_target); + } + } + +// break; + } + // reptile hack :: find active sheet from workbook.xml + if ($workbook->bookViews->workbookView) { + foreach ($workbook->bookViews->workbookView as $v) { + if (!empty($v['activeTab'])) { + $this->activeSheet = (int)$v['activeTab']; + } + } + } + + break; + } + } + } + +// $m2 = memory_get_peak_usage(true); +// echo __FUNCTION__.' M='.round( ($m2-$m1) / 1048576, 2).'MB'.PHP_EOL; + + if (count($this->sheets)) { + // Sort sheets + ksort($this->sheets); + + return true; + } + + return false; + } + + public function getEntryXML($name) + { + if ($entry_xml = $this->getEntryData($name)) { + $this->deleteEntry($name); // economy memory + // dirty remove namespace prefixes and empty rows + $entry_xml = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $entry_xml); // remove namespaces + $entry_xml .= ' '; // force run garbage collector + // remove namespaced attrs + $entry_xml = preg_replace('/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1', $entry_xml); + $entry_xml .= ' '; + $entry_xml = preg_replace('/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml); // fix namespaced openned tags + $entry_xml .= ' '; + $entry_xml = preg_replace('/<\/[a-zA-Z0-9]+:([^>]+)>/', '', $entry_xml); // fix namespaced closed tags + $entry_xml .= ' '; + + if (strpos($name, '/sheet')) { // dirty skip empty rows + // remove + $cnt = $cnt2 = $cnt3 = null; + $entry_xml = preg_replace('/]+>\s*(\s*)+<\/row>/', '', $entry_xml, -1, $cnt); + $entry_xml .= ' '; + // remove + $entry_xml = preg_replace('/]*\/>/', '', $entry_xml, -1, $cnt2); + $entry_xml .= ' '; + // remove + $entry_xml = preg_replace('/]*><\/row>/', '', $entry_xml, -1, $cnt3); + $entry_xml .= ' '; + if ($cnt || $cnt2 || $cnt3) { + $entry_xml = preg_replace('//', '', $entry_xml); + $entry_xml .= ' '; + } +// file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!! + } + $entry_xml = trim($entry_xml); + +// $m1 = memory_get_usage(); + // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8 + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) { + $_old = libxml_disable_entity_loader(); + } + + $_old_uie = libxml_use_internal_errors(true); + + $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE); + + libxml_use_internal_errors($_old_uie); + + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) { + /** @noinspection PhpUndefinedVariableInspection */ + libxml_disable_entity_loader($_old); + } + +// $m2 = memory_get_usage(); +// echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL; + + if ($entry_xmlobj) { + return $entry_xmlobj; + } + $e = libxml_get_last_error(); + if ($e) { + $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line); + } + } else { + $this->error(4, 'XML-entry not found ' . $name); + } + + return false; + } + + // sheets numeration: 1,2,3.... + + public function getEntryData($name) + { + $name = ltrim(str_replace('\\', '/', $name), '/'); + $dir = self::strtoupper(dirname($name)); + $name = self::strtoupper(basename($name)); + foreach ($this->package['entries'] as &$entry) { + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) { + if ($entry['error']) { + return false; + } + switch ($entry['cm']) { + case -1: + case 0: // Stored + // Here is nothing to do, the file ist flat. + break; + case 8: // Deflated + $entry['data'] = gzinflate($entry['data']); + break; + case 12: // BZIP2 + if (extension_loaded('bz2')) { + $entry['data'] = bzdecompress($entry['data']); + } else { + $entry['error'] = 7; + $entry['error_message'] = 'PHP BZIP2 extension not available.'; + } + break; + default: + $entry['error'] = 6; + $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.'; + } + if (!$entry['error'] && $entry['cm'] > -1) { + $entry['cm'] = -1; + if ($entry['data'] === false) { + $entry['error'] = 2; + $entry['error_msg'] = 'Decompression of data failed.'; + } elseif ($entry['ucs'] > 0 && (self::strlen($entry['data']) !== (int)$entry['ucs'])) { + $entry['error'] = 3; + $entry['error_msg'] = 'Uncompressed size is not equal with the value in header information.'; + } elseif (crc32($entry['data']) !== $entry['crc']) { + $entry['error'] = 4; + $entry['error_msg'] = 'CRC32 checksum is not equal with the value in header information.'; + } + } + + return $entry['data']; + } + } + unset($entry); + $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name); + + return false; + } + public function deleteEntry($name) + { + $name = ltrim(str_replace('\\', '/', $name), '/'); + $dir = self::strtoupper(dirname($name)); + $name = self::strtoupper(basename($name)); + foreach ($this->package['entries'] as $k => $entry) { + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) { + unset($this->package['entries'][$k]); + return true; + } + } + return false; + } + + public static function strtoupper($str) + { + return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str); + } + + /* + * @param string $name Filename in archive + * @return SimpleXMLElement|bool + */ + + public function entryExists($name) + { + // 0.6.6 + $dir = self::strtoupper(dirname($name)); + $name = self::strtoupper(basename($name)); + foreach ($this->package['entries'] as $entry) { + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) { + return true; + } + } + + return false; + } + + public static function parseFile($filename, $debug = false) + { + return self::parse($filename, false, $debug); + } + + public static function parse($filename, $is_data = false, $debug = false) + { + $xlsx = new self(); + $xlsx->debug = $debug; + if ($xlsx->unzip($filename, $is_data)) { + $xlsx->parseEntries(); + } + if ($xlsx->success()) { + return $xlsx; + } + self::parseError($xlsx->error()); + self::parseErrno($xlsx->errno()); + + return false; + } + + public function success() + { + return !$this->error; + } + + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex + + public static function parseError($set = false) + { + static $error = false; + + return $set ? $error = $set : $error; + } + + public static function parseErrno($set = false) + { + static $errno = false; + + return $set ? $errno = $set : $errno; + } + + public function errno() + { + return $this->errno; + } + + public static function parseData($data, $debug = false) + { + return self::parse($data, true, $debug); + } + + + + public function worksheet($worksheetIndex = 0) + { + if (isset($this->sheets[$worksheetIndex])) { + return $this->sheets[$worksheetIndex]; + } + $this->error(6, 'Worksheet not found ' . $worksheetIndex); + + return false; + } + + /** + * returns [numCols,numRows] of worksheet + * + * @param int $worksheetIndex + * + * @return array + */ + public function dimension($worksheetIndex = 0) + { + + if (($ws = $this->worksheet($worksheetIndex)) === false) { + return [0, 0]; + } + /* @var SimpleXMLElement $ws */ + + $ref = (string)$ws->dimension['ref']; + + if (self::strpos($ref, ':') !== false) { + $d = explode(':', $ref); + $idx = $this->getIndex($d[1]); + + return [$idx[0] + 1, $idx[1] + 1]; + } + /* + if ( $ref !== '' ) { // 0.6.8 + $index = $this->getIndex( $ref ); + + return [ $index[0] + 1, $index[1] + 1 ]; + } + */ + + // slow method + $maxC = $maxR = 0; + $iR = -1; + foreach ($ws->sheetData->row as $row) { + $iR++; + $iC = -1; + foreach ($row->c as $c) { + $iC++; + $idx = $this->getIndex((string)$c['r']); + $x = $idx[0]; + $y = $idx[1]; + if ($x > -1) { + if ($x > $maxC) { + $maxC = $x; + } + if ($y > $maxR) { + $maxR = $y; + } + } else { + if ($iC > $maxC) { + $maxC = $iC; + } + if ($iR > $maxR) { + $maxR = $iR; + } + } + } + } + + return [$maxC + 1, $maxR + 1]; + } + + public function getIndex($cell = 'A1') + { + $m = null; + + if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) { + $col = $m[1]; + $row = $m[2]; + + $colLen = self::strlen($col); + $index = 0; + + for ($i = $colLen - 1; $i >= 0; $i--) { + $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1); + } + + return [$index - 1, $row - 1]; + } + +// $this->error( 'Invalid cell index ' . $cell ); + + return [-1, -1]; + } + + public function value($cell) + { + // Determine data type + $dataType = (string)$cell['t']; + + if ($dataType === '' || $dataType === 'n') { // number + $s = (int)$cell['s']; + if ($s > 0 && isset($this->cellFormats[$s])) { + if (array_key_exists('format', $this->cellFormats[$s])) { + $format = $this->cellFormats[$s]['format']; + if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM + $dataType = 'D'; + } + } else { + $dataType = 'n'; + } + } + } + + $value = ''; + + switch ($dataType) { + case 's': + // Value is a shared string + if ((string)$cell->v !== '') { + $value = $this->sharedstrings[(int)$cell->v]; + } + break; + + case 'str': // formula? + if ((string)$cell->v !== '') { + $value = (string)$cell->v; + } + break; + + case 'b': + // Value is boolean + $value = self::boolean((string)$cell->v); + + break; + + case 'inlineStr': + // Value is rich text inline + $value = self::parseRichText($cell->is); + + break; + + case 'e': + // Value is an error message + if ((string)$cell->v !== '') { + $value = (string)$cell->v; + } + break; + + case 'D': + // Date as float + if (!empty($cell->v)) { + $value = $this->datetimeFormat ? + gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v; + } + break; + + case 'd': + // Date as ISO YYYY-MM-DD + if ((string)$cell->v !== '') { + $value = (string)$cell->v; + } + break; + + default: + // Value is a string + $value = (string)$cell->v; + + // Check for numeric values + if (is_numeric($value)) { + /** @noinspection TypeUnsafeComparisonInspection */ + if ($value == (int)$value) { + $value = (int)$value; + } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) { + $value = (float)$value; + } + } + } + + return $value; + } + + public function unixstamp($excelDateTime) + { + + $d = floor($excelDateTime); // days since 1900 or 1904 + $t = $excelDateTime - $d; + + if ($this->date1904) { + $d += 1462; + } + + $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400); + + return (int)$t; + } + + public function toHTML($worksheetIndex = 0) + { + $s = ''; + foreach ($this->readRows($worksheetIndex) as $r) { + $s .= ''; + foreach ($r as $c) { + $s .= ''; + } + $s .= "\r\n"; + } + $s .= '
' . ($c === '' ? ' ' : htmlspecialchars($c, ENT_QUOTES)) . '
'; + + return $s; + } + public function toHTMLEx($worksheetIndex = 0) + { + $s = ''; + $y = 0; + foreach ($this->readRowsEx($worksheetIndex) as $r) { + $s .= ''; + $x = 0; + foreach ($r as $c) { + $tag = 'td'; + $css = $c['css']; + if ($y === 0) { + $tag = 'th'; + $css .= $c['width'] ? 'width: '.round($c['width'] * 0.47, 2).'em;' : ''; + } + + if ($x === 0 && $c['height']) { + $css .= 'height: '.round($c['height'] * 1.3333).'px;'; + } + $s .= '<'.$tag.' style="'.$css.'" nowrap>' + . ($c['value'] === '' ? ' ' : htmlspecialchars($c['value'], ENT_QUOTES)) . ''; + $x++; + } + $s .= "\r\n"; + $y++; + } + $s .= '
'; + + return $s; + } + public function rows($worksheetIndex = 0, $limit = 0) + { + return iterator_to_array($this->readRows($worksheetIndex, $limit), false); + } + // thx Gonzo + /** + * @param $worksheetIndex + * @param $limit + * @return \Generator + */ + public function readRows($worksheetIndex = 0, $limit = 0) + { + + if (($ws = $this->worksheet($worksheetIndex)) === false) { + return; + } + $dim = $this->dimension($worksheetIndex); + $numCols = $dim[0]; + $numRows = $dim[1]; + + $emptyRow = []; + for ($i = 0; $i < $numCols; $i++) { + $emptyRow[] = ''; + } + + $curR = 0; + $_limit = $limit; + /* @var SimpleXMLElement $ws */ + foreach ($ws->sheetData->row as $row) { + $r = $emptyRow; + $curC = 0; + foreach ($row->c as $c) { + // detect skipped cols + $idx = $this->getIndex((string)$c['r']); + $x = $idx[0]; + $y = $idx[1]; + + if ($x > -1) { + $curC = $x; + while ($curR < $y) { + yield $emptyRow; + $curR++; + $_limit--; + if ($_limit === 0) { + return; + } + } + } + $r[$curC] = $this->value($c); + $curC++; + } + yield $r; + + $curR++; + $_limit--; + if ($_limit === 0) { + return; + } + } + while ($curR < $numRows) { + yield $emptyRow; + $curR++; + $_limit--; + if ($_limit === 0) { + return; + } + } + } + + public function rowsEx($worksheetIndex = 0, $limit = 0) + { + return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false); + } + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex + /** + * @param $worksheetIndex + * @param $limit + * @return \Generator|null + */ + public function readRowsEx($worksheetIndex = 0, $limit = 0) + { + if (!$this->rowsExReader) { + require_once __DIR__ . '/SimpleXLSXEx.php'; + $this->rowsExReader = new SimpleXLSXEx($this); + } + return $this->rowsExReader->readRowsEx($worksheetIndex, $limit); + } + + /** + * Returns cell value + * VERY SLOW! Use ->rows() or ->rowsEx() + * + * @param int $worksheetIndex + * @param string|array $cell ref or coords, D12 or [3,12] + * + * @return mixed Returns NULL if not found + */ + public function getCell($worksheetIndex = 0, $cell = 'A1') + { + + if (($ws = $this->worksheet($worksheetIndex)) === false) { + return false; + } + if (is_array($cell)) { + $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21 + } + if (is_string($cell)) { + $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']"); + if (count($result)) { + return $this->value($result[0]); + } + } + + return null; + } + + public function getSheets() + { + return $this->sheets; + } + + public function sheetsCount() + { + return count($this->sheets); + } + + public function sheetName($worksheetIndex) + { + $sn = $this->sheetNames(); + if (isset($sn[$worksheetIndex])) { + return $sn[$worksheetIndex]; + } + + return false; + } + + public function sheetNames() + { + $a = []; + foreach ($this->sheetMetaData as $k => $v) { + $a[$k] = $v['name']; + } + return $a; + } + public function sheetMeta($worksheetIndex = null) + { + if ($worksheetIndex === null) { + return $this->sheetMetaData; + } + return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false; + } + public function isHiddenSheet($worksheetIndex) + { + return isset($this->sheetMetaData[$worksheetIndex]['state']) + && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden'; + } + + public function getStyles() + { + return $this->styles; + } + + public function getPackage() + { + return $this->package; + } + + public function setDateTimeFormat($value) + { + $this->datetimeFormat = is_string($value) ? $value : false; + } + + public static function getTarget($base, $target) + { + $target = trim($target); + if (strpos($target, '/') === 0) { + return self::substr($target, 1); + } + $target = ($base ? $base . '/' : '') . $target; + // a/b/../c -> a/c + $parts = explode('/', $target); + $abs = []; + foreach ($parts as $p) { + if ('.' === $p) { + continue; + } + if ('..' === $p) { + array_pop($abs); + } else { + $abs[] = $p; + } + } + return implode('/', $abs); + } + + public static function parseRichText($is = null) + { + $value = []; + + if (isset($is->t)) { + $value[] = (string)$is->t; + } elseif (isset($is->r)) { + foreach ($is->r as $run) { + $value[] = (string)$run->t; + } + } + + return implode('', $value); + } + + public static function num2name($num) + { + $numeric = ($num - 1) % 26; + $letter = chr(65 + $numeric); + $num2 = (int)(($num - 1) / 26); + if ($num2 > 0) { + return self::num2name($num2) . $letter; + } + return $letter; + } + + public static function strlen($str) + { + return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str); + } + + public static function substr($str, $start, $length = null) + { + return (ini_get('mbstring.func_overload') & 2) ? + mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit') + : substr($str, $start, ($length === null) ? strlen($str) : $length); + } + + public static function strpos($haystack, $needle, $offset = 0) + { + return (ini_get('mbstring.func_overload') & 2) ? + mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset); + } + public static function boolean($value) + { + if (is_numeric($value)) { + return (bool) $value; + } + + return $value === 'true' || $value === 'TRUE'; + } +} diff --git a/libs/SimpleXLSXGen.php b/libs/SimpleXLSXGen.php new file mode 100644 index 0000000..be5a4cb --- /dev/null +++ b/libs/SimpleXLSXGen.php @@ -0,0 +1,1340 @@ +subject = ''; + $this->title = ''; + $this->author = ''; + $this->company = ''; + $this->manager = ''; + $this->description = ''; + $this->keywords = ''; + $this->category = ''; + $this->language = 'en-US'; + $this->lastModifiedBy = ''; + $this->application = __CLASS__; + + $this->curSheet = -1; + $this->defaultFont = 'Calibri'; + $this->defaultFontSize = 10; + $this->rtl = false; + $this->sheets = []; + $this->SI = []; // sharedStrings index + $this->SI_KEYS = []; // & keys + + // https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_numFmts_topic_ID0E6KK6.html + $this->NF = [ + self::N_RUB => '#,##0.00\ "₽"', + self::N_DOLLAR => '[$$-1]#,##0.00', + self::N_EURO => '#,##0.00\ [$€-1]' + ]; + $this->NF_KEYS = array_flip($this->NF); + + $this->BR_STYLE = [ + self::B_NONE => 'none', + self::B_THIN => 'thin', + self::B_MEDIUM => 'medium', + self::B_DASHED => 'dashed', + self::B_DOTTED => 'dotted', + self::B_THICK => 'thick', + self::B_DOUBLE => 'double', + self::B_HAIR => 'hair', + self::B_MEDIUM_DASHED => 'mediumDashed', + self::B_DASH_DOT => 'dashDot', + self::B_MEDIUM_DASH_DOT => 'mediumDashDot', + self::B_DASH_DOT_DOT => 'dashDotDot', + self::B_MEDIUM_DASH_DOT_DOT => 'mediumDashDotDot', + self::B_SLANT_DASH_DOT => 'slantDashDot' + ]; + + $this->XF = [ // styles 0 - num fmt, 1 - align, 2 - font, 3 - fill, 4 - font color, 5 - bgcolor, 6 - border, 7 - font size + [self::N_NORMAL, self::A_DEFAULT, self::F_NORMAL, self::FL_NONE, 0, 0, '', 0], + [self::N_NORMAL, self::A_DEFAULT, self::F_NORMAL, self::FL_GRAY_125, 0, 0, '', 0], // hack + ]; + $this->XF_KEYS[implode('-', $this->XF[0])] = 0; // & keys + $this->XF_KEYS[implode('-', $this->XF[1])] = 1; + $this->template = [ + '_rels/.rels' => ''."\r\n" + .''."\r\n" + .''."\r\n" + .''."\r\n" + .''."\r\n" + .'', + 'docProps/app.xml' => ''."\r\n" + .''."\r\n" + .'0'."\r\n" + .'{APP}'."\r\n" + .'{COMPANY}'."\r\n" + .'{MANAGER}'."\r\n" + .'', + 'docProps/core.xml' => ''."\r\n" + .''."\r\n" + .'{DATE}'."\r\n" + .'{TITLE}'."\r\n" + .'{SUBJECT}'."\r\n" + .'{AUTHOR}'."\r\n" + .'{LAST_MODIFY_BY}'."\r\n" + .'{KEYWORD}'."\r\n" + .'{DESCRIPTION}'."\r\n" + .'{CATEGORY}'."\r\n" + .'{LANGUAGE}'."\r\n" + .'{DATE}'."\r\n" + .'1'."\r\n" + .'', + 'xl/_rels/workbook.xml.rels' => ''."\r\n" + .'' + ."\r\n{RELS}\r\n", + 'xl/worksheets/sheet1.xml' => ''."\r\n" + . '' . "\r\n" + .''."\r\n" + ."{SHEETVIEWS}\r\n{COLS}\r\n{ROWS}\r\n{AUTOFILTER}{MERGECELLS}{HYPERLINKS}{VML}", + 'xl/worksheets/_rels/sheet1.xml.rels' => ''."\r\n" + .'{RELS}', + 'xl/sharedStrings.xml' => ''."\r\n" + .'{STRINGS}', + 'xl/styles.xml' => ''."\r\n" + .'' + ."\r\n{NUMFMTS}\r\n{FONTS}\r\n{FILLS}\r\n{BORDERS}\r\n" + .'' + ."\r\n{XF}\r\n" + .'', + 'xl/workbook.xml' => ''."\r\n" + .''."\r\n" + .'{SHEETS}', + 'xl/comments1.xml' => ''."\r\n" + . '' . "\r\n" + . '{AUTHORS}' . "\r\n" + . '{COMMENTS}' . "\r\n" + . '', + 'xl/drawings/vmlDrawing1.vml' => '' . "\r\n" + . '' . "\r\n" + .'' . "\r\n" + .'{SHAPES}', + '[Content_Types].xml' => ''."\r\n" + .''."\r\n" + .'' . "\r\n" + .'' . "\r\n" + .'' . "\r\n" + .''."\r\n" + .''."\r\n" + .''."\r\n" + .' +' + ."\r\n{TYPES}", + ]; + } + public static function create($title = null) + { + $xlsx = new static(); + if ($title) { + $xlsx->setTitle($title); + } + return $xlsx; + } + + public static function fromArray(array $rows, $sheetName = null) + { + $xlsx = new static(); + $xlsx->addSheet($rows, $sheetName); + if ($sheetName) { + $xlsx->setTitle($sheetName); + } + return $xlsx; + } + + public function addSheet(array $rows, $name = null) + { + $this->curSheet++; + if ($name === null) { // autogenerated sheet names + $name = ($this->title ? mb_substr($this->title, 0, 31) : 'Sheet') . ($this->curSheet + 1); + } else { + $name = mb_substr($name, 0, 31); + $names = []; + foreach ($this->sheets as $sh) { + $names[mb_strtoupper($sh['name'])] = 1; + } + for ($i = 0; $i < 100; $i++) { + $postfix = ' (' . $i . ')'; + $new_name = ($i === 0) ? $name : $name . $postfix; + if (mb_strlen($new_name) > 31) { + $new_name = mb_substr($name, 0, 31 - mb_strlen($postfix)) . $postfix; + } + $NEW_NAME = mb_strtoupper($new_name); + if (!isset($names[$NEW_NAME])) { + $name = $new_name; + break; + } + } + } + $this->sheets[$this->curSheet] = ['name' => $name, 'relidx' => 0, 'hyperlinks' => [], 'comments' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '', 'frozen' => '']; + if (isset($rows[0]) && is_array($rows[0])) { + $this->sheets[$this->curSheet]['rows'] = $rows; + } else { + $this->sheets[$this->curSheet]['rows'] = []; + } + return $this; + } + + public function __toString() + { + $fh = fopen('php://memory', 'wb'); + if (!$fh) { + return ''; + } + if (!$this->_write($fh)) { + fclose($fh); + return ''; + } + $size = ftell($fh); + fseek($fh, 0); + return (string)fread($fh, $size); + } + + public function save() + { + return $this->saveAs(($this->title ?: gmdate('YmdHi')) . '.xlsx'); + } + public function saveAs($filename) + { + $fh = fopen(str_replace(["\0","\r","\n","\t",'"'], '', $filename), 'wb'); + if (!$fh) { + return false; + } + if (!$this->_write($fh)) { + fclose($fh); + return false; + } + fclose($fh); + return true; + } + + public function download() + { + return $this->downloadAs(($this->title ?: gmdate('YmdHi')) . '.xlsx'); + } + + public function downloadAs($filename) + { + $fh = fopen('php://memory', 'wb'); + if (!$fh) { + return false; + } + if (!$this->_write($fh)) { + fclose($fh); + return false; + } + $size = ftell($fh); + header('Content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment; filename="' . str_replace(["\0","\r","\n","\t",'"'], '', $filename) . '"'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', time())); + header('Content-Length: ' . $size); + while (ob_get_level()) { + ob_end_clean(); + } + fseek($fh, 0); + //Some servers disable fpassthru function. The alternative, stream_get_contents, use more memory + if (function_exists('fpassthru')) { + fpassthru($fh); + } else { + echo stream_get_contents($fh); + } + fclose($fh); + return true; + } + + protected function _write($fh) + { + $dirSignatureE = "\x50\x4b\x05\x06"; // end of central dir signature + $zipComments = 'Generated by ' . __CLASS__ . ' PHP class, thanks sergey.shuchkin@gmail.com'; + if (!$fh) { + return false; + } + $cdrec = ''; // central directory content + $entries = 0; // number of zipped files + $cnt_sheets = count($this->sheets); + if ($cnt_sheets === 0) { + $this->addSheet([], 'No data'); + $cnt_sheets = 1; + } + foreach ($this->template as $cfilename => $template) { + if ($cfilename === 'xl/_rels/workbook.xml.rels') { + $s = ''; + for ($i = 0; $i < $cnt_sheets; $i++) { + $s .= '\r\n"; + } + $s .= '' . "\r\n"; + $s .= ''; + + $template = str_replace('{RELS}', $s, $template); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } elseif ($cfilename === 'xl/workbook.xml') { + $s = ''; + foreach ($this->sheets as $k => $v) { + $s .= ''; + } + $search = ['{SHEETS}', '{APP}']; + $replace = [$s, self::esc($this->application)]; + $template = str_replace($search, $replace, $template); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } elseif ($cfilename === 'docProps/app.xml') { + $search = ['{APP}', '{COMPANY}', '{MANAGER}']; + $replace = [self::esc($this->application), self::esc($this->company), self::esc($this->manager)]; + $template = str_replace($search, $replace, $template); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } elseif ($cfilename === 'docProps/core.xml') { + $search = ['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LANGUAGE}', '{LAST_MODIFY_BY}']; + $replace = [gmdate('Y-m-d\TH:i:s\Z'), self::esc($this->author), self::esc($this->title), self::esc($this->subject), self::esc($this->keywords), self::esc($this->description), self::esc($this->category), self::esc($this->language), self::esc($this->lastModifiedBy)]; + $template = str_replace($search, $replace, $template); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } elseif ($cfilename === 'xl/sharedStrings.xml') { + $si_cnt = count($this->SI); + if ($si_cnt) { + $si = []; + foreach ($this->SI as $s) { + $si[] = '' . (preg_match('/^\s|\s$/', $s) ? '' . $s . '' : '' . $s . '') . ''; + } + $template = str_replace(['{CNT}', '{STRINGS}'], [$si_cnt, implode("\r\n", $si)], $template); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } + } elseif ($cfilename === 'xl/worksheets/sheet1.xml') { + foreach ($this->sheets as $k => $v) { + $filename = 'xl/worksheets/sheet' . ($k + 1) . '.xml'; + $xml = $this->_sheetToXML($k, $template); + $this->_writeEntry($fh, $cdrec, $filename, $xml); + $entries++; + } + $xml = null; + } elseif ($cfilename === 'xl/worksheets/_rels/sheet1.xml.rels') { + foreach ($this->sheets as $k => $v) { + $R = []; + if (count($v['comments'])) { + $this->sheets[$k]['relidx']++; + $R[] = ''; + $R[] = ''; + } + foreach ($v['hyperlinks'] as $h) { + if ($h['ID']) { + $R[] = ''; + } + } + if (count($R)) { + $filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels'; + $xml = str_replace('{RELS}', implode("\r\n", $R), $template); + $this->_writeEntry($fh, $cdrec, $filename, $xml); + $entries++; + } + } + $xml = null; + } elseif ($cfilename === 'xl/comments1.xml') { + foreach ($this->sheets as $k => $v) { + $C = []; + $A = []; + foreach ($v['comments'] as $com) { + $A[$com['A']] = count($A); + } + foreach ($v['comments'] as $com) { + $C[] = '' . "\r\n" + . ($com['A'] ? ('' . $com['A'] . ":\r\n" .'' . "\r\n") : '') + .'' . self::esc($com['T']) . '' . "\r\n" +// .'' . self::esc($com['T']) . ($com['A'] ? "\r\n\t-" . $com['A'] : '') . '' . "\r\n" + . ''; + } + if (count($C)) { + $filename = 'xl/comments' . ($k + 1) . '.xml'; + $xml = str_replace( + array('{AUTHORS}', '{COMMENTS}'), + array('' . implode("\r\n", array_keys($A)) . '', + implode("\r\n", $C)), $template + ); + $this->_writeEntry($fh, $cdrec, $filename, $xml); + $entries++; + } + } + $xml = null; + } elseif ($cfilename === 'xl/drawings/vmlDrawing1.vml') { + foreach ($this->sheets as $k => $v) { + if (count($v['comments'])) { + $S = []; + foreach ($v['comments'] as $k2 => $com) { +// Anchor: LeftColumn, LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, BottomOffset + $a = [ + $com['C'], + 15, + $com['R'], + 10, + $com['C'] + 4, + 15, + $com['R'] + 4, + 4 + ]; + $S[] = ''; + } + $filename = 'xl/drawings/vmlDrawing' . ($k + 1) . '.vml'; + $xml = str_replace('{SHAPES}', "\r\n" . implode("\r\n", $S) . "\r\n", $template); + $this->_writeEntry($fh, $cdrec, $filename, $xml); + $entries++; + } + } + $xml = null; + } elseif ($cfilename === '[Content_Types].xml') { + $TYPES = []; + foreach ($this->sheets as $k => $v) { + $TYPES[] = ''; + if (count($v['comments'])) { + $TYPES[] = ''; + } + } + $template = str_replace('{TYPES}', implode("\r\n", $TYPES), $template); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } elseif ($cfilename === 'xl/styles.xml') { + $NF = $XF = $FONTS = $F_KEYS = $FILLS = $FL_KEYS = []; + $BR = ['']; + $BR_KEYS = [0 => 0]; + foreach ($this->NF as $k => $v) { + $NF[] = ''; + } + foreach ($this->XF as $xf) { + // 0 - num fmt, 1 - align, 2 - font, 3 - fill, 4 - font color, 5 - bgcolor, 6 - border, 7 - font size + // fonts + $F_KEY = $xf[2] . '-' . $xf[4] . '-' . $xf[7]; + if (isset($F_KEYS[$F_KEY])) { + $F_ID = $F_KEYS[$F_KEY]; + } else { + $F_ID = $F_KEYS[$F_KEY] = count($FONTS); + $FONTS[] = '' + . ($xf[7] ? '' : '') + . ($xf[2] & self::F_BOLD ? '' : '') + . ($xf[2] & self::F_ITALIC ? '' : '') + . ($xf[2] & self::F_UNDERLINE ? '' : '') + . ($xf[2] & self::F_STRIKE ? '' : '') + . ($xf[2] & self::F_HYPERLINK ? '' : '') + . ($xf[2] & self::F_COLOR ? '' : '') + . ''; + } + // fills + $FL_KEY = $xf[3] . '-' . $xf[5]; + if (isset($FL_KEYS[$FL_KEY])) { + $FL_ID = $FL_KEYS[$FL_KEY]; + } else { + $FL_ID = $FL_KEYS[$FL_KEY] = count($FILLS); + $FILLS[] = '' : ' />') + . ''; + } + $align = ''; + if ($xf[1] & self::A_LEFT) { + $align .= ' horizontal="left"'; + } elseif ($xf[1] & self::A_RIGHT) { + $align .= ' horizontal="right"'; + } elseif ($xf[1] & self::A_CENTER) { + $align .= ' horizontal="center"'; + } + if ($xf[1] & self::A_TOP) { + $align .= ' vertical="top"'; + } elseif ($xf[1] & self::A_MIDDLE) { + $align .= ' vertical="center"'; + } elseif ($xf[1] & self::A_BOTTOM) { + $align .= ' vertical="bottom"'; + } + if ($xf[1] & self::A_WRAPTEXT) { + $align .= ' wrapText="1"'; + } + if ($xf[1] & self::A_ROTATION_90) { + $align .= ' textRotation="90"'; + } + + // border + $BR_ID = 0; + if ($xf[6] !== '') { + $b = $xf[6]; + if (isset($BR_KEYS[$b])) { + $BR_ID = $BR_KEYS[$b]; + } else { + $BR_ID = count($BR_KEYS); + $BR_KEYS[$b] = $BR_ID; + $border = ''; + $ba = explode(' ', $b); + if (!isset($ba[1])) { + $ba[] = $ba[0]; + $ba[] = $ba[0]; + $ba[] = $ba[0]; + } + if (!isset($ba[4])) { // diagonal + $ba[] = 'none'; + } + $sides = ['left' => 3, 'right' => 1, 'top' => 0, 'bottom' => 2, 'diagonal' => 4]; + foreach ($sides as $side => $idx) { + $s = 'thin'; + $c = ''; + $va = explode('#', $ba[$idx]); + if (isset($va[1])) { + $s = $va[0] === '' ? 'thin' : $va[0]; + $c = $va[1]; + } elseif (in_array($va[0], $this->BR_STYLE, true)) { + $s = $va[0]; + } else { + $c = $va[0]; + } + if (strlen($c) === 6) { + $c = 'FF' . $c; + } + if ($s && $s !== 'none') { + $border .= '<' . $side . ' style="' . $s . '">' + . '' + . ''; + } else { + $border .= '<' . $side . '/>'; + } + } + $border .= ''; + $BR[] = $border; + } + } + $XF[] = ' 0 ? ' applyNumberFormat="1"' : '') + . ($F_ID > 0 ? ' applyFont="1"' : '') + . ($FL_ID > 0 ? ' applyFill="1"' : '') + . ($BR_ID > 0 ? ' applyBorder="1"' : '') + . ($align ? ' applyAlignment="1">' : '/>'); + } + // wrap collections + array_unshift($NF, ''); + $NF[] = ''; + array_unshift($XF, ''); + $XF[] = ''; + array_unshift($FONTS, ''); + $FONTS[] = ''; + array_unshift($FILLS, ''); + $FILLS[] = ''; + array_unshift($BR, ''); + $BR[] = ''; + + $template = str_replace( + ['{NUMFMTS}', '{FONTS}', '{XF}', '{FILLS}', '{BORDERS}'], + [implode("\r\n", $NF), implode("\r\n", $FONTS), implode("\r\n", $XF), implode("\r\n", $FILLS), implode("\r\n", $BR)], + $template + ); + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } else { + $this->_writeEntry($fh, $cdrec, $cfilename, $template); + $entries++; + } + } + $before_cd = ftell($fh); + fwrite($fh, $cdrec); + // end of central dir + fwrite($fh, $dirSignatureE); + fwrite($fh, pack('v', 0)); // number of this disk + fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory + fwrite($fh, pack('v', $entries)); // total # of entries "on this disk" + fwrite($fh, pack('v', $entries)); // total # of entries overall + fwrite($fh, pack('V', mb_strlen($cdrec, '8bit'))); // size of central dir + fwrite($fh, pack('V', $before_cd)); // offset to start of central dir + fwrite($fh, pack('v', mb_strlen($zipComments, '8bit'))); // .zip file comment length + fwrite($fh, $zipComments); + + return true; + } + + protected function _writeEntry($fh, &$cdrec, $cfilename, $data) + { + $zipSignature = "\x50\x4b\x03\x04"; // local file header signature + $dirSignature = "\x50\x4b\x01\x02"; // central dir header signature + + $e = []; + $e['uncsize'] = mb_strlen($data, '8bit'); + // if data to compress is too small, just store it + if ($e['uncsize'] < 256) { + $e['comsize'] = $e['uncsize']; + $e['vneeded'] = 10; + $e['cmethod'] = 0; + $zdata = $data; + } else { // otherwise, compress it + $zdata = gzcompress($data); + $zdata = substr(substr($zdata, 0, -4), 2); // fix crc bug (thanks to Eric Mueller) + $e['comsize'] = mb_strlen($zdata, '8bit'); + $e['vneeded'] = 10; + $e['cmethod'] = 8; + } + $e['bitflag'] = 0; + $e['crc_32'] = crc32($data); + + // Convert date and time to DOS Format, and set then + $date = getdate(); + $e['dostime'] = ( + (($date['year'] - 1980) << 25) + | ($date['mon'] << 21) + | ($date['mday'] << 16) + | ($date['hours'] << 11) + | ($date['minutes'] << 5) + | ($date['seconds'] >> 1) + ); + + $e['offset'] = ftell($fh); + + fwrite($fh, $zipSignature); + fwrite($fh, pack('v', $e['vneeded'])); // version_needed + fwrite($fh, pack('v', $e['bitflag'])); // general_bit_flag + fwrite($fh, pack('v', $e['cmethod'])); // compression_method + fwrite($fh, pack('V', $e['dostime'])); // lastmod datetime + fwrite($fh, pack('V', $e['crc_32'])); // crc-32 + fwrite($fh, pack('V', $e['comsize'])); // compressed_size + fwrite($fh, pack('V', $e['uncsize'])); // uncompressed_size + fwrite($fh, pack('v', mb_strlen($cfilename, '8bit'))); // file_name_length + fwrite($fh, pack('v', 0)); // extra_field_length + fwrite($fh, $cfilename); // file_name + // ignoring extra_field + fwrite($fh, $zdata); + + // Append it to central dir + $e['external_attributes'] = (substr($cfilename, -1) === '/' && !$zdata) ? 16 : 32; // Directory or file name + $e['comments'] = ''; + + $cdrec .= $dirSignature; + $cdrec .= "\x0\x0"; // version made by + $cdrec .= pack('v', $e['vneeded']); // version needed to extract + $cdrec .= "\x0\x0"; // general bit flag + $cdrec .= pack('v', $e['cmethod']); // compression method + $cdrec .= pack('V', $e['dostime']); // lastmod datetime + $cdrec .= pack('V', $e['crc_32']); // crc32 + $cdrec .= pack('V', $e['comsize']); // compressed filesize + $cdrec .= pack('V', $e['uncsize']); // uncompressed filesize + $cdrec .= pack('v', mb_strlen($cfilename, '8bit')); // file name length + $cdrec .= pack('v', 0); // extra field length + $cdrec .= pack('v', mb_strlen($e['comments'], '8bit')); // file comment length + $cdrec .= pack('v', 0); // disk number start + $cdrec .= pack('v', 0); // internal file attributes + $cdrec .= pack('V', $e['external_attributes']); // internal file attributes + $cdrec .= pack('V', $e['offset']); // relative offset of local header + $cdrec .= $cfilename; + $cdrec .= $e['comments']; + } + + protected function _sheetToXML($idx, $template) + { + // locale floats fr_FR 1.234,56 -> 1234.56 + $_loc = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, 'C'); + $COLS = []; + $ROWS = []; + $VML = []; + // $SHEETVIEWS = 'rtl ? ' rightToLeft="1"' : '').'>'; + $SHEETVIEWS = ''; + $PANE = ''; + if (count($this->sheets[$idx]['rows'])) { + $ROWS[] = ''; + if ($this->sheets[$idx]['frozen'] !== '' || isset($this->sheets[$idx]['frozen'][0]) || isset($this->sheets[$idx]['frozen'][1])) { + // $AC = 'A1'; // Active Cell + $x = $y = 0; + if (is_string($this->sheets[$idx]['frozen'])) { + $AC = $this->sheets[$idx]['frozen']; + self::cell2coord($AC, $x, $y); + } else { + if (isset($this->sheets[$idx]['frozen'][0])) { + $x = $this->sheets[$idx]['frozen'][0]; + } + if (isset($this->sheets[$idx]['frozen'][1])) { + $y = $this->sheets[$idx]['frozen'][1]; + } + $AC = self::coord2cell($x, $y); + } + if ($x > 0 || $y > 0) { + $split = ''; + if ($x > 0) { + $split .= ' xSplit="' . $x . '"'; + } + if ($y > 0) { + $split .= ' ySplit="' . $y . '"'; + } + $activepane = 'bottomRight'; + if ($x > 0 && $y === 0) { + $activepane = 'topRight'; + } + if ($x === 0 && $y > 0) { + $activepane = 'bottomLeft'; + } + $PANE .= ''; + $PANE .= ''; + } + } + if ($this->rtl || $PANE) { + $SHEETVIEWS .= ' +rtl ? ' rightToLeft="1"' : ''); + $SHEETVIEWS .= $PANE ? ">\r\n" . $PANE . "\r\n" : ' />'; + $SHEETVIEWS .= "\r\n"; + } + $CUR_ROW = 0; + $COL = []; + foreach ($this->sheets[$idx]['rows'] as $r) { + $CUR_ROW++; + $row = ''; + $CUR_COL = 0; + $RH = 0; // row height + foreach ($r as $v) { + $CUR_COL++; + if (!isset($COL[$CUR_COL])) { + $COL[$CUR_COL] = 0; + } + $cname = self::coord2cell($CUR_COL-1) . $CUR_ROW; + if ($v === null || $v === '') { + $row .= ''; + continue; + } + $ct = $cv = $cf = null; + $N = $NF = $A = $F = $FL = $C = $BG = $FS = $FR = 0; + $m = $m2 = null; + $BR = ''; + if (is_string($v)) { + if (strpos($v, "\0") === 0) { // RAW value as string + $v = substr($v, 1); + $vl = mb_strlen($v); + } else { + if (strpos($v, '<') !== false) { // tags? + if (preg_match('//s', $v, $m)) { + $com = trim($m[1]); + $a = $this->author; + if (preg_match('/^([^:]+): (.*)$/us', $com, $m2)) { + $a = $m2[1]; + /** @noinspection MultiAssignmentUsageInspection */ + $com = $m2[2]; + } + $this->sheets[$idx]['comments'][] = ['C' => $CUR_COL, 'R' => $CUR_ROW, 'N' => $cname, 'T' => $com, 'A' => $a]; + $v = str_replace($m[0], '', $v); + } + if (strpos($v, '') !== false) { + $F += self::F_BOLD; + } + if (strpos($v, '') !== false) { + $F += self::F_ITALIC; + } + if (strpos($v, '') !== false) { + $F += self::F_UNDERLINE; + } + if (strpos($v, '') !== false) { + $F += self::F_STRIKE; + } + if (preg_match('/]+)>/', $v, $m)) { + if (preg_match('/ color="([^"]+)"/', $m[1], $m2)) { + $F += self::F_COLOR; + $c = ltrim($m2[1], '#'); + $C = strlen($c) === 8 ? $c : ('FF' . $c); + } + if (preg_match('/ bgcolor="([^"]+)"/', $m[1], $m2)) { + $FL += self::FL_COLOR; + $c = ltrim($m2[1], '#'); + $BG = strlen($c) === 8 ? $c : ('FF' . $c); + } + if (preg_match('/ height="([^"]+)"/', $m[1], $m2)) { + $RH = $m2[1]; + } + if (preg_match('/ nf="([^"]+)"/', $m[1], $m2)) { + $c = htmlspecialchars_decode($m2[1], ENT_QUOTES); + $NF = $this->getNumFmtId($c); + } + if (preg_match('/ border="([^"]+)"/', $m[1], $m2)) { + $b = htmlspecialchars_decode($m2[1], ENT_QUOTES); + if ($b && $b !== 'none') { + $BR = $b; + } + } + if (preg_match('/ font-size="([^"]+)"/', $m[1], $m2)) { + $FS = (int)$m2[1]; + if ($RH === 0) { // fix row height + $RH = ($FS > $this->defaultFontSize) ? round($FS * 1.50, 1, PHP_ROUND_HALF_UP) : 0; + } + } + } + if (strpos($v, '') !== false) { + $A += self::A_LEFT; + } + if (strpos($v, '
') !== false) { + $A += self::A_CENTER; + } + if (strpos($v, '') !== false) { + $A += self::A_RIGHT; + } + if (strpos($v, '') !== false) { + $A += self::A_TOP; + } + if (strpos($v, '') !== false) { + $A += self::A_MIDDLE; + } + if (strpos($v, '') !== false) { + $A += self::A_BOTTOM; + } + if (strpos($v, '') !== false) { + $A += self::A_WRAPTEXT; + } + if (strpos($v, '') !== false) { + $A += self::A_ROTATION_90; + } + if (preg_match('/(.*?)<\/a>/i', $v, $m)) { + $F += self::F_HYPERLINK; + + $h = explode('#', $m[1]); + if (count($h) === 1) { + if (strpos($h[0], '!') > 0) { // internal hyperlink + $this->sheets[$idx]['hyperlinks'][] = ['ID' => null, 'R' => $cname, 'H' => null, 'L' => $m[1]]; + } else { + $this->sheets[$idx]['relidx']++; + $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->sheets[$idx]['relidx'], 'R' => $cname, 'H' => $m[1], 'L' => '']; + } + } else { + $this->sheets[$idx]['relidx']++; + $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->sheets[$idx]['relidx'], 'R' => $cname, 'H' => $h[0], 'L' => $h[1]]; + } + } + // formatted raw? + if (preg_match('/(.*)<\/raw>/', $v, $m)) { + $FR = 1; + $v = $m[1]; + } elseif (preg_match('/]*)>/', $v, $m)) { + $cf = strip_tags($v); + $v = 0; + if (preg_match('/ v="([^"]+)"/', $m[1], $m2)) { + $v = $m2[1]; + } + } else { + $v = strip_tags($v); + } + } // \tags + $vl = mb_strlen($v); + if ($FR) { + $v = htmlspecialchars_decode($v); + $vl = mb_strlen($v); + } elseif ($v === '0' || preg_match('/^[-+]?[1-9]\d{0,14}$/', $v)) { // Integer as General + $cv = ltrim($v, '+'); + if ($vl > 10) { + $N = self::N_INT; // [1] 0 + } + } elseif (preg_match('/^[-+]?(0|[1-9]\d*)\.(\d+)$/', $v, $m)) { + $cv = ltrim($v, '+'); + if (strlen($m[2]) < 3) { + $N = self::N_DEC; + } + } elseif (preg_match('/^\$[-+]?[0-9\.]+$/', $v)) { // currency $? + $N = self::N_DOLLAR; + $cv = ltrim($v, '+$'); + } elseif (preg_match('/^[-+]?[0-9\.]+( ₽| €)$/u', $v, $m)) { // currency ₽ €? + if ($m[1] === ' ₽') { + $N = self::N_RUB; + } elseif ($m[1] === ' €') { + $N = self::N_EURO; + } + $cv = trim($v, ' +₽€'); + } elseif (preg_match('/^([-+]?\d+)%$/', $v, $m)) { + $cv = round($m[1] / 100, 2, PHP_ROUND_HALF_UP); + $N = self::N_PERCENT_INT; // [9] 0% + } elseif (preg_match('/^([-+]?\d+\.\d+)%$/', $v, $m)) { + $cv = round($m[1] / 100, 4, PHP_ROUND_HALF_UP); + $N = self::N_PRECENT_DEC; // [10] 0.00% + } elseif (preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d)$/', $v, $m)) { + $cv = self::date2excel($m[1], $m[2], $m[3]); + $N = self::N_DATE; // [14] mm-dd-yy + } elseif (preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d)$/', $v, $m)) { + $cv = self::date2excel($m[3], $m[2], $m[1]); + $N = self::N_DATE; // [14] mm-dd-yy + } elseif (preg_match('/^(\d\d):(\d\d):(\d\d)$/', $v, $m)) { + $cv = self::date2excel(0, 0, 0, $m[1], $m[2], $m[3]); + $N = self::N_TIME; // time + } elseif (preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m)) { + $cv = self::date2excel($m[1], $m[2], $m[3], $m[4], $m[5], $m[6]); + $N = ((int)$m[1] === 0) ? self::N_TIME : self::N_DATETIME; // [22] m/d/yy h:mm + } elseif (preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m)) { + $cv = self::date2excel($m[3], $m[2], $m[1], $m[4], $m[5], $m[6]); + $N = self::N_DATETIME; // [22] m/d/yy h:mm + } elseif (preg_match('/^[0-9+-.]+$/', $v)) { // Long ? + $A += ($A & (self::A_LEFT | self::A_CENTER)) ? 0 : self::A_RIGHT; + } elseif (preg_match('/^https?:\/\/\S+$/i', $v)) { // Hyperlink ? + $h = explode('#', $v); + $this->sheets[$idx]['relidx']++; + $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->sheets[$idx]['relidx'], 'R' => $cname, 'H' => $h[0], 'L' => isset($h[1]) ? $h[1] : '']; + $F += self::F_HYPERLINK; + } elseif (preg_match("/^[a-zA-Z0-9_\.\-]+@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/", $v)) { // email? + $this->sheets[$idx]['relidx']++; + $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->sheets[$idx]['relidx'], 'R' => $cname, 'H' => 'mailto:' . $v, 'L' => '']; + $F += self::F_HYPERLINK; + } elseif (strpos($v,"\n") !== false) { + $A |= self::A_WRAPTEXT; + } + + if (($N === self::N_DATE || $N === self::N_DATETIME) && $cv < 0) { + $cv = null; + $N = 0; + } + + } + if ($cv === null) { // string + $v = self::esc($v); + if ($cf) { + $ct = 'str'; + $cv = $v; + } elseif ($v === '') { + $ct = 's'; + } elseif (mb_strlen($v) > 160) { + $ct = 'inlineStr'; + $cv = $v; + } else { + $ct = 's'; // shared string + $cv = false; + $skey = '~' . $v; + if (isset($this->SI_KEYS[$skey])) { + $cv = $this->SI_KEYS[$skey]; + } + if ($cv === false) { + $this->SI[] = $v; + $cv = count($this->SI) - 1; + $this->SI_KEYS[$skey] = $cv; + } + } + } + } elseif (is_int($v)) { + $vl = mb_strlen((string)$v); + $cv = $v; + } elseif (is_float($v)) { + $vl = mb_strlen((string)$v); + $cv = $v; + } elseif ($v instanceof \DateTime) { + $vl = 16; + $cv = self::date2excel($v->format('Y'), $v->format('m'), $v->format('d'), $v->format('H'), $v->format('i'), $v->format('s')); + $N = self::N_DATETIME; // [22] m/d/yy h:mm + } else { + continue; + } + if ($NF) { + $N = $NF; + } + $COL[$CUR_COL] = max($vl, $COL[$CUR_COL]); + $cs = 0; + if (($N + $A + $F + $FL + $FS > 0) || $BR !== '') { + if ($FL === self::FL_COLOR) { + $FL += self::FL_SOLID; + } + if (($F & self::F_HYPERLINK) && !($F & self::F_COLOR)) { + $F += self::F_COLOR; + $C = 'FF0563C1'; + } + $XF_KEY = $N . '-' . $A . '-' . $F . '-' . $FL . '-' . $C . '-' . $BG . '-' . $BR . '-' . $FS; + if (isset($this->XF_KEYS[$XF_KEY])) { + $cs = $this->XF_KEYS[$XF_KEY]; + } + if ($cs === 0) { + $cs = count($this->XF); + $this->XF_KEYS[$XF_KEY] = $cs; + $this->XF[] = [$N, $A, $F, $FL, $C, $BG, $BR, $FS]; + } + } + $row .= '' . $cf . '' : '') + . ($ct === 'inlineStr' ? '' . $cv . '' : '' . $cv . '') + . "\r\n"; + } + } + $ROWS[] = '' . $row . ""; + } + $COLS[] = ''; + foreach ($COL as $k => $max) { + if ($max === 0) { + continue; + } + $w = isset($this->sheets[$idx]['colwidth'][$k]) ? $this->sheets[$idx]['colwidth'][$k] : min($max + 1, 60); + $COLS[] = ''; + } + $COLS[] = ''; + if (count($COLS) === 2) { + $COLS = []; + } + $ROWS[] = ''; + $REF = 'A1:' . self::coord2cell(count($COL)-1) . $CUR_ROW; + } else { + $ROWS[] = ''; + $REF = 'A1:A1'; + } + + $AUTOFILTER = ''; + if ($this->sheets[$idx]['autofilter']) { + $AUTOFILTER = ''; + } + + $MERGECELLS = []; + if (count($this->sheets[$idx]['mergecells'])) { + $MERGECELLS[] = ''; + $MERGECELLS[] = ''; + foreach ($this->sheets[$idx]['mergecells'] as $m) { + $MERGECELLS[] = ''; + } + $MERGECELLS[] = ''; + } + + $HYPERLINKS = []; + if (count($this->sheets[$idx]['hyperlinks'])) { + $HYPERLINKS[] = ''; + foreach ($this->sheets[$idx]['hyperlinks'] as $h) { + $HYPERLINKS[] = ''; + } + $HYPERLINKS[] = ''; + } + if (count($this->sheets[$idx]['comments'])) { + + $VML[] = ''; + } + + //restore locale + setlocale(LC_NUMERIC, $_loc); + + return str_replace( + ['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}', '{VML}'], + [ + $REF, + implode("\r\n", $COLS), + implode("\r\n", $ROWS), + $AUTOFILTER, + implode("\r\n", $MERGECELLS), + implode("\r\n", $HYPERLINKS), + $SHEETVIEWS, + implode("\r\n", $VML) + ], + $template + ); + } + + public function setDefaultFont($name) + { + $this->defaultFont = $name; + return $this; + } + + public function setDefaultFontSize($size) + { + $this->defaultFontSize = $size; + return $this; + } + + public function setTitle($title) + { + $this->title = $title; + return $this; + } + public function setSubject($subject) + { + $this->subject = $subject; + return $this; + } + public function setAuthor($author) + { + $this->author = $author; + return $this; + } + public function setCompany($company) + { + $this->company = $company; + return $this; + } + public function setManager($manager) + { + $this->manager = $manager; + return $this; + } + public function setKeywords($keywords) + { + $this->keywords = $keywords; + return $this; + } + public function setDescription($description) + { + $this->description = $description; + return $this; + } + public function setCategory($category) + { + $this->category = $category; + return $this; + } + + public function setLanguage($language) + { + $this->language = $language; + return $this; + } + + public function setApplication($application) + { + $this->application = $application; + return $this; + } + public function setLastModifiedBy($lastModifiedBy) + { + $this->lastModifiedBy = $lastModifiedBy; + return $this; + } + + /** + * @param $range string 'A2:B10' + * @return $this + */ + public function autoFilter($range) + { + $this->sheets[$this->curSheet]['autofilter'] = $range; + return $this; + } + + public function mergeCells($range) + { + $this->sheets[$this->curSheet]['mergecells'][] = $range; + return $this; + } + + public function setColWidth($col, $width) + { + $this->sheets[$this->curSheet]['colwidth'][$col] = $width; + return $this; + } + public function rightToLeft($value = true) + { + $this->rtl = $value; + return $this; + } + + public function freezePanes($cell) + { + $this->sheets[$this->curSheet]['frozen'] = $cell; + return $this; + } + + public function getNumFmtId($code) + { + if (isset($this->NF[$code])) { // id? + return (int)$code; + } + if (isset($this->NF_KEYS[$code])) { + return $this->NF_KEYS[$code]; + } + $id = self::N_CUSTOM + count($this->NF); // custom + $this->NF[$id] = $code; + $this->NF_KEYS[$code] = $id; + return $id; + } + + public static function date2excel($year, $month, $day, $hours = 0, $minutes = 0, $seconds = 0) + { + $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; + $year = (int) $year; + $month = (int) $month; + $day = (int) $day; +// echo "y=$year m=$month d=$day h=$hours m=$minutes s=$seconds".PHP_EOL; + if ($year === 0) { + return $excelTime; + } + // self::CALENDAR_WINDOWS_1900 + $excel1900isLeapYear = 1; + if (($year === 1900) && ($month <= 2)) { + $excel1900isLeapYear = 0; + } + $myExcelBaseDate = 2415020; + // Julian base date Adjustment + if ($month > 2) { + $month -= 3; + } else { + $month += 9; + --$year; + } + $century = floor($year / 100); + $decade = $year - floor($year / 100) * 100; +// echo "y=$year m=$month d=$day cent=$century dec=$decade h=$hours m=$minutes s=$seconds".PHP_EOL; + // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) + $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myExcelBaseDate + $excel1900isLeapYear; + return (float)$excelDate + $excelTime; + } + + + + public static function esc($str) + { + // XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + // but we use fast version + return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&', '<', '>', '', '', ''], $str); + } + + + public static function raw($value) + { + return "\0" . $value; + } + public static function rawArray($array) + { + foreach ($array as &$row) { + foreach ($row as &$value) { + $value = "\0" . $value; + } + } + return $array; + } + + public static function cell2coord($cell, &$x, &$y) + { + $x = $y = 0; + $m = null; + if (preg_match('/^([A-Z]+)(\d+)$/', $cell, $m)) { + $len = strlen($m[1]); + for ($i = 0; $i < $len; $i++) { + $int = ord($m[1][$i]) - 65; // A -> 0, B -> 1 + $int += ($i === $len - 1) ? 0 : 1; + $x += $int * pow(26, $len-$i-1); + } + $y = ((int)$m[2]) - 1; + } + } + + public static function coord2cell($x, $y = null) + { + $c = ''; + for ($i = $x; $i >= 0; $i = ((int)($i / 26)) - 1) { + $c = chr(65 + $i % 26) . $c; + } + return $c . ($y === null ? '' : ($y + 1)); + } + +} diff --git a/templates/attendance.php b/templates/attendance.php new file mode 100644 index 0000000..fefd804 --- /dev/null +++ b/templates/attendance.php @@ -0,0 +1,148 @@ + 'Minggu', + 'Monday' => 'Senin', + 'Tuesday' => 'Selasa', + 'Wednesday' => 'Rabu', + 'Thursday' => 'Kamis', + 'Friday' => 'Jumat', + 'Saturday' => 'Sabtu' +]; +$day_of_week = date('l', strtotime($selected_tanggal)); +$hari_indonesia = $day_map[$day_of_week]; + +if ($selected_kelas_id) { + $murid_list = get_murid_by_kelas($selected_kelas_id); + $jadwal_hari_ini = get_jadwal_by_kelas_and_hari($selected_kelas_id, $hari_indonesia); + $absensi_data = get_absensi($selected_kelas_id, $selected_tanggal); +} + +$statuses = ['Hadir', 'Izin', 'Sakit', 'Alpa']; + +?> + +
+
+

Manajemen Absensi

+
+ + +
+ +
+ + + +
+
+
Pilih Kelas dan Tanggal
+
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + + +
+ + Tidak ada murid di kelas ini. + + Tidak ada jadwal pelajaran untuk hari di kelas ini. + +
+ + +
+
+
Formulir Absensi -
+
+
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + +
Nama Murid +
+ +
+ +
+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/templates/classes.php b/templates/classes.php new file mode 100644 index 0000000..13d2409 --- /dev/null +++ b/templates/classes.php @@ -0,0 +1,108 @@ + + +
\ No newline at end of file diff --git a/templates/dashboard.php b/templates/dashboard.php new file mode 100644 index 0000000..d830bbb --- /dev/null +++ b/templates/dashboard.php @@ -0,0 +1,27 @@ + +
+
+

Dashboard

+
+ +
+
+
+
+
Manajemen Siswa
+

Kelola data siswa, tambah siswa baru, dan lihat daftar siswa.

+ Go to Siswa +
+
+
+
+
+
+
Manajemen Kelas
+

Buat kelas baru, atur siswa ke dalam kelas, dan kelola informasi kelas.

+ Go to Kelas +
+
+
+
+
diff --git a/templates/footer.php b/templates/footer.php new file mode 100644 index 0000000..ab5e490 --- /dev/null +++ b/templates/footer.php @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + diff --git a/templates/header.php b/templates/header.php new file mode 100644 index 0000000..1eecebc --- /dev/null +++ b/templates/header.php @@ -0,0 +1,16 @@ + + + + + + + + Sistem Informasi Akademik + + + + + + + +
diff --git a/templates/murid.php b/templates/murid.php new file mode 100644 index 0000000..31665f9 --- /dev/null +++ b/templates/murid.php @@ -0,0 +1,243 @@ + + +
+
+

Manajemen Murid

+ + + + Kembali ke Daftar + +
+ + +
+ +
+ + + +
Data murid berhasil dihapus.
+ + +
Terjadi kesalahan saat menghapus data.
+ + +
Data murid tidak ditemukan.
+ + + + + + +
+
+
+
+
+
+ + + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
Foto dan Lokasi
+ +
+
+ + + +
+ Foto saat ini + +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+

Klik pada peta untuk mengatur koordinat Latitude dan Longitude.

+ + +
+
+
+ + + + +
+
+
Impor Data dari XLSX
+
+
+

Pastikan urutan kolom di file Excel Anda adalah: NIS, NISN, Nama Lengkap, Tanggal Lahir, Alamat.

+
+ +
+ + +
+ +
+
+
+ + +
+ + + + + + + + + + + + + + 0): + $i = 1; + foreach ($all_murid as $m): + $foto_path = 'assets/uploads/murid/' . ($m['foto'] ?? 'default.png'); + if (empty($m['foto']) || !file_exists($foto_path)) { + $foto_path = 'assets/images/placeholder.png'; // Fallback to a placeholder + } + ?> + + + + + + + + + + + + + + + +
#FotoNISNama LengkapKelasJenis KelaminAksi
+ Foto <?php echo htmlspecialchars($m['nama_lengkap']); ?> + Belum diatur'); ?> + Ubah + Hapus +
Belum ada data murid.
+
+ + +
diff --git a/templates/reports.php b/templates/reports.php new file mode 100644 index 0000000..47bdcb2 --- /dev/null +++ b/templates/reports.php @@ -0,0 +1,111 @@ + + +
+

Laporan Absensi Murid

+ + +
+
+
Filter Laporan
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + + +
+
+
Rekapitulasi Absensi
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + $rekap) : ?> + + + + + + + + + + + + + +
NoNISNama LengkapHadirSakitIzinAlpaTotal Jam
Tidak ada data absensi untuk periode dan kelas yang dipilih.
+
+
+
+ +
\ No newline at end of file diff --git a/templates/schedule.php b/templates/schedule.php new file mode 100644 index 0000000..48c2733 --- /dev/null +++ b/templates/schedule.php @@ -0,0 +1,160 @@ + + +
+ +

+ + +
+ +
+ + +
+
+
Formulir Jadwal
+
+
+
+ + + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + Batal + +
+
+
+ +
+
+
Daftar Jadwal Pelajaran
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KelasHariWaktuMata PelajaranGuruAksi
Belum ada data jadwal.
+ Ubah + Hapus +
+
+
+
+ +
\ No newline at end of file diff --git a/templates/sidebar.php b/templates/sidebar.php new file mode 100644 index 0000000..72a51cc --- /dev/null +++ b/templates/sidebar.php @@ -0,0 +1,58 @@ + + diff --git a/templates/subjects.php b/templates/subjects.php new file mode 100644 index 0000000..cf62f77 --- /dev/null +++ b/templates/subjects.php @@ -0,0 +1,88 @@ + + +
+
+ +
+
+
+

+
+
+ +
+ +
+ +
+ + +
+ + +
+ + + Batal + +
+
+
+
+ + +
+
+
+

Daftar Mata Pelajaran

+
+
+ + + + + + + + + + $mapel): ?> + + + + + + + +
NoNama Mata PelajaranAksi
+ Ubah + Hapus +
+
+
+
+
+
+ diff --git a/templates/teachers.php b/templates/teachers.php new file mode 100644 index 0000000..9450a19 --- /dev/null +++ b/templates/teachers.php @@ -0,0 +1,166 @@ + + +
+
+

Manajemen Guru

+ + Tambah Guru + + Kembali ke Daftar + +
+ + +
+ +
+ + + +
Data guru berhasil dihapus.
+ + +
Terjadi kesalahan saat menghapus data.
+ + +
Data guru tidak ditemukan.
+ + + + + +
+
+
+
+
+
+ + + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
Foto dan Lokasi
+ +
+ + + +
+ Foto saat ini + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+

Klik pada peta untuk mengatur koordinat Latitude dan Longitude.

+ + +
+
+
+ + + +
+ + + + + + + + + + + + 0): + $i = 1; + foreach ($all_guru as $g): + $foto_path = 'assets/uploads/guru/' . ($g['foto'] ?? 'default.png'); + if (empty($g['foto']) || !file_exists($foto_path)) { + $foto_path = 'assets/images/placeholder.png'; + } + ?> + + + + + + + + + + + + + +
#FotoNIPNamaAksi
+ Foto <?php echo htmlspecialchars($g['nama']); ?> + + Ubah + Hapus +
Belum ada data guru.
+
+ + +
\ No newline at end of file