Files
Aplikasi-Kehadiran-KTM/app/Support/SimpleXlsx.php
Saufi dfd22ff2af Aplikasi kehadiran KTM ambil peti undi (SPR 2026)
Sistem untuk admin DUN merekod waktu KTM (Ketua Tempat Mengundi) hadir
mengambil peti undi. Data KTM dibaca daripada DB sppm (spr2026) melalui
connection kedua — hanya penempatan berjawatan KTM yang dipaparkan.

- Dashboard: senarai KTM ikut pusat mengundi (asc) & saluran (asc),
  status BELUM HADIR (merah) / HADIR (hijau) + tarikh & masa
- Carian nama/no. KP, tapisan pusat mengundi & status
- Tanda hadir / batal dengan kawalan DUN (admin terhad ke DUN sendiri)
- Eksport Excel (worksheet Hadir & Belum Hadir) guna SimpleXlsx
- Peranan: superadmin (urus pengguna, semua DUN) & admin DUN
- Docker: nginx + php-fpm + queue + scheduler + webhook deploy,
  port 127.0.0.1:8009, TZ Asia/Kuala_Lumpur di semua container
- Zon waktu aplikasi: Asia/Kuala_Lumpur (APP_TIMEZONE)

Berasaskan scaffold spr2026_taklimat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 02:40:00 +08:00

192 lines
6.9 KiB
PHP

<?php
namespace App\Support;
use ZipArchive;
/**
* Penjana .xlsx ringan tanpa kebergantungan luar. Setiap nilai sel ditulis
* sebagai "inline string" supaya nilai seperti No. KP kekal sebagai teks
* (mengekalkan sengkang / sifar di hadapan). Menyokong berbilang worksheet.
*/
class SimpleXlsx
{
/** @var array<int,array{name:string,rows:array<int,array<int,scalar|null>>}> */
private array $sheets = [];
/**
* Tambah satu worksheet. Baris pertama dianggap pengepala (huruf tebal).
*
* @param array<int,array<int,scalar|null>> $rows
*/
public function addSheet(string $name, array $rows): static
{
$this->sheets[] = ['name' => $this->namaSah($name), 'rows' => $rows];
return $this;
}
/**
* Hasilkan kandungan binari fail .xlsx.
*/
public function toString(): string
{
$tmp = tempnam(sys_get_temp_dir(), 'xlsx');
$zip = new ZipArchive;
$zip->open($tmp, ZipArchive::OVERWRITE);
$zip->addFromString('[Content_Types].xml', $this->contentTypes());
$zip->addFromString('_rels/.rels', $this->rootRels());
$zip->addFromString('xl/workbook.xml', $this->workbook());
$zip->addFromString('xl/_rels/workbook.xml.rels', $this->workbookRels());
$zip->addFromString('xl/styles.xml', $this->styles());
foreach ($this->sheets as $i => $sheet) {
$zip->addFromString('xl/worksheets/sheet'.($i + 1).'.xml', $this->sheetXml($sheet['rows']));
}
$zip->close();
$data = file_get_contents($tmp);
@unlink($tmp);
return $data;
}
private function namaSah(string $name): string
{
$name = preg_replace('/[\\\\\/\*\?\[\]:]/', ' ', $name);
return mb_substr(trim($name), 0, 31) ?: 'Sheet';
}
/**
* Tukar indeks lajur (0-asas) kepada huruf A, B, … AA.
*/
private function col(int $index): string
{
$letters = '';
$index++;
while ($index > 0) {
$mod = ($index - 1) % 26;
$letters = chr(65 + $mod).$letters;
$index = intdiv($index - 1, 26);
}
return $letters;
}
private function cell(string $ref, string $value, int $style): string
{
$v = htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
$s = $style > 0 ? ' s="'.$style.'"' : '';
return '<c r="'.$ref.'"'.$s.' t="inlineStr"><is><t xml:space="preserve">'.$v.'</t></is></c>';
}
/**
* @param array<int,array<int,scalar|null>> $rows
*/
private function sheetXml(array $rows): string
{
$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
.'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>';
foreach ($rows as $r => $cells) {
$rowNum = $r + 1;
$style = $r === 0 ? 1 : 0;
$xml .= '<row r="'.$rowNum.'">';
foreach (array_values($cells) as $c => $val) {
$xml .= $this->cell($this->col($c).$rowNum, (string) $val, $style);
}
$xml .= '</row>';
}
return $xml.'</sheetData></worksheet>';
}
private function contentTypes(): string
{
$overrides = '';
foreach ($this->sheets as $i => $s) {
$overrides .= '<Override PartName="/xl/worksheets/sheet'.($i + 1).'.xml" '
.'ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
}
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
.'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
.'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
.'<Default Extension="xml" ContentType="application/xml"/>'
.'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
.'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
.$overrides
.'</Types>';
}
private function rootRels(): string
{
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
.'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
.'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
.'</Relationships>';
}
private function workbook(): string
{
$sheetsXml = '';
foreach ($this->sheets as $i => $s) {
$name = htmlspecialchars($s['name'], ENT_QUOTES | ENT_XML1, 'UTF-8');
$sheetsXml .= '<sheet name="'.$name.'" sheetId="'.($i + 1).'" r:id="rId'.($i + 1).'"/>';
}
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
.'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
.'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
.'<sheets>'.$sheetsXml.'</sheets></workbook>';
}
private function workbookRels(): string
{
$rels = '';
$count = count($this->sheets);
foreach ($this->sheets as $i => $s) {
$rels .= '<Relationship Id="rId'.($i + 1).'" '
.'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" '
.'Target="worksheets/sheet'.($i + 1).'.xml"/>';
}
$rels .= '<Relationship Id="rId'.($count + 1).'" '
.'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" '
.'Target="styles.xml"/>';
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
.'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
.$rels.'</Relationships>';
}
private function styles(): string
{
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
.'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
.'<fonts count="2">'
.'<font><sz val="11"/><name val="Calibri"/></font>'
.'<font><b/><sz val="11"/><name val="Calibri"/></font>'
.'</fonts>'
.'<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
.'<borders count="1"><border/></borders>'
.'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
.'<cellXfs count="2">'
.'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
.'<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>'
.'</cellXfs>'
.'</styleSheet>';
}
}