Compare commits
36
Commits
0360838b56
...
dc9315be9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc9315be9e | ||
|
|
0f8171e3c1 | ||
|
|
e6eb7ac419 | ||
|
|
80062fcfb4 | ||
|
|
221f3ae027 | ||
|
|
6688865090 | ||
|
|
0bfeed1d08 | ||
|
|
ac85976d18 | ||
|
|
79f3b8c757 | ||
|
|
ea61a16427 | ||
|
|
1edca2da0a | ||
|
|
753b23fef5 | ||
|
|
aa30c9a0b2 | ||
|
|
2dde88e10d | ||
|
|
f80ac989b3 | ||
|
|
44207c9aed | ||
|
|
df74334617 | ||
|
|
e742b16b76 | ||
|
|
86f554c944 | ||
|
|
fb0456b1e8 | ||
|
|
436f02a97d | ||
|
|
105d307c59 | ||
|
|
14cb098bbf | ||
|
|
7760e2e494 | ||
|
|
0003ade44a | ||
|
|
38400b5cd6 | ||
|
|
2988aa4988 | ||
|
|
1a76385cf0 | ||
|
|
bc4b481631 | ||
|
|
b72c105247 | ||
|
|
076cb7bb3c | ||
|
|
cfd82125c5 | ||
|
|
629f6efb08 | ||
|
|
39c9e031fd | ||
|
|
25da284ca2 | ||
|
|
f7adecc22c |
@@ -30,3 +30,4 @@ public/aplikasi-wali/
|
|||||||
public/preview/
|
public/preview/
|
||||||
public/apk/
|
public/apk/
|
||||||
/storage/firebase/
|
/storage/firebase/
|
||||||
|
public/aplikasi-ejurnal/
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Backup Otomatis — Aplikasi Ketertiban
|
||||||
|
|
||||||
|
Aplikasi ini tercakup dalam sistem **backup harian otomatis** server.
|
||||||
|
|
||||||
|
## Lokasi backup
|
||||||
|
- Script backup: `/home/guru-tkj/backups/backup-all.sh`
|
||||||
|
- Hasil backup: `/home/guru-tkj/backups/harian/YYYYMMDD-HHMM/`
|
||||||
|
- Log: `/home/guru-tkj/backups/backup-all.log`
|
||||||
|
|
||||||
|
## Detail aplikasi ini
|
||||||
|
- Folder aplikasi: `/var/www/aplikasi-ketertiban`
|
||||||
|
- Database: `aplikasi_ketertiban` → file `aplikasi_ketertiban.sql.gz`
|
||||||
|
- File upload aplikasi: `upload-aplikasi-ketertiban.tar.gz` (berisi folder `storage/app/public`)
|
||||||
|
|
||||||
|
## Jadwal & retensi
|
||||||
|
- Berjalan otomatis tiap hari ±02:30 WIB (cron server)
|
||||||
|
- Retensi lokal: 30 hari (backup lama otomatis dihapus)
|
||||||
|
- Cakupan backup: semua database produksi + file upload per aplikasi + faceid + keystore
|
||||||
|
|
||||||
|
## Cara restore
|
||||||
|
- Database: `gunzip -c aplikasi_ketertiban.sql.gz | mysql -u <user> -p <db>`
|
||||||
|
- Upload: `tar xzf upload-aplikasi-ketertiban.tar.gz -C /` (jika ada)
|
||||||
|
|
||||||
|
> Perbarui file ini jika lokasi/struktur backup berubah.
|
||||||
@@ -69,7 +69,7 @@ class AttendanceSyncController extends Controller
|
|||||||
'attendances.*.source_student_id' => 'required|integer',
|
'attendances.*.source_student_id' => 'required|integer',
|
||||||
'attendances.*.date' => 'required|date',
|
'attendances.*.date' => 'required|date',
|
||||||
'attendances.*.lesson_hour' => 'required|integer|min:1|max:10',
|
'attendances.*.lesson_hour' => 'required|integer|min:1|max:10',
|
||||||
'attendances.*.status' => 'required|in:hadir,sakit,izin,alpha',
|
'attendances.*.status' => 'required|in:hadir,sakit,izin,alpha,pkl',
|
||||||
'attendances.*.notes' => 'nullable|string|max:255',
|
'attendances.*.notes' => 'nullable|string|max:255',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -90,8 +90,7 @@ class AttendanceSyncController extends Controller
|
|||||||
$classNameMap = $students->pluck('class_name', 'id');
|
$classNameMap = $students->pluck('class_name', 'id');
|
||||||
|
|
||||||
$alphaCounts = [];
|
$alphaCounts = [];
|
||||||
$alphaDate = null;
|
$datesByStudent = []; // student_id => [date => true] — untuk pembersihan violation per tanggal
|
||||||
$allStudentIdsInPush = []; // track all students for cleanup
|
|
||||||
|
|
||||||
foreach ($attendances as $item) {
|
foreach ($attendances as $item) {
|
||||||
$sourceId = $item['source_student_id'];
|
$sourceId = $item['source_student_id'];
|
||||||
@@ -103,13 +102,14 @@ class AttendanceSyncController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$localStudentId = $studentMap[$sourceId];
|
$localStudentId = $studentMap[$sourceId];
|
||||||
$allStudentIdsInPush[$localStudentId] = true;
|
$datesByStudent[$localStudentId][$item['date']] = true;
|
||||||
|
|
||||||
$statusMap = [
|
$statusMap = [
|
||||||
'present' => 'hadir',
|
'present' => 'hadir',
|
||||||
'sick' => 'sakit',
|
'sick' => 'sakit',
|
||||||
'permission' => 'izin',
|
'permission' => 'izin',
|
||||||
'absent' => 'alpha',
|
'absent' => 'alpha',
|
||||||
|
'pkl' => 'pkl', // PKL/Magang industri (e-jurnal)
|
||||||
];
|
];
|
||||||
$localStatus = $statusMap[$item['status']] ?? $item['status'];
|
$localStatus = $statusMap[$item['status']] ?? $item['status'];
|
||||||
|
|
||||||
@@ -136,7 +136,6 @@ class AttendanceSyncController extends Controller
|
|||||||
|
|
||||||
if ($localStatus === 'alpha') {
|
if ($localStatus === 'alpha') {
|
||||||
$alphaCounts[$localStudentId] = ($alphaCounts[$localStudentId] ?? 0) + 1;
|
$alphaCounts[$localStudentId] = ($alphaCounts[$localStudentId] ?? 0) + 1;
|
||||||
$alphaDate = $item['date'];
|
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$results['errors'][] = "student_id={$sourceId} jam={$item['lesson_hour']}: " . $e->getMessage();
|
$results['errors'][] = "student_id={$sourceId} jam={$item['lesson_hour']}: " . $e->getMessage();
|
||||||
@@ -144,57 +143,63 @@ class AttendanceSyncController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-generate / update / hapus violations untuk alpha siswa di push ini
|
// Auto-generate / update / hapus violations alpha per (siswa, tanggal) dari push ini.
|
||||||
|
// FIX 2026-08-31: sebelumnya memakai $alphaDate global (null saat push tanpa alpha)
|
||||||
|
// sehingga violation lama tidak pernah dihapus → false positive; dan $totalAlpha
|
||||||
|
// dihitung untuk semua siswa memakai satu tanggal global → rawan salah tanggal/siswa.
|
||||||
|
// Sekarang iterasi per (student, date) yang benar-benar muncul di push.
|
||||||
$alphaType = ViolationType::where('slug', 'alpha')->first();
|
$alphaType = ViolationType::where('slug', 'alpha')->first();
|
||||||
|
|
||||||
if ($alphaType) {
|
if ($alphaType) {
|
||||||
$violationService = app(\App\Services\ViolationService::class);
|
$violationService = app(\App\Services\ViolationService::class);
|
||||||
|
|
||||||
foreach ($allStudentIdsInPush as $studentId => $_) {
|
foreach ($datesByStudent as $studentId => $dates) {
|
||||||
try {
|
foreach (array_keys($dates) as $date) {
|
||||||
// Hitung total alpha di DB (akumulasi dari semua push, bukan cuma push ini)
|
try {
|
||||||
$totalAlpha = \App\Models\Attendance::where('student_id', $studentId)
|
// Hitung total alpha di DB per (siswa, tanggal) — akumulasi dari semua push
|
||||||
->where('date', $alphaDate)
|
$totalAlpha = \App\Models\Attendance::where('student_id', $studentId)
|
||||||
->where('status', 'alpha')
|
->where('date', $date)
|
||||||
->count();
|
->where('status', 'alpha')
|
||||||
|
->count();
|
||||||
|
|
||||||
// Cari existing system-generated violation
|
// Cari existing system-generated violation untuk (siswa, tanggal) yang sama
|
||||||
$existing = \App\Models\Violation::where('student_id', $studentId)
|
$existing = \App\Models\Violation::where('student_id', $studentId)
|
||||||
->where('violation_type_id', $alphaType->id)
|
->where('violation_type_id', $alphaType->id)
|
||||||
->where('violation_date', $alphaDate)
|
->where('violation_date', $date)
|
||||||
->whereNull('recorded_by')
|
->whereNull('recorded_by')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($totalAlpha > 0) {
|
if ($totalAlpha > 0) {
|
||||||
// Ada alpha → buat/update violation
|
// Ada alpha → buat/update violation
|
||||||
$points = max(1, (int) round(($alphaType->points / 10) * $totalAlpha));
|
$points = max(1, (int) round(($alphaType->points / 10) * $totalAlpha));
|
||||||
$desc = "Alpha - Tidak hadir tanpa keterangan ({$totalAlpha} jam pelajaran)";
|
$desc = "Alpha - Tidak hadir tanpa keterangan ({$totalAlpha} jam pelajaran)";
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
$existing->update([
|
$existing->update([
|
||||||
'points' => $points,
|
'points' => $points,
|
||||||
'description' => $desc,
|
'description' => $desc,
|
||||||
'notes' => 'Diperbarui otomatis dari sinkron E-Jurnal.',
|
'notes' => 'Diperbarui otomatis dari sinkron E-Jurnal.',
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$violationService->recordViolation([
|
$violationService->recordViolation([
|
||||||
'student_id' => $studentId,
|
'student_id' => $studentId,
|
||||||
'violation_type_id' => $alphaType->id,
|
'violation_type_id' => $alphaType->id,
|
||||||
'violation_date' => $alphaDate,
|
'violation_date' => $date,
|
||||||
'points' => $points,
|
'points' => $points,
|
||||||
'description' => $desc,
|
'description' => $desc,
|
||||||
'notes' => 'Dibuat otomatis dari sinkron E-Jurnal.',
|
'notes' => 'Dibuat otomatis dari sinkron E-Jurnal.',
|
||||||
'evidences' => [],
|
'evidences' => [],
|
||||||
], null);
|
], null);
|
||||||
|
}
|
||||||
|
$results['violations']++;
|
||||||
|
} elseif ($existing) {
|
||||||
|
// Tidak ada alpha di tanggal ini tapi ada violation system lama → hapus (ganti status ke hadir)
|
||||||
|
$existing->delete();
|
||||||
|
$results['violations']++;
|
||||||
}
|
}
|
||||||
$results['violations']++;
|
} catch (\Exception $e) {
|
||||||
} elseif ($existing) {
|
$results['errors'][] = "Violation sync failed for student={$studentId} date={$date}: " . $e->getMessage();
|
||||||
// Tidak ada alpha tapi ada violation lama → hapus (ganti status ke hadir)
|
|
||||||
$existing->delete();
|
|
||||||
$results['violations']++;
|
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
|
||||||
$results['errors'][] = "Violation sync failed for student={$studentId}: " . $e->getMessage();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -414,6 +414,7 @@ class AttendanceController extends Controller
|
|||||||
$alpha = round($alphaCount / 10, 1);
|
$alpha = round($alphaCount / 10, 1);
|
||||||
$sakit = $studentAtt->where('status', 'sakit')->count() > 0 ? 1 : 0;
|
$sakit = $studentAtt->where('status', 'sakit')->count() > 0 ? 1 : 0;
|
||||||
$izin = $studentAtt->where('status', 'izin')->count() > 0 ? 1 : 0;
|
$izin = $studentAtt->where('status', 'izin')->count() > 0 ? 1 : 0;
|
||||||
|
$pkl = $studentAtt->where('status', 'pkl')->count() > 0 ? 1 : 0;
|
||||||
$terlambat = $studentAtt->where('status', 'terlambat')->count() > 0 ? 1 : 0;
|
$terlambat = $studentAtt->where('status', 'terlambat')->count() > 0 ? 1 : 0;
|
||||||
$total = $studentAtt->count();
|
$total = $studentAtt->count();
|
||||||
|
|
||||||
@@ -423,6 +424,7 @@ class AttendanceController extends Controller
|
|||||||
'alpha' => $alpha,
|
'alpha' => $alpha,
|
||||||
'sakit' => $sakit,
|
'sakit' => $sakit,
|
||||||
'izin' => $izin,
|
'izin' => $izin,
|
||||||
|
'pkl' => $pkl,
|
||||||
'terlambat' => $terlambat,
|
'terlambat' => $terlambat,
|
||||||
'total' => $studentAtt->count(),
|
'total' => $studentAtt->count(),
|
||||||
];
|
];
|
||||||
@@ -433,7 +435,7 @@ class AttendanceController extends Controller
|
|||||||
// Hitung total per siswa selama seminggu
|
// Hitung total per siswa selama seminggu
|
||||||
$totals = [];
|
$totals = [];
|
||||||
foreach ($students as $student) {
|
foreach ($students as $student) {
|
||||||
$totals[$student->id] = ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'terlambat' => 0];
|
$totals[$student->id] = ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'pkl' => 0, 'terlambat' => 0];
|
||||||
}
|
}
|
||||||
foreach ($recap as $row) {
|
foreach ($recap as $row) {
|
||||||
foreach ($row['students'] as $sid => $s) {
|
foreach ($row['students'] as $sid => $s) {
|
||||||
@@ -441,6 +443,7 @@ class AttendanceController extends Controller
|
|||||||
$totals[$sid]['sakit'] += $s['sakit'];
|
$totals[$sid]['sakit'] += $s['sakit'];
|
||||||
$totals[$sid]['izin'] += $s['izin'];
|
$totals[$sid]['izin'] += $s['izin'];
|
||||||
$totals[$sid]['alpha'] += $s['alpha'];
|
$totals[$sid]['alpha'] += $s['alpha'];
|
||||||
|
$totals[$sid]['pkl'] += $s['pkl'];
|
||||||
$totals[$sid]['terlambat'] += $s['terlambat'];
|
$totals[$sid]['terlambat'] += $s['terlambat'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,7 +506,7 @@ class AttendanceController extends Controller
|
|||||||
|
|
||||||
$row = ['label' => $week['label'], 'start' => $week['start'], 'end' => $week['end'], 'students' => []];
|
$row = ['label' => $week['label'], 'start' => $week['start'], 'end' => $week['end'], 'students' => []];
|
||||||
foreach ($students as $student) {
|
foreach ($students as $student) {
|
||||||
$hCount = 0; $sCount = 0; $iCount = 0; $aCount = 0; $tCount = 0; $totalDaysForStudent = 0;
|
$hCount = 0; $sCount = 0; $iCount = 0; $aCount = 0; $pCount = 0; $tCount = 0; $totalDaysForStudent = 0;
|
||||||
|
|
||||||
foreach ($datesInWeek as $dateStr) {
|
foreach ($datesInWeek as $dateStr) {
|
||||||
$dayAtt = $weekByDay->get($dateStr, collect())->where('student_id', $student->id);
|
$dayAtt = $weekByDay->get($dateStr, collect())->where('student_id', $student->id);
|
||||||
@@ -513,12 +516,14 @@ class AttendanceController extends Controller
|
|||||||
$hasHadir = $dayAtt->where('status', 'hadir')->isNotEmpty();
|
$hasHadir = $dayAtt->where('status', 'hadir')->isNotEmpty();
|
||||||
$hasSakit = $dayAtt->where('status', 'sakit')->isNotEmpty();
|
$hasSakit = $dayAtt->where('status', 'sakit')->isNotEmpty();
|
||||||
$hasIzin = $dayAtt->where('status', 'izin')->isNotEmpty();
|
$hasIzin = $dayAtt->where('status', 'izin')->isNotEmpty();
|
||||||
|
$hasPkl = $dayAtt->where('status', 'pkl')->isNotEmpty();
|
||||||
$hasAlpha = $dayAtt->where('status', 'alpha')->isNotEmpty();
|
$hasAlpha = $dayAtt->where('status', 'alpha')->isNotEmpty();
|
||||||
$hasTerlambat = $dayAtt->where('status', 'terlambat')->isNotEmpty();
|
$hasTerlambat = $dayAtt->where('status', 'terlambat')->isNotEmpty();
|
||||||
|
|
||||||
if ($hasHadir) $hCount++;
|
if ($hasHadir) $hCount++;
|
||||||
if ($hasSakit) $sCount++;
|
if ($hasSakit) $sCount++;
|
||||||
if ($hasIzin) $iCount++;
|
if ($hasIzin) $iCount++;
|
||||||
|
if ($hasPkl) $pCount++;
|
||||||
if ($hasTerlambat) $tCount++;
|
if ($hasTerlambat) $tCount++;
|
||||||
|
|
||||||
// Alpha proporsional: jumlah jam alpha / 10 (max jam sekolah per hari)
|
// Alpha proporsional: jumlah jam alpha / 10 (max jam sekolah per hari)
|
||||||
@@ -532,6 +537,7 @@ class AttendanceController extends Controller
|
|||||||
'alpha' => $aCount,
|
'alpha' => $aCount,
|
||||||
'sakit' => $sCount,
|
'sakit' => $sCount,
|
||||||
'izin' => $iCount,
|
'izin' => $iCount,
|
||||||
|
'pkl' => $pCount,
|
||||||
'terlambat' => $tCount,
|
'terlambat' => $tCount,
|
||||||
'total' => $totalDaysForStudent,
|
'total' => $totalDaysForStudent,
|
||||||
];
|
];
|
||||||
@@ -542,7 +548,7 @@ class AttendanceController extends Controller
|
|||||||
// Hitung total per siswa selama sebulan
|
// Hitung total per siswa selama sebulan
|
||||||
$totals = [];
|
$totals = [];
|
||||||
foreach ($students as $student) {
|
foreach ($students as $student) {
|
||||||
$totals[$student->id] = ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'terlambat' => 0];
|
$totals[$student->id] = ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'pkl' => 0, 'terlambat' => 0];
|
||||||
}
|
}
|
||||||
foreach ($recap as $row) {
|
foreach ($recap as $row) {
|
||||||
foreach ($row['students'] as $sid => $s) {
|
foreach ($row['students'] as $sid => $s) {
|
||||||
@@ -550,6 +556,7 @@ class AttendanceController extends Controller
|
|||||||
$totals[$sid]['sakit'] += $s['sakit'];
|
$totals[$sid]['sakit'] += $s['sakit'];
|
||||||
$totals[$sid]['izin'] += $s['izin'];
|
$totals[$sid]['izin'] += $s['izin'];
|
||||||
$totals[$sid]['alpha'] += $s['alpha'];
|
$totals[$sid]['alpha'] += $s['alpha'];
|
||||||
|
$totals[$sid]['pkl'] += $s['pkl'];
|
||||||
$totals[$sid]['terlambat'] += $s['terlambat'];
|
$totals[$sid]['terlambat'] += $s['terlambat'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,7 +623,7 @@ class AttendanceController extends Controller
|
|||||||
$title .= ' (' . \Carbon\Carbon::parse($data['start'])->format('d/m/Y') . ' - ' . \Carbon\Carbon::parse($data['end'])->format('d/m/Y') . ')';
|
$title .= ' (' . \Carbon\Carbon::parse($data['start'])->format('d/m/Y') . ' - ' . \Carbon\Carbon::parse($data['end'])->format('d/m/Y') . ')';
|
||||||
|
|
||||||
$sheet->setCellValue('A1', $title);
|
$sheet->setCellValue('A1', $title);
|
||||||
$totalCols = 1 + $data['recap']->count() * 4 + 4; // +4 untuk kolom TOTAL
|
$totalCols = 1 + $data['recap']->count() * 5 + 5; // +5 untuk kolom TOTAL
|
||||||
$sheet->mergeCells('A1:' . $this->getColLetter($totalCols - 1) . '1');
|
$sheet->mergeCells('A1:' . $this->getColLetter($totalCols - 1) . '1');
|
||||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||||
|
|
||||||
@@ -627,15 +634,15 @@ class AttendanceController extends Controller
|
|||||||
foreach ($data['recap'] as $row) {
|
foreach ($data['recap'] as $row) {
|
||||||
$cell = $this->getColLetter($col) . '2';
|
$cell = $this->getColLetter($col) . '2';
|
||||||
$sheet->setCellValue($cell, $row['label'] . ' ' . \Carbon\Carbon::parse($row['date'])->format('d/m'));
|
$sheet->setCellValue($cell, $row['label'] . ' ' . \Carbon\Carbon::parse($row['date'])->format('d/m'));
|
||||||
$endCell = $this->getColLetter($col + 3) . '2';
|
$endCell = $this->getColLetter($col + 4) . '2';
|
||||||
$sheet->mergeCells($cell . ':' . $endCell);
|
$sheet->mergeCells($cell . ':' . $endCell);
|
||||||
$sheet->getStyle($cell)->getFont()->setBold(true);
|
$sheet->getStyle($cell)->getFont()->setBold(true);
|
||||||
$col += 4;
|
$col += 5;
|
||||||
}
|
}
|
||||||
// Kolom TOTAL
|
// Kolom TOTAL
|
||||||
$totalCell = $this->getColLetter($col) . '2';
|
$totalCell = $this->getColLetter($col) . '2';
|
||||||
$sheet->setCellValue($totalCell, 'TOTAL');
|
$sheet->setCellValue($totalCell, 'TOTAL');
|
||||||
$sheet->mergeCells($totalCell . ':' . $this->getColLetter($col + 3) . '2');
|
$sheet->mergeCells($totalCell . ':' . $this->getColLetter($col + 4) . '2');
|
||||||
$sheet->getStyle($totalCell)->getFont()->setBold(true);
|
$sheet->getStyle($totalCell)->getFont()->setBold(true);
|
||||||
|
|
||||||
// Header row 3: H, S, I, A per day + TOTAL
|
// Header row 3: H, S, I, A per day + TOTAL
|
||||||
@@ -646,16 +653,19 @@ class AttendanceController extends Controller
|
|||||||
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
||||||
foreach (range(0, 3) as $i) {
|
$sheet->setCellValue($this->getColLetter($col + 4) . '3', 'P');
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . '3', 'P');
|
||||||
|
foreach (range(0, 4) as $i) {
|
||||||
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
$col += 4;
|
$col += 5;
|
||||||
}
|
}
|
||||||
// Sub-header TOTAL
|
// Sub-header TOTAL
|
||||||
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
|
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . '3', 'P');
|
||||||
foreach (range(0, 3) as $i) {
|
foreach (range(0, 3) as $i) {
|
||||||
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
@@ -672,19 +682,21 @@ class AttendanceController extends Controller
|
|||||||
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $s['sakit']);
|
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $s['sakit']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $s['izin']);
|
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $s['izin']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $s['alpha']);
|
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $s['alpha']);
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . $rowIdx, $s['pkl']);
|
||||||
} else {
|
} else {
|
||||||
foreach (range(0, 3) as $i) {
|
foreach (range(0, 4) as $i) {
|
||||||
$sheet->setCellValue($this->getColLetter($col + $i) . $rowIdx, '-');
|
$sheet->setCellValue($this->getColLetter($col + $i) . $rowIdx, '-');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$col += 4;
|
$col += 5;
|
||||||
}
|
}
|
||||||
// TOTAL per siswa
|
// TOTAL per siswa
|
||||||
$t = $data['totals'][$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
|
$t = $data['totals'][$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'pkl' => 0];
|
||||||
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $t['hadir']);
|
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $t['hadir']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $t['sakit']);
|
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $t['sakit']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $t['izin']);
|
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $t['izin']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $t['alpha']);
|
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $t['alpha']);
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . $rowIdx, $t['pkl']);
|
||||||
|
|
||||||
$rowIdx++;
|
$rowIdx++;
|
||||||
}
|
}
|
||||||
@@ -728,7 +740,7 @@ class AttendanceController extends Controller
|
|||||||
$title .= ' (' . \Carbon\Carbon::create()->month($data['month'])->format('F') . ' ' . $data['year'] . ')';
|
$title .= ' (' . \Carbon\Carbon::create()->month($data['month'])->format('F') . ' ' . $data['year'] . ')';
|
||||||
|
|
||||||
$sheet->setCellValue('A1', $title);
|
$sheet->setCellValue('A1', $title);
|
||||||
$totalCols = 1 + $data['recap']->count() * 4 + 4; // +4 untuk kolom TOTAL
|
$totalCols = 1 + $data['recap']->count() * 5 + 5; // +5 untuk kolom TOTAL
|
||||||
$sheet->mergeCells('A1:' . $this->getColLetter($totalCols - 1) . '1');
|
$sheet->mergeCells('A1:' . $this->getColLetter($totalCols - 1) . '1');
|
||||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||||
|
|
||||||
@@ -739,15 +751,15 @@ class AttendanceController extends Controller
|
|||||||
foreach ($data['recap'] as $row) {
|
foreach ($data['recap'] as $row) {
|
||||||
$cell = $this->getColLetter($col) . '2';
|
$cell = $this->getColLetter($col) . '2';
|
||||||
$sheet->setCellValue($cell, $row['label'] . ' ' . \Carbon\Carbon::parse($row['start'])->format('d/m') . '-' . \Carbon\Carbon::parse($row['end'])->format('d/m'));
|
$sheet->setCellValue($cell, $row['label'] . ' ' . \Carbon\Carbon::parse($row['start'])->format('d/m') . '-' . \Carbon\Carbon::parse($row['end'])->format('d/m'));
|
||||||
$endCell = $this->getColLetter($col + 3) . '2';
|
$endCell = $this->getColLetter($col + 4) . '2';
|
||||||
$sheet->mergeCells($cell . ':' . $endCell);
|
$sheet->mergeCells($cell . ':' . $endCell);
|
||||||
$sheet->getStyle($cell)->getFont()->setBold(true);
|
$sheet->getStyle($cell)->getFont()->setBold(true);
|
||||||
$col += 4;
|
$col += 5;
|
||||||
}
|
}
|
||||||
// Kolom TOTAL
|
// Kolom TOTAL
|
||||||
$totalCell = $this->getColLetter($col) . '2';
|
$totalCell = $this->getColLetter($col) . '2';
|
||||||
$sheet->setCellValue($totalCell, 'TOTAL');
|
$sheet->setCellValue($totalCell, 'TOTAL');
|
||||||
$sheet->mergeCells($totalCell . ':' . $this->getColLetter($col + 3) . '2');
|
$sheet->mergeCells($totalCell . ':' . $this->getColLetter($col + 4) . '2');
|
||||||
$sheet->getStyle($totalCell)->getFont()->setBold(true);
|
$sheet->getStyle($totalCell)->getFont()->setBold(true);
|
||||||
|
|
||||||
// Header row 3: H, S, I, A per week + TOTAL
|
// Header row 3: H, S, I, A per week + TOTAL
|
||||||
@@ -758,16 +770,19 @@ class AttendanceController extends Controller
|
|||||||
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
||||||
foreach (range(0, 3) as $i) {
|
$sheet->setCellValue($this->getColLetter($col + 4) . '3', 'P');
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . '3', 'P');
|
||||||
|
foreach (range(0, 4) as $i) {
|
||||||
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
$col += 4;
|
$col += 5;
|
||||||
}
|
}
|
||||||
// Sub-header TOTAL
|
// Sub-header TOTAL
|
||||||
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
|
$sheet->setCellValue($this->getColLetter($col) . '3', 'H');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
$sheet->setCellValue($this->getColLetter($col + 1) . '3', 'S');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
$sheet->setCellValue($this->getColLetter($col + 2) . '3', 'I');
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
$sheet->setCellValue($this->getColLetter($col + 3) . '3', 'A');
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . '3', 'P');
|
||||||
foreach (range(0, 3) as $i) {
|
foreach (range(0, 3) as $i) {
|
||||||
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
$sheet->getStyle($this->getColLetter($col + $i) . '3')->getFont()->setBold(true);
|
||||||
}
|
}
|
||||||
@@ -784,19 +799,21 @@ class AttendanceController extends Controller
|
|||||||
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $s['sakit']);
|
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $s['sakit']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $s['izin']);
|
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $s['izin']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $s['alpha']);
|
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $s['alpha']);
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . $rowIdx, $s['pkl']);
|
||||||
} else {
|
} else {
|
||||||
foreach (range(0, 3) as $i) {
|
foreach (range(0, 4) as $i) {
|
||||||
$sheet->setCellValue($this->getColLetter($col + $i) . $rowIdx, '-');
|
$sheet->setCellValue($this->getColLetter($col + $i) . $rowIdx, '-');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$col += 4;
|
$col += 5;
|
||||||
}
|
}
|
||||||
// TOTAL per siswa
|
// TOTAL per siswa
|
||||||
$t = $data['totals'][$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
|
$t = $data['totals'][$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'pkl' => 0];
|
||||||
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $t['hadir']);
|
$sheet->setCellValue($this->getColLetter($col) . $rowIdx, $t['hadir']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $t['sakit']);
|
$sheet->setCellValue($this->getColLetter($col + 1) . $rowIdx, $t['sakit']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $t['izin']);
|
$sheet->setCellValue($this->getColLetter($col + 2) . $rowIdx, $t['izin']);
|
||||||
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $t['alpha']);
|
$sheet->setCellValue($this->getColLetter($col + 3) . $rowIdx, $t['alpha']);
|
||||||
|
$sheet->setCellValue($this->getColLetter($col + 4) . $rowIdx, $t['pkl']);
|
||||||
|
|
||||||
$rowIdx++;
|
$rowIdx++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Models\SpLetter;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Barryvdh\DomPDF\Facade\Pdf;
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class SpLetterController extends Controller
|
class SpLetterController extends Controller
|
||||||
@@ -39,10 +40,56 @@ class SpLetterController extends Controller
|
|||||||
|
|
||||||
$spLetter->update(['printed_at' => now(), 'status' => 'printed']);
|
$spLetter->update(['printed_at' => now(), 'status' => 'printed']);
|
||||||
|
|
||||||
// For now, return an HTML print view
|
// HTML print view (untuk cetak langsung dari browser)
|
||||||
return view('sp-letters.print', compact('spLetter', 'school'));
|
return view('sp-letters.print', compact('spLetter', 'school'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unduh PDF surat peringatan (format F4 resmi).
|
||||||
|
*/
|
||||||
|
public function pdf(SpLetter $spLetter)
|
||||||
|
{
|
||||||
|
abort_unless(auth()->user()->canViewStudent($spLetter->student_id), 403);
|
||||||
|
|
||||||
|
$spLetter->load(['student', 'spThreshold', 'generator']);
|
||||||
|
$school = $this->schoolSettings();
|
||||||
|
$forPdf = true;
|
||||||
|
|
||||||
|
$spLetter->update(['printed_at' => now(), 'status' => 'printed']);
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('sp-letters.print', compact('spLetter', 'school', 'forPdf'))
|
||||||
|
->setPaper([0, 0, 595.28, 935.43], 'portrait'); // F4 (210×330 mm)
|
||||||
|
|
||||||
|
$filename = 'SP-'.Str::slug(str_replace(['/', '\\'], '-', $spLetter->letter_number)).'-'.now()->format('Ymd-His').'.pdf';
|
||||||
|
|
||||||
|
return $pdf->download($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ekspor PDF rekap seluruh surat peringatan (daftar, A4 landscape).
|
||||||
|
*/
|
||||||
|
public function exportPdf()
|
||||||
|
{
|
||||||
|
$letters = SpLetter::with(['student', 'spThreshold', 'generator'])
|
||||||
|
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds())))
|
||||||
|
->latest()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$summary = [
|
||||||
|
'total' => $letters->count(),
|
||||||
|
'total_poin' => $letters->sum('total_points_at_time'),
|
||||||
|
'siswa' => $letters->pluck('student_id')->unique()->count(),
|
||||||
|
'per_status' => $letters->groupBy('status')->map->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$school = $this->schoolSettings();
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('sp-letters.export-pdf', compact('letters', 'summary', 'school'))
|
||||||
|
->setPaper('a4', 'portrait');
|
||||||
|
|
||||||
|
return $pdf->download('rekap-surat-peringatan-'.now()->format('Ymd-His').'.pdf');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data sekolah untuk kop surat (format resmi).
|
* Data sekolah untuk kop surat (format resmi).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 407 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 415 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 493 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
|
||||||
|
<rect width="400" height="400" rx="44" fill="#F4F8FF"/>
|
||||||
|
<circle cx="200" cy="165" r="78" fill="#DBEAFE"/>
|
||||||
|
<circle cx="200" cy="135" r="14" fill="#2563EB"/>
|
||||||
|
<path d="M200 170 v70" stroke="#2563EB" stroke-width="16" stroke-linecap="round"/>
|
||||||
|
<circle cx="200" cy="272" r="14" fill="#2563EB"/>
|
||||||
|
<path d="M120 330 q80 -55 160 0" stroke="#0D9488" stroke-width="14" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="130" cy="120" r="10" fill="#F59E0B" opacity="0.85"/>
|
||||||
|
<circle cx="285" cy="105" r="8" fill="#F59E0B" opacity="0.6"/>
|
||||||
|
<circle cx="300" cy="300" r="9" fill="#0D9488" opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 673 B |
@@ -154,9 +154,11 @@
|
|||||||
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'hadir')" title="Semua Hadir"
|
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'hadir')" title="Semua Hadir"
|
||||||
class="w-8 h-8 rounded-lg text-xs font-bold bg-emerald-100 text-emerald-700 border-2 border-emerald-300 hover:bg-emerald-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">H</button>
|
class="w-8 h-8 rounded-lg text-xs font-bold bg-emerald-100 text-emerald-700 border-2 border-emerald-300 hover:bg-emerald-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">H</button>
|
||||||
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'sakit')" title="Semua Sakit"
|
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'sakit')" title="Semua Sakit"
|
||||||
class="w-8 h-8 rounded-lg text-xs font-bold bg-purple-100 text-purple-700 border-2 border-purple-300 hover:bg-purple-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">S</button>
|
class="w-8 h-8 rounded-lg text-xs font-bold bg-amber-100 text-amber-700 border-2 border-amber-300 hover:bg-amber-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">S</button>
|
||||||
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'izin')" title="Semua Izin"
|
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'izin')" title="Semua Izin"
|
||||||
class="w-8 h-8 rounded-lg text-xs font-bold bg-blue-100 text-blue-700 border-2 border-blue-300 hover:bg-blue-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">I</button>
|
class="w-8 h-8 rounded-lg text-xs font-bold bg-blue-100 text-blue-700 border-2 border-blue-300 hover:bg-blue-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">I</button>
|
||||||
|
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'pkl')" title="Semua PKL"
|
||||||
|
class="w-8 h-8 rounded-lg text-xs font-bold bg-violet-100 text-violet-700 border-2 border-violet-300 hover:bg-violet-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">P</button>
|
||||||
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'alpha')" title="Semua Alpha"
|
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'alpha')" title="Semua Alpha"
|
||||||
class="w-8 h-8 rounded-lg text-xs font-bold bg-red-100 text-red-700 border-2 border-red-300 hover:bg-red-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">A</button>
|
class="w-8 h-8 rounded-lg text-xs font-bold bg-red-100 text-red-700 border-2 border-red-300 hover:bg-red-200 hover:scale-110 active:scale-95 transition-all duration-150 shadow-sm">A</button>
|
||||||
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'terlambat')" title="Semua Terlambat"
|
<button type="button" onclick="studentBulkSet('{{ $s->id }}', 'terlambat')" title="Semua Terlambat"
|
||||||
@@ -172,19 +174,20 @@
|
|||||||
$statusColors = [
|
$statusColors = [
|
||||||
'belum' => 'bg-gray-100 border-gray-300 text-gray-400 hover:bg-gray-200',
|
'belum' => 'bg-gray-100 border-gray-300 text-gray-400 hover:bg-gray-200',
|
||||||
'hadir' => 'bg-emerald-100 border-emerald-300 text-emerald-700 hover:bg-emerald-200',
|
'hadir' => 'bg-emerald-100 border-emerald-300 text-emerald-700 hover:bg-emerald-200',
|
||||||
'sakit' => 'bg-purple-100 border-purple-300 text-purple-700 hover:bg-purple-200',
|
'sakit' => 'bg-amber-100 border-amber-300 text-amber-700 hover:bg-amber-200',
|
||||||
'izin' => 'bg-blue-100 border-blue-300 text-blue-700 hover:bg-blue-200',
|
'izin' => 'bg-blue-100 border-blue-300 text-blue-700 hover:bg-blue-200',
|
||||||
|
'pkl' => 'bg-violet-100 border-violet-300 text-violet-700 hover:bg-violet-200',
|
||||||
'alpha' => 'bg-red-100 border-red-300 text-red-700 hover:bg-red-200',
|
'alpha' => 'bg-red-100 border-red-300 text-red-700 hover:bg-red-200',
|
||||||
'terlambat' => 'bg-yellow-100 border-yellow-300 text-yellow-700 hover:bg-yellow-200',
|
'terlambat' => 'bg-yellow-100 border-yellow-300 text-yellow-700 hover:bg-yellow-200',
|
||||||
];
|
];
|
||||||
$statusLabels = [
|
$statusLabels = [
|
||||||
'belum' => '—',
|
'belum' => '—',
|
||||||
'hadir' => 'H', 'sakit' => 'S', 'izin' => 'I',
|
'hadir' => 'H', 'sakit' => 'S', 'izin' => 'I', 'pkl' => 'P',
|
||||||
'alpha' => 'A', 'terlambat' => 'T',
|
'alpha' => 'A', 'terlambat' => 'T',
|
||||||
];
|
];
|
||||||
$statusFull = [
|
$statusFull = [
|
||||||
'belum' => 'Belum diisi',
|
'belum' => 'Belum diisi',
|
||||||
'hadir' => 'Hadir', 'sakit' => 'Sakit', 'izin' => 'Izin',
|
'hadir' => 'Hadir', 'sakit' => 'Sakit', 'izin' => 'Izin', 'pkl' => 'PKL',
|
||||||
'alpha' => 'Alpha', 'terlambat' => 'Terlambat',
|
'alpha' => 'Alpha', 'terlambat' => 'Terlambat',
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
@@ -224,8 +227,9 @@
|
|||||||
@php
|
@php
|
||||||
$legends = [
|
$legends = [
|
||||||
'hadir' => ['emerald', 'H', 'Hadir'],
|
'hadir' => ['emerald', 'H', 'Hadir'],
|
||||||
'sakit' => ['purple', 'S', 'Sakit'],
|
'sakit' => ['amber', 'S', 'Sakit'],
|
||||||
'izin' => ['blue', 'I', 'Izin'],
|
'izin' => ['blue', 'I', 'Izin'],
|
||||||
|
'pkl' => ['violet', 'P', 'PKL'],
|
||||||
'alpha' => ['red', 'A', 'Alpha'],
|
'alpha' => ['red', 'A', 'Alpha'],
|
||||||
'terlambat' => ['yellow', 'T', 'Terlambat'],
|
'terlambat' => ['yellow', 'T', 'Terlambat'],
|
||||||
];
|
];
|
||||||
@@ -252,7 +256,7 @@
|
|||||||
<div class="w-6 h-6 rounded-lg bg-emerald-100 flex items-center justify-center flex-shrink-0 shadow-sm">
|
<div class="w-6 h-6 rounded-lg bg-emerald-100 flex items-center justify-center flex-shrink-0 shadow-sm">
|
||||||
<i class="fa-solid fa-circle-info text-emerald-500 text-[10px]"></i>
|
<i class="fa-solid fa-circle-info text-emerald-500 text-[10px]"></i>
|
||||||
</div>
|
</div>
|
||||||
<span>Siswa dengan status <strong>Alpha (A)</strong> minimal 1 jam akan otomatis dicatat sebagai pelanggaran. Klik tombol status untuk <strong>rotasi</strong>: H → S → I → A → T → H.</span>
|
<span>Siswa dengan status <strong>Alpha (A)</strong> minimal 1 jam akan otomatis dicatat sebagai pelanggaran. Klik tombol status untuk <strong>rotasi</strong>: H → S → I → P → A → T → H.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,23 +267,24 @@
|
|||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
const STATUS_CYCLE = ['belum', 'hadir', 'sakit', 'izin', 'alpha', 'terlambat'];
|
const STATUS_CYCLE = ['belum', 'hadir', 'sakit', 'izin', 'pkl', 'alpha', 'terlambat'];
|
||||||
const STATUS_COLORS = {
|
const STATUS_COLORS = {
|
||||||
'belum': 'bg-gray-100 border-gray-300 text-gray-400 hover:bg-gray-200',
|
'belum': 'bg-gray-100 border-gray-300 text-gray-400 hover:bg-gray-200',
|
||||||
'hadir': 'bg-emerald-100 border-emerald-300 text-emerald-700 hover:bg-emerald-200',
|
'hadir': 'bg-emerald-100 border-emerald-300 text-emerald-700 hover:bg-emerald-200',
|
||||||
'sakit': 'bg-purple-100 border-purple-300 text-purple-700 hover:bg-purple-200',
|
'sakit': 'bg-amber-100 border-amber-300 text-amber-700 hover:bg-amber-200',
|
||||||
'izin': 'bg-blue-100 border-blue-300 text-blue-700 hover:bg-blue-200',
|
'izin': 'bg-blue-100 border-blue-300 text-blue-700 hover:bg-blue-200',
|
||||||
|
'pkl': 'bg-violet-100 border-violet-300 text-violet-700 hover:bg-violet-200',
|
||||||
'alpha': 'bg-red-100 border-red-300 text-red-700 hover:bg-red-200',
|
'alpha': 'bg-red-100 border-red-300 text-red-700 hover:bg-red-200',
|
||||||
'terlambat': 'bg-yellow-100 border-yellow-300 text-yellow-700 hover:bg-yellow-200',
|
'terlambat': 'bg-yellow-100 border-yellow-300 text-yellow-700 hover:bg-yellow-200',
|
||||||
};
|
};
|
||||||
const STATUS_LABELS = {
|
const STATUS_LABELS = {
|
||||||
'belum': '-',
|
'belum': '-',
|
||||||
'hadir': 'H', 'sakit': 'S', 'izin': 'I',
|
'hadir': 'H', 'sakit': 'S', 'izin': 'I', 'pkl': 'P',
|
||||||
'alpha': 'A', 'terlambat': 'T',
|
'alpha': 'A', 'terlambat': 'T',
|
||||||
};
|
};
|
||||||
const STATUS_FULL = {
|
const STATUS_FULL = {
|
||||||
'belum': 'Belum diisi',
|
'belum': 'Belum diisi',
|
||||||
'hadir': 'Hadir', 'sakit': 'Sakit', 'izin': 'Izin',
|
'hadir': 'Hadir', 'sakit': 'Sakit', 'izin': 'Izin', 'pkl': 'PKL',
|
||||||
'alpha': 'Alpha', 'terlambat': 'Terlambat',
|
'alpha': 'Alpha', 'terlambat': 'Terlambat',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -144,8 +144,9 @@
|
|||||||
$color = match($status) {
|
$color = match($status) {
|
||||||
'hadir' => 'text-emerald-600 bg-emerald-50',
|
'hadir' => 'text-emerald-600 bg-emerald-50',
|
||||||
'alpha' => 'text-red-600 bg-red-50',
|
'alpha' => 'text-red-600 bg-red-50',
|
||||||
'sakit' => 'text-purple-600 bg-purple-50',
|
'sakit' => 'text-amber-600 bg-amber-50',
|
||||||
'izin' => 'text-blue-600 bg-blue-50',
|
'izin' => 'text-blue-600 bg-blue-50',
|
||||||
|
'pkl' => 'text-violet-600 bg-violet-50',
|
||||||
'terlambat' => 'text-yellow-600 bg-yellow-50',
|
'terlambat' => 'text-yellow-600 bg-yellow-50',
|
||||||
default => 'text-gray-300',
|
default => 'text-gray-300',
|
||||||
};
|
};
|
||||||
@@ -154,6 +155,7 @@
|
|||||||
'alpha' => 'A',
|
'alpha' => 'A',
|
||||||
'sakit' => 'S',
|
'sakit' => 'S',
|
||||||
'izin' => 'I',
|
'izin' => 'I',
|
||||||
|
'pkl' => 'P',
|
||||||
'terlambat' => 'T',
|
'terlambat' => 'T',
|
||||||
default => '-',
|
default => '-',
|
||||||
};
|
};
|
||||||
@@ -172,7 +174,8 @@
|
|||||||
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
||||||
<span><span class="inline-block w-7 h-7 rounded-lg bg-emerald-50 text-emerald-600 text-center text-xs font-bold">H</span> Hadir</span>
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-emerald-50 text-emerald-600 text-center text-xs font-bold">H</span> Hadir</span>
|
||||||
<span><span class="inline-block w-7 h-7 rounded-lg bg-red-50 text-red-600 text-center text-xs font-bold">A</span> Alpha</span>
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-red-50 text-red-600 text-center text-xs font-bold">A</span> Alpha</span>
|
||||||
<span><span class="inline-block w-7 h-7 rounded-lg bg-purple-50 text-purple-600 text-center text-xs font-bold">S</span> Sakit</span>
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-amber-50 text-amber-600 text-center text-xs font-bold">S</span> Sakit</span>
|
||||||
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-violet-50 text-violet-600 text-center text-xs font-bold">P</span> PKL</span>
|
||||||
<span><span class="inline-block w-7 h-7 rounded-lg bg-blue-50 text-blue-600 text-center text-xs font-bold">I</span> Izin</span>
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-blue-50 text-blue-600 text-center text-xs font-bold">I</span> Izin</span>
|
||||||
<span><span class="inline-block w-7 h-7 rounded-lg bg-yellow-50 text-yellow-600 text-center text-xs font-bold">T</span> Terlambat</span>
|
<span><span class="inline-block w-7 h-7 rounded-lg bg-yellow-50 text-yellow-600 text-center text-xs font-bold">T</span> Terlambat</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +229,7 @@
|
|||||||
<span class="text-[10px] text-gray-400 font-normal">{{ \Carbon\Carbon::parse($row['date'])->format('d/m') }}</span>
|
<span class="text-[10px] text-gray-400 font-normal">{{ \Carbon\Carbon::parse($row['date'])->format('d/m') }}</span>
|
||||||
</th>
|
</th>
|
||||||
@endforeach
|
@endforeach
|
||||||
<th colspan="4" class="px-3 py-2 text-center text-xs font-semibold text-gray-800 uppercase tracking-wider border-b border-slate-200 border-l-2 border-gray-400 bg-gray-100/80">
|
<th colspan="5" class="px-3 py-2 text-center text-xs font-semibold text-gray-800 uppercase tracking-wider border-b border-slate-200 border-l-2 border-gray-400 bg-gray-100/80">
|
||||||
TOTAL<br>
|
TOTAL<br>
|
||||||
<span class="text-[10px] text-gray-500 font-normal">Hari</span>
|
<span class="text-[10px] text-gray-500 font-normal">Hari</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -237,14 +240,16 @@
|
|||||||
$sep = !$loop->last ? 'border-r-2 border-gray-300' : '';
|
$sep = !$loop->last ? 'border-r-2 border-gray-300' : '';
|
||||||
@endphp
|
@endphp
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-600 uppercase tracking-wider">H</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-600 uppercase tracking-wider">H</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-purple-600 uppercase tracking-wider">S</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-amber-600 uppercase tracking-wider">S</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-600 uppercase tracking-wider">I</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-600 uppercase tracking-wider">I</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-600 uppercase tracking-wider {{ $sep }}">A</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-600 uppercase tracking-wider">A</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-violet-600 uppercase tracking-wider {{ $sep }}">P</th>
|
||||||
@endforeach
|
@endforeach
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-700 uppercase tracking-wider bg-gray-100/80 border-l-2 border-gray-400">H</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-700 uppercase tracking-wider bg-gray-100/80 border-l-2 border-gray-400">H</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-purple-700 uppercase tracking-wider bg-gray-100/80">S</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-amber-700 uppercase tracking-wider bg-gray-100/80">S</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-700 uppercase tracking-wider bg-gray-100/80">I</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-700 uppercase tracking-wider bg-gray-100/80">I</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-700 uppercase tracking-wider bg-gray-100/80">A</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-700 uppercase tracking-wider bg-gray-100/80">A</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-violet-700 uppercase tracking-wider bg-gray-100/80">P</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-50">
|
<tbody class="divide-y divide-gray-50">
|
||||||
@@ -260,24 +265,27 @@
|
|||||||
@endphp
|
@endphp
|
||||||
@if($s)
|
@if($s)
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-600">{{ $s['hadir'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-600">{{ $s['hadir'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-purple-600">{{ $s['sakit'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-amber-600">{{ $s['sakit'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-600">{{ $s['izin'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-600">{{ $s['izin'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['alpha'] > 0 ? 'text-red-600' : 'text-gray-400' }} {{ $sep }}">{{ $s['alpha'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['alpha'] > 0 ? 'text-red-600' : 'text-gray-400' }}">{{ $s['alpha'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['pkl'] > 0 ? 'text-violet-600' : 'text-gray-400' }} {{ $sep }}">{{ $s['pkl'] }}</td>
|
||||||
@else
|
@else
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300 {{ $sep }}">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300 {{ $sep }}">—</td>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
{{-- TOTAL --}}
|
{{-- TOTAL --}}
|
||||||
@php
|
@php
|
||||||
$t = $totals[$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
|
$t = $totals[$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'pkl' => 0];
|
||||||
@endphp
|
@endphp
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-700 bg-gray-50 border-l-2 border-gray-300">{{ $t['hadir'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-700 bg-gray-50 border-l-2 border-gray-300">{{ $t['hadir'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-purple-700 bg-gray-50">{{ $t['sakit'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-amber-700 bg-gray-50">{{ $t['sakit'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-700 bg-gray-50">{{ $t['izin'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-700 bg-gray-50">{{ $t['izin'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['alpha'] > 0 ? 'text-red-700' : 'text-gray-400' }} bg-gray-50">{{ $t['alpha'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['alpha'] > 0 ? 'text-red-700' : 'text-gray-400' }} bg-gray-50">{{ $t['alpha'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['pkl'] > 0 ? 'text-violet-700' : 'text-gray-400' }} bg-gray-50">{{ $t['pkl'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -285,7 +293,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
||||||
<span class="text-emerald-600 font-bold">H</span> Hadir
|
<span class="text-emerald-600 font-bold">H</span> Hadir
|
||||||
<span class="text-purple-600 font-bold">S</span> Sakit
|
<span class="text-amber-600 font-bold">S</span> Sakit
|
||||||
|
<span class="text-violet-600 font-bold">P</span> PKL
|
||||||
<span class="text-blue-600 font-bold">I</span> Izin
|
<span class="text-blue-600 font-bold">I</span> Izin
|
||||||
<span class="text-red-600 font-bold">A</span> Alpha
|
<span class="text-red-600 font-bold">A</span> Alpha
|
||||||
</div>
|
</div>
|
||||||
@@ -339,7 +348,7 @@
|
|||||||
<span class="text-[10px] text-gray-400 font-normal">{{ \Carbon\Carbon::parse($row['start'])->format('d/m') }}-{{ \Carbon\Carbon::parse($row['end'])->format('d/m') }}</span>
|
<span class="text-[10px] text-gray-400 font-normal">{{ \Carbon\Carbon::parse($row['start'])->format('d/m') }}-{{ \Carbon\Carbon::parse($row['end'])->format('d/m') }}</span>
|
||||||
</th>
|
</th>
|
||||||
@endforeach
|
@endforeach
|
||||||
<th colspan="4" class="px-3 py-2 text-center text-xs font-semibold text-gray-800 uppercase tracking-wider border-b border-slate-200 border-l-2 border-gray-400 bg-gray-100/80">
|
<th colspan="5" class="px-3 py-2 text-center text-xs font-semibold text-gray-800 uppercase tracking-wider border-b border-slate-200 border-l-2 border-gray-400 bg-gray-100/80">
|
||||||
TOTAL<br>
|
TOTAL<br>
|
||||||
<span class="text-[10px] text-gray-500 font-normal">Hari</span>
|
<span class="text-[10px] text-gray-500 font-normal">Hari</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -350,14 +359,16 @@
|
|||||||
$sep = !$loop->last ? 'border-r-2 border-gray-300' : '';
|
$sep = !$loop->last ? 'border-r-2 border-gray-300' : '';
|
||||||
@endphp
|
@endphp
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-600 uppercase tracking-wider">H</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-600 uppercase tracking-wider">H</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-purple-600 uppercase tracking-wider">S</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-amber-600 uppercase tracking-wider">S</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-600 uppercase tracking-wider">I</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-600 uppercase tracking-wider">I</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-600 uppercase tracking-wider {{ $sep }}">A</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-600 uppercase tracking-wider">A</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-violet-600 uppercase tracking-wider {{ $sep }}">P</th>
|
||||||
@endforeach
|
@endforeach
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-700 uppercase tracking-wider bg-gray-100/80 border-l-2 border-gray-400">H</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-emerald-700 uppercase tracking-wider bg-gray-100/80 border-l-2 border-gray-400">H</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-purple-700 uppercase tracking-wider bg-gray-100/80">S</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-amber-700 uppercase tracking-wider bg-gray-100/80">S</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-700 uppercase tracking-wider bg-gray-100/80">I</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-blue-700 uppercase tracking-wider bg-gray-100/80">I</th>
|
||||||
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-700 uppercase tracking-wider bg-gray-100/80">A</th>
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-red-700 uppercase tracking-wider bg-gray-100/80">A</th>
|
||||||
|
<th class="px-2 py-1.5 text-center text-[10px] font-bold text-violet-700 uppercase tracking-wider bg-gray-100/80">P</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-50">
|
<tbody class="divide-y divide-gray-50">
|
||||||
@@ -373,24 +384,27 @@
|
|||||||
@endphp
|
@endphp
|
||||||
@if($s)
|
@if($s)
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-600">{{ $s['hadir'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-600">{{ $s['hadir'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-purple-600">{{ $s['sakit'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-amber-600">{{ $s['sakit'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-600">{{ $s['izin'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-600">{{ $s['izin'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['alpha'] > 0 ? 'text-red-600' : 'text-gray-400' }} {{ $sep }}">{{ $s['alpha'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['alpha'] > 0 ? 'text-red-600' : 'text-gray-400' }}">{{ $s['alpha'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $s['pkl'] > 0 ? 'text-violet-600' : 'text-gray-400' }} {{ $sep }}">{{ $s['pkl'] }}</td>
|
||||||
@else
|
@else
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300">—</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs text-gray-300 {{ $sep }}">—</td>
|
<td class="px-2 py-2.5 text-center text-xs text-gray-300 {{ $sep }}">—</td>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
{{-- TOTAL --}}
|
{{-- TOTAL --}}
|
||||||
@php
|
@php
|
||||||
$t = $totals[$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0];
|
$t = $totals[$student->id] ?? ['hadir' => 0, 'sakit' => 0, 'izin' => 0, 'alpha' => 0, 'pkl' => 0];
|
||||||
@endphp
|
@endphp
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-700 bg-gray-50 border-l-2 border-gray-300">{{ $t['hadir'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-emerald-700 bg-gray-50 border-l-2 border-gray-300">{{ $t['hadir'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-purple-700 bg-gray-50">{{ $t['sakit'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-amber-700 bg-gray-50">{{ $t['sakit'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-700 bg-gray-50">{{ $t['izin'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold text-blue-700 bg-gray-50">{{ $t['izin'] }}</td>
|
||||||
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['alpha'] > 0 ? 'text-red-700' : 'text-gray-400' }} bg-gray-50">{{ $t['alpha'] }}</td>
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['alpha'] > 0 ? 'text-red-700' : 'text-gray-400' }} bg-gray-50">{{ $t['alpha'] }}</td>
|
||||||
|
<td class="px-2 py-2.5 text-center text-xs font-bold {{ $t['pkl'] > 0 ? 'text-violet-700' : 'text-gray-400' }} bg-gray-50">{{ $t['pkl'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -398,7 +412,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
<div class="px-5 py-3 border-t border-gray-100 bg-gray-50/50 text-xs text-gray-400 text-center flex items-center justify-center gap-4">
|
||||||
<span class="text-emerald-600 font-bold">H</span> Hadir
|
<span class="text-emerald-600 font-bold">H</span> Hadir
|
||||||
<span class="text-purple-600 font-bold">S</span> Sakit
|
<span class="text-amber-600 font-bold">S</span> Sakit
|
||||||
|
<span class="text-violet-600 font-bold">P</span> PKL
|
||||||
<span class="text-blue-600 font-bold">I</span> Izin
|
<span class="text-blue-600 font-bold">I</span> Izin
|
||||||
<span class="text-red-600 font-bold">A</span> Alpha
|
<span class="text-red-600 font-bold">A</span> Alpha
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
@include('errors._error', [
|
@include('errors.layout', [
|
||||||
'code' => '403',
|
'code' => '403',
|
||||||
'title' => 'Akses Ditolak',
|
'title' => 'Akses Ditolak',
|
||||||
'message' => 'Anda tidak memiliki izin untuk mengakses halaman ini. Jika menurut Anda ini keliru, silakan hubungi administrator.',
|
'message' => 'Kamu tidak memiliki izin untuk membuka halaman ini. Jika menurutmu ini sebuah kesalahan, silakan hubungi administrator.',
|
||||||
'icon' => 'fa-lock',
|
'image' => 'error-403.png',
|
||||||
'iconFrom' => '#ef4444', 'iconTo' => '#dc2626', 'iconShadow' => 'rgba(239, 68, 68, 0.35)',
|
|
||||||
'codeFrom' => '#f87171', 'codeTo' => '#dc2626',
|
|
||||||
'ctaFrom' => '#ef4444', 'ctaTo' => '#dc2626', 'ctaShadow' => 'rgba(239, 68, 68, 0.35)',
|
|
||||||
'showContact' => true,
|
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
@include('errors._error', [
|
@include('errors.layout', [
|
||||||
'code' => '404',
|
'code' => '404',
|
||||||
'title' => 'Halaman Tidak Ditemukan',
|
'title' => 'Halaman Tidak Ditemukan',
|
||||||
'message' => 'Halaman yang Anda cari mungkin telah dipindah, dihapus, atau alamatnya salah. Coba periksa kembali atau kembali ke beranda.',
|
'message' => 'Halaman yang kamu cari sepertinya telah dipindahkan, dihapus, atau alamatnya salah. Coba periksa kembali alamatnya ya.',
|
||||||
'icon' => 'fa-map',
|
'image' => 'error-404.png',
|
||||||
'iconFrom' => '#f59e0b', 'iconTo' => '#f97316', 'iconShadow' => 'rgba(245, 158, 11, 0.35)',
|
|
||||||
'codeFrom' => '#fbbf24', 'codeTo' => '#f97316',
|
|
||||||
'ctaFrom' => '#f59e0b', 'ctaTo' => '#f97316', 'ctaShadow' => 'rgba(249, 115, 22, 0.35)',
|
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
@include('errors._error', [
|
@include('errors.layout', [
|
||||||
'code' => '419',
|
'code' => '419',
|
||||||
'title' => 'Sesi Berakhir',
|
'title' => 'Sesi Berakhir',
|
||||||
'message' => 'Sesi Anda telah berakhir atau tidak valid. Silakan muat ulang halaman dan coba lagi.',
|
'message' => 'Sesi login kamu sudah kedaluwarsa. Muat ulang halaman ini lalu masuk kembali untuk melanjutkan aktivitas.',
|
||||||
'icon' => 'fa-hourglass-half',
|
'image' => 'error-419.png',
|
||||||
'iconFrom' => '#f59e0b', 'iconTo' => '#d97706', 'iconShadow' => 'rgba(245, 158, 11, 0.35)',
|
|
||||||
'codeFrom' => '#fbbf24', 'codeTo' => '#d97706',
|
|
||||||
'ctaFrom' => '#f59e0b', 'ctaTo' => '#d97706', 'ctaShadow' => 'rgba(245, 158, 11, 0.35)',
|
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@include('errors.layout', [
|
||||||
|
'code' => '429',
|
||||||
|
'title' => 'Terlalu Banyak Permintaan',
|
||||||
|
'message' => 'Kamu mengirim terlalu banyak permintaan dalam waktu singkat. Tunggu beberapa saat, lalu coba lagi.',
|
||||||
|
'image' => 'error-429.png',
|
||||||
|
])
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
@include('errors._error', [
|
@include('errors.layout', [
|
||||||
'code' => '500',
|
'code' => '500',
|
||||||
'title' => 'Terjadi Kesalahan',
|
'title' => 'Terjadi Kesalahan Server',
|
||||||
'message' => 'Maaf, terjadi kendala pada server kami. Silakan muat ulang halaman atau coba lagi beberapa saat. Jika terus berlanjut, hubungi administrator.',
|
'message' => 'Ups, terjadi kendala di server kami. Tim kami sudah mendapat laporan dan akan segera memperbaikinya. Coba lagi dalam beberapa saat.',
|
||||||
'icon' => 'fa-triangle-exclamation',
|
'image' => 'error-500.png',
|
||||||
'iconFrom' => '#8b5cf6', 'iconTo' => '#7c3aed', 'iconShadow' => 'rgba(139, 92, 246, 0.35)',
|
|
||||||
'codeFrom' => '#a78bfa', 'codeTo' => '#7c3aed',
|
|
||||||
'ctaFrom' => '#8b5cf6', 'ctaTo' => '#7c3aed', 'ctaShadow' => 'rgba(139, 92, 246, 0.35)',
|
|
||||||
'showContact' => true,
|
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
@include('errors._error', [
|
@include('errors.layout', [
|
||||||
'code' => '503',
|
'code' => '503',
|
||||||
'title' => 'Sedang Pemeliharaan',
|
'title' => 'Sedang Dalam Perbaikan',
|
||||||
'message' => 'Sistem sedang dalam pemeliharaan. Silakan kembali beberapa saat lagi. Terima kasih atas kesabaran Anda.',
|
'message' => 'Sistem sedang dalam perawatan sebentar. Tenang, data ketertiban dan poin kamu tetap aman. Silakan kembali beberapa saat lagi.',
|
||||||
'icon' => 'fa-screwdriver-wrench',
|
'image' => 'error-503.png',
|
||||||
'iconFrom' => '#0ea5e9', 'iconTo' => '#0284c7', 'iconShadow' => 'rgba(14, 165, 233, 0.35)',
|
|
||||||
'codeFrom' => '#38bdf8', 'codeTo' => '#0284c7',
|
|
||||||
'ctaFrom' => '#0ea5e9', 'ctaTo' => '#0284c7', 'ctaShadow' => 'rgba(14, 165, 233, 0.35)',
|
|
||||||
'showBack' => false,
|
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
{{-- Template halaman error premium v2 (standalone, CSS murni — tanpa CDN) --}}
|
|
||||||
@php
|
|
||||||
$appName = \App\Models\Setting::getValue('app_name', 'Aplikasi Ketertiban');
|
|
||||||
$schoolName = \App\Models\Setting::getValue('school_name', 'SMKN 1 Wonorejo');
|
|
||||||
@endphp
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
||||||
<title>{{ $code }} - {{ $title }} | {{ $appName }}</title>
|
|
||||||
<link rel="stylesheet" href="/vendor/font-awesome/css/all.min.css">
|
|
||||||
<link rel="icon" href="/favicon.ico">
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
html, body { height: 100%; }
|
|
||||||
body {
|
|
||||||
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
||||||
padding: 20px; position: relative; overflow: hidden;
|
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
||||||
background:
|
|
||||||
radial-gradient(1200px 600px at 80% -10%, {{ $ctaFrom }}22, transparent 60%),
|
|
||||||
radial-gradient(900px 500px at -10% 110%, {{ $ctaTo }}1f, transparent 60%),
|
|
||||||
linear-gradient(135deg, #f8fafc 0%, #eef4ff 50%, #f4f8ff 100%);
|
|
||||||
}
|
|
||||||
{{-- Pola grid halus --}}
|
|
||||||
.bg-grid {
|
|
||||||
position: absolute; inset: 0; pointer-events: none; opacity: .5;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(100,116,139,.06) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(100,116,139,.06) 1px, transparent 1px);
|
|
||||||
background-size: 44px 44px;
|
|
||||||
mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
|
|
||||||
-webkit-mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
|
|
||||||
}
|
|
||||||
{{-- Orb mengambang --}}
|
|
||||||
.orb { position: absolute; border-radius: 9999px; filter: blur(70px); pointer-events: none; animation: drift 14s ease-in-out infinite; }
|
|
||||||
.orb-1 { width: 420px; height: 420px; top: -120px; right: -100px; background: {{ $ctaFrom }}38; }
|
|
||||||
.orb-2 { width: 480px; height: 480px; bottom: -160px; left: -120px; background: {{ $ctaTo }}33; animation-delay: -5s; }
|
|
||||||
.orb-3 { width: 220px; height: 220px; top: 40%; left: 55%; background: {{ $codeFrom }}26; animation-delay: -9s; }
|
|
||||||
@keyframes drift {
|
|
||||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
||||||
50% { transform: translate(30px, -30px) scale(1.08); }
|
|
||||||
}
|
|
||||||
{{-- Titik dekoratif --}}
|
|
||||||
.dots {
|
|
||||||
position: absolute; inset: 0; pointer-events: none; opacity: .35;
|
|
||||||
background-image: radial-gradient(rgba(148,163,184,.35) 1.2px, transparent 1.2px);
|
|
||||||
background-size: 26px 26px;
|
|
||||||
mask-image: radial-gradient(ellipse at 50% 40%, #000 10%, transparent 70%);
|
|
||||||
-webkit-mask-image: radial-gradient(ellipse at 50% 40%, #000 10%, transparent 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
{{-- Kartu glass + border gradien berputar --}}
|
|
||||||
.card {
|
|
||||||
position: relative; z-index: 10; width: 100%; max-width: 500px;
|
|
||||||
background: rgba(255,255,255,.72); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
|
|
||||||
border-radius: 30px; padding: 44px 36px; text-align: center;
|
|
||||||
box-shadow: 0 30px 70px -20px rgba(15,23,42,.25), 0 0 0 1px rgba(255,255,255,.6) inset;
|
|
||||||
animation: fadeUp .55s cubic-bezier(.22,.9,.3,1.2) both;
|
|
||||||
}
|
|
||||||
.card::before {
|
|
||||||
content: ""; position: absolute; inset: -2px; border-radius: 32px; z-index: -1;
|
|
||||||
background: conic-gradient(from var(--angle, 0deg),
|
|
||||||
transparent 0%, {{ $ctaTo }} 12%, transparent 26%,
|
|
||||||
transparent 50%, {{ $ctaFrom }} 62%, transparent 78%);
|
|
||||||
animation: spin 7s linear infinite;
|
|
||||||
}
|
|
||||||
@property --angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
|
|
||||||
@keyframes spin { to { --angle: 360deg; } }
|
|
||||||
@keyframes fadeUp { from { opacity: 0; transform: translateY(22px) scale(.98); } to { opacity: 1; transform: none; } }
|
|
||||||
|
|
||||||
.icon-box {
|
|
||||||
width: 92px; height: 92px; margin: 0 auto 26px; position: relative;
|
|
||||||
border-radius: 26px; display: flex; align-items: center; justify-content: center;
|
|
||||||
background: linear-gradient(135deg, {{ $iconFrom }}, {{ $iconTo }});
|
|
||||||
box-shadow: 0 16px 34px {{ $iconShadow }}, 0 0 0 6px rgba(255,255,255,.7);
|
|
||||||
animation: floaty 3.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.icon-box::after {
|
|
||||||
content: ""; position: absolute; inset: -14px; border-radius: 34px;
|
|
||||||
background: linear-gradient(135deg, {{ $iconFrom }}33, transparent 60%);
|
|
||||||
filter: blur(8px); z-index: -1;
|
|
||||||
}
|
|
||||||
.icon-box i { font-size: 34px; color: #fff; filter: drop-shadow(0 3px 6px rgba(0,0,0,.18)); }
|
|
||||||
@keyframes floaty { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
|
||||||
|
|
||||||
{{-- Angka besar berlapis --}}
|
|
||||||
.code-wrap { position: relative; margin-bottom: 14px; }
|
|
||||||
.code {
|
|
||||||
position: relative; z-index: 2; font-size: 108px; font-weight: 900; letter-spacing: -4px; line-height: 1;
|
|
||||||
background: linear-gradient(135deg, {{ $codeFrom }}, {{ $codeTo }});
|
|
||||||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
||||||
filter: drop-shadow(0 10px 24px {{ $iconShadow }});
|
|
||||||
}
|
|
||||||
.code-ghost {
|
|
||||||
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
||||||
font-size: 190px; font-weight: 900; letter-spacing: -6px; line-height: 1;
|
|
||||||
color: transparent; -webkit-text-stroke: 1.5px {{ $codeFrom }}1f;
|
|
||||||
pointer-events: none; user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 { font-size: 21px; font-weight: 800; color: #0f172a; margin-bottom: 10px; }
|
|
||||||
.msg { font-size: 14px; color: #64748b; line-height: 1.7; margin-bottom: 34px; }
|
|
||||||
|
|
||||||
.actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
|
||||||
.btn {
|
|
||||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
||||||
padding: 13px 22px; font-size: 14px; font-weight: 700; border-radius: 16px;
|
|
||||||
text-decoration: none; border: 2px solid transparent; cursor: pointer;
|
|
||||||
transition: transform .15s ease, box-shadow .15s ease, background .15s ease, border-color .15s ease;
|
|
||||||
animation: fadeUp .55s cubic-bezier(.22,.9,.3,1.2) both;
|
|
||||||
}
|
|
||||||
.btn:hover { transform: translateY(-2px); }
|
|
||||||
.btn:active { transform: translateY(0) scale(.98); }
|
|
||||||
.btn-ghost { background: rgba(255,255,255,.9); color: #334155; border-color: #e2e8f0; animation-delay: .08s; }
|
|
||||||
.btn-ghost:hover { background: #fff; border-color: #cbd5e1; }
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, {{ $ctaFrom }}, {{ $ctaTo }}); color: #fff;
|
|
||||||
box-shadow: 0 12px 28px {{ $ctaShadow }};
|
|
||||||
}
|
|
||||||
.btn-primary:hover { filter: brightness(1.06); box-shadow: 0 16px 34px {{ $ctaShadow }}; }
|
|
||||||
.btn-contact { background: #eff6ff; color: #2563eb; border-color: #dbeafe; animation-delay: .16s; }
|
|
||||||
.btn-contact:hover { background: #dbeafe; }
|
|
||||||
|
|
||||||
.footer { text-align: center; margin-top: 26px; font-size: 12px; color: #94a3b8; animation: fadeUp .55s both .22s; }
|
|
||||||
.footer i { margin-right: 5px; color: #cbd5e1; }
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.card { padding: 34px 22px; }
|
|
||||||
.code { font-size: 84px; letter-spacing: -3px; }
|
|
||||||
.code-ghost { font-size: 140px; }
|
|
||||||
.actions { flex-direction: column; }
|
|
||||||
.btn { width: 100%; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="bg-grid"></div>
|
|
||||||
<div class="orb orb-1"></div>
|
|
||||||
<div class="orb orb-2"></div>
|
|
||||||
<div class="orb orb-3"></div>
|
|
||||||
<div class="dots"></div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="icon-box"><i class="fa-solid {{ $icon }}"></i></div>
|
|
||||||
<div class="code-wrap">
|
|
||||||
<div class="code-ghost">{{ $code }}</div>
|
|
||||||
<div class="code">{{ $code }}</div>
|
|
||||||
</div>
|
|
||||||
<h1>{{ $title }}</h1>
|
|
||||||
<p class="msg">{{ $message }}</p>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
@if($showBack ?? true)
|
|
||||||
<button onclick="history.length > 1 ? history.back() : window.location.href='{{ url('/') }}'" class="btn btn-ghost">
|
|
||||||
<i class="fa-solid fa-arrow-left" style="font-size:12px"></i> Kembali
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
<a href="{{ url('/') }}" class="btn btn-primary">
|
|
||||||
<i class="fa-solid fa-house" style="font-size:12px"></i> Ke Beranda
|
|
||||||
</a>
|
|
||||||
@if($showContact ?? false)
|
|
||||||
<a href="mailto:admin@smkn1-wonorejo.sch.id" class="btn btn-contact">
|
|
||||||
<i class="fa-solid fa-headset" style="font-size:12px"></i> Hubungi Admin
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="footer"><i class="fa-solid fa-shield-halved"></i>{{ $appName }} • {{ $schoolName }}</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
@php
|
||||||
|
$appName = \App\Models\Setting::getValue('app_name', config('app.name', 'Aplikasi Ketertiban'));
|
||||||
|
$schoolName = \App\Models\Setting::getValue('school_name', 'SMKN 1 Wonorejo');
|
||||||
|
@endphp
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
<meta name="theme-color" content="#2563eb">
|
||||||
|
<title>{{ $code }} — {{ $appName }}</title>
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height:100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background:#ffffff; color:#1e293b;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
min-height:100vh; padding:32px 20px; position:relative; overflow-x:hidden;
|
||||||
|
}
|
||||||
|
/* dekorasi background biru sangat muda */
|
||||||
|
body::before, body::after {
|
||||||
|
content:''; position:fixed; border-radius:50%; z-index:0; pointer-events:none;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
width:460px; height:460px; background:rgba(219,234,254,.55);
|
||||||
|
top:-140px; right:-120px; filter:blur(20px);
|
||||||
|
}
|
||||||
|
body::after {
|
||||||
|
width:400px; height:400px; background:rgba(204,251,241,.5);
|
||||||
|
bottom:-130px; left:-100px; filter:blur(20px);
|
||||||
|
}
|
||||||
|
.page { width:100%; max-width:1040px; position:relative; z-index:1; }
|
||||||
|
.card {
|
||||||
|
display:grid; grid-template-columns:45fr 55fr; gap:40px; align-items:center;
|
||||||
|
background:#fff; border-radius:28px; padding:56px 52px;
|
||||||
|
box-shadow:0 30px 70px -20px rgba(30,58,138,.18);
|
||||||
|
border:1px solid #eef2f7;
|
||||||
|
}
|
||||||
|
/* ===== Kolom kiri: teks ===== */
|
||||||
|
.text-side { text-align:left; }
|
||||||
|
.code {
|
||||||
|
font-size:116px; font-weight:900; line-height:1; letter-spacing:1px;
|
||||||
|
color:#1e3a8a; font-variant-numeric:tabular-nums;
|
||||||
|
}
|
||||||
|
h1 { font-size:27px; font-weight:800; color:#0f172a; margin-top:6px; }
|
||||||
|
.divider {
|
||||||
|
width:56px; height:5px; border-radius:99px; background:#2563eb; margin:18px 0 16px;
|
||||||
|
}
|
||||||
|
p.desc { font-size:15.5px; line-height:1.75; color:#334155; max-width:400px; }
|
||||||
|
.actions { margin-top:30px; display:flex; flex-direction:column; align-items:flex-start; gap:16px; }
|
||||||
|
.btn-primary {
|
||||||
|
display:inline-flex; align-items:center; gap:10px; padding:14px 28px; border-radius:12px;
|
||||||
|
background:#2563eb; color:#fff; font-size:14.5px; font-weight:700; text-decoration:none;
|
||||||
|
box-shadow:0 10px 24px -8px rgba(37,99,235,.55); transition:.2s; border:0; cursor:pointer; font-family:inherit;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 14px 28px -8px rgba(37,99,235,.6); }
|
||||||
|
.btn-link {
|
||||||
|
display:inline-flex; align-items:center; gap:8px; padding:6px 2px;
|
||||||
|
background:none; border:0; color:#1d4ed8; font-size:14px; font-weight:700; cursor:pointer;
|
||||||
|
text-decoration:none; font-family:inherit; transition:.2s;
|
||||||
|
}
|
||||||
|
.btn-link:hover { opacity:.75; }
|
||||||
|
/* ===== Kolom kanan: ilustrasi ===== */
|
||||||
|
.art-side { display:flex; justify-content:center; align-items:center; }
|
||||||
|
.art {
|
||||||
|
width:100%; max-width:460px; height:auto; object-fit:contain; display:block;
|
||||||
|
}
|
||||||
|
/* ===== Info box footer ===== */
|
||||||
|
.infobox {
|
||||||
|
margin-top:22px; display:flex; align-items:center; gap:14px;
|
||||||
|
background:#fff; border:1px solid #e2e8f0; border-radius:16px;
|
||||||
|
padding:15px 22px; box-shadow:0 10px 30px -18px rgba(30,58,138,.25);
|
||||||
|
}
|
||||||
|
.ico-info {
|
||||||
|
flex:0 0 auto; width:28px; height:28px; border-radius:50%;
|
||||||
|
background:#dbeafe; color:#2563eb; font-weight:800; font-size:15px;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
}
|
||||||
|
.info-text { flex:1; font-size:13.5px; color:#334155; line-height:1.6; }
|
||||||
|
.ico-lock { flex:0 0 auto; color:#93c5fd; display:flex; }
|
||||||
|
/* ===== Responsif ===== */
|
||||||
|
@media (max-width:820px){
|
||||||
|
body { padding:24px 16px; }
|
||||||
|
.card { grid-template-columns:1fr; gap:18px; padding:30px 24px 26px; border-radius:22px; text-align:center; }
|
||||||
|
.text-side { text-align:center; display:flex; flex-direction:column; align-items:center; }
|
||||||
|
.actions { align-items:center; margin-top:24px; }
|
||||||
|
.divider { margin-left:auto; margin-right:auto; }
|
||||||
|
.code { font-size:84px; }
|
||||||
|
h1 { font-size:22px; }
|
||||||
|
p.desc { max-width:100%; }
|
||||||
|
.art { max-width:280px; }
|
||||||
|
.infobox { padding:13px 16px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="card">
|
||||||
|
<div class="text-side">
|
||||||
|
<div class="code">{{ $code }}</div>
|
||||||
|
<h1>{{ $title }}</h1>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<p class="desc">{{ $message }}</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn-primary" href="{{ url('/') }}">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>
|
||||||
|
Kembali ke Beranda
|
||||||
|
</a>
|
||||||
|
<button class="btn-link" type="button" onclick="history.length > 1 ? history.back() : location.href='/'">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg>
|
||||||
|
Kembali
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="art-side">
|
||||||
|
<img class="art" src="{{ asset('errors/' . $image) }}" alt="{{ $title }}" onerror="this.onerror=null;this.src='{{ asset('errors/placeholder.svg') }}'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="infobox">
|
||||||
|
<span class="ico-info">i</span>
|
||||||
|
<span class="info-text">Butuh bantuan? Hubungi administrator {{ $schoolName }} untuk informasi lebih lanjut.</span>
|
||||||
|
<span class="ico-lock">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Rekap Surat Peringatan — {{ $school['name'] }}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: 'DejaVu Sans', sans-serif; font-size: 9px; color: #1a1a1a; margin: 0; padding: 0; }
|
||||||
|
.kop { width: 100%; border-collapse: collapse; margin-bottom: 0; }
|
||||||
|
.kop td { border: none; padding: 0; }
|
||||||
|
.kop-logo { width: 118px; text-align: center; vertical-align: middle; }
|
||||||
|
.kop-logo img { height: 88px; width: auto; max-width: 106px; object-fit: contain; }
|
||||||
|
.kop-text { text-align: center; vertical-align: middle; }
|
||||||
|
.kop-1, .kop-2 { font-size: 12px; font-weight: bold; color: #000; }
|
||||||
|
.kop-3 { font-size: 14.5px; font-weight: bold; color: #000; margin: 2px 0; }
|
||||||
|
.kop-4, .kop-5, .kop-6 { font-size: 8.5px; color: #000; }
|
||||||
|
.kop-line { border-bottom: 2.5px solid #000; margin: 8px 0 14px; }
|
||||||
|
.judul { text-align: center; margin: 12px 0 4px; }
|
||||||
|
.judul h2 { font-size: 13px; margin: 0; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
.judul p { font-size: 9px; margin: 3px 0 0; color: #444; }
|
||||||
|
.meta { width: 100%; border-collapse: collapse; margin: 10px 0 6px; }
|
||||||
|
.meta td { padding: 2px 0; }
|
||||||
|
.summary { width: 100%; border-collapse: collapse; margin: 8px 0 14px; }
|
||||||
|
.summary td { border: 1px solid #cbd5e1; padding: 7px 8px; text-align: center; background: #f8fafc; }
|
||||||
|
.summary .angka { font-size: 16px; font-weight: bold; color: #1e3a8a; display: block; }
|
||||||
|
.summary .label { font-size: 8px; color: #555; text-transform: uppercase; }
|
||||||
|
table.detail { width: 100%; border-collapse: collapse; margin: 4px 0 14px; }
|
||||||
|
table.detail th { background: #1e3a8a; color: #fff; padding: 4px 5px; text-align: left; font-size: 8px; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||||
|
table.detail td { border: 1px solid #cbd5e1; padding: 4px 5px; font-size: 8px; }
|
||||||
|
table.detail td .sub { font-size: 7px; color: #64748b; display: block; margin-top: 1px; }
|
||||||
|
table.detail tr:nth-child(even) td { background: #f8fafc; }
|
||||||
|
.center { text-align: center; }
|
||||||
|
.right { text-align: right; }
|
||||||
|
.ttd { width: 100%; margin-top: 30px; }
|
||||||
|
.ttd td { width: 50%; text-align: center; font-size: 9px; vertical-align: top; }
|
||||||
|
.ttd .nama { margin-top: 70px; font-weight: bold; text-decoration: underline; }
|
||||||
|
.footer { margin-top: 16px; text-align: center; font-size: 7.5px; color: #888; border-top: 1px solid #e2e8f0; padding-top: 6px; }
|
||||||
|
.badge { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 7.5px; font-weight: bold; }
|
||||||
|
.badge-draft { background: #fef3c7; color: #92400e; }
|
||||||
|
.badge-printed { background: #dbeafe; color: #1e40af; }
|
||||||
|
.badge-signed { background: #dcfce7; color: #166534; }
|
||||||
|
.badge-delivered { background: #ede9fe; color: #6b21a8; }
|
||||||
|
.badge-other { background: #e2e8f0; color: #334155; }
|
||||||
|
.poin { font-weight: bold; color: #b91c1c; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{-- Kop Surat (format resmi: logo kiri + teks tengah + garis tebal) --}}
|
||||||
|
<table class="kop">
|
||||||
|
<tr>
|
||||||
|
<td class="kop-logo">
|
||||||
|
@if (! empty($school['kop_logo']) && file_exists(public_path('storage/'.$school['kop_logo'])))
|
||||||
|
<img src="{{ public_path('storage/'.$school['kop_logo']) }}" alt="logo">
|
||||||
|
@elseif (! empty($school['logo']) && file_exists(public_path('storage/'.$school['logo'])))
|
||||||
|
<img src="{{ public_path('storage/'.$school['logo']) }}" alt="logo">
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="kop-text">
|
||||||
|
<div class="kop-1">{{ $school['government'] }}</div>
|
||||||
|
<div class="kop-2">{{ $school['agency'] }}</div>
|
||||||
|
<div class="kop-3">{{ $school['full_name'] }}</div>
|
||||||
|
<div class="kop-4">{{ $school['address_detail'] }}</div>
|
||||||
|
<div class="kop-5">{{ $school['website_email'] }}</div>
|
||||||
|
<div class="kop-6">{{ $school['postal'] }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="kop-line"></div>
|
||||||
|
|
||||||
|
{{-- Judul --}}
|
||||||
|
<div class="judul">
|
||||||
|
<h2>Rekap Surat Peringatan Siswa</h2>
|
||||||
|
<p>Dicetak {{ now()->locale('id')->isoFormat('dddd, D MMMM YYYY') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Ringkasan --}}
|
||||||
|
<table class="summary">
|
||||||
|
<tr>
|
||||||
|
<td><span class="angka">{{ $summary['total'] }}</span><span class="label">Total Surat</span></td>
|
||||||
|
<td><span class="angka">{{ $summary['total_poin'] }}</span><span class="label">Total Poin</span></td>
|
||||||
|
<td><span class="angka">{{ $summary['siswa'] }}</span><span class="label">Siswa Terkena SP</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="angka" style="font-size:12px; color:#92400e;">
|
||||||
|
{{ $summary['per_status']->get('draft', 0) }} / {{ $summary['per_status']->get('printed', 0) }}
|
||||||
|
</span>
|
||||||
|
<span class="label">Draft / Tercetak</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="angka" style="font-size:12px; color:#166534;">
|
||||||
|
{{ $summary['per_status']->get('signed', 0) }} / {{ $summary['per_status']->get('delivered', 0) }}
|
||||||
|
</span>
|
||||||
|
<span class="label">TTD / Diserahkan</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Rincian --}}
|
||||||
|
<table class="detail">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:22px">No</th>
|
||||||
|
<th>No. Surat</th>
|
||||||
|
<th>Nama Siswa</th>
|
||||||
|
<th style="width:46px">Kelas</th>
|
||||||
|
<th style="width:62px">Jenis SP</th>
|
||||||
|
<th class="center" style="width:30px">Poin</th>
|
||||||
|
<th style="width:64px">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($letters as $i => $sp)
|
||||||
|
@php
|
||||||
|
$statusLabels = ['draft' => 'Draft', 'printed' => 'Tercetak', 'signed' => 'Ditandatangani', 'delivered' => 'Diserahkan'];
|
||||||
|
$label = $statusLabels[$sp->status] ?? ucfirst($sp->status);
|
||||||
|
$badge = isset($statusLabels[$sp->status]) ? 'badge-'.$sp->status : 'badge-other';
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td>{{ $i + 1 }}</td>
|
||||||
|
<td>{{ $sp->letter_number }}<span class="sub">{{ $sp->created_at?->format('d/m/Y') }}</span></td>
|
||||||
|
<td>{{ $sp->student?->full_name }}<span class="sub">NISN: {{ $sp->student?->nisn }}</span></td>
|
||||||
|
<td>{{ $sp->student?->class_name }}</td>
|
||||||
|
<td>{{ $sp->spThreshold?->name }}</td>
|
||||||
|
<td class="center"><span class="poin">{{ $sp->total_points_at_time }}</span></td>
|
||||||
|
<td class="center"><span class="badge {{ $badge }} ">{{ $label }}</span></td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="7" class="center">Belum ada surat peringatan</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- TTD --}}
|
||||||
|
<table class="ttd">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>Petugas / Guru BK</div>
|
||||||
|
<div class="nama"> </div>
|
||||||
|
<div>______________________</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Mengetahui,<br>Kepala {{ $school['name'] }}</div>
|
||||||
|
<div class="nama">{{ $school['kepala_sekolah'] }}</div>
|
||||||
|
<div>NIP. {{ $school['kepala_sekolah_nip'] }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Dicetak {{ now()->locale('id')->isoFormat('dddd, D MMMM YYYY, HH:mm') }} WIB • E-TATIB {{ $school['name'] }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,9 +3,18 @@
|
|||||||
@section('title', 'Surat Peringatan')
|
@section('title', 'Surat Peringatan')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="mb-6">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Surat Peringatan</h1>
|
<div>
|
||||||
<p class="text-sm text-gray-500">Daftar surat peringatan siswa</p>
|
<h1 class="text-2xl font-bold text-gray-900">Surat Peringatan</h1>
|
||||||
|
<p class="text-sm text-gray-500">Daftar surat peringatan siswa</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('sp-letters.export-pdf') }}"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition shadow-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5 5-5M12 15V3"/>
|
||||||
|
</svg>
|
||||||
|
Ekspor PDF
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200/80 overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200/80 overflow-hidden">
|
||||||
@@ -50,6 +59,7 @@
|
|||||||
<td class="px-4 py-3 text-sm text-gray-500">{{ $sp->created_at->format('d/m/Y') }}</td>
|
<td class="px-4 py-3 text-sm text-gray-500">{{ $sp->created_at->format('d/m/Y') }}</td>
|
||||||
<td class="px-4 py-3 text-right space-x-2">
|
<td class="px-4 py-3 text-right space-x-2">
|
||||||
<a href="{{ route('sp-letters.show', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Detail</a>
|
<a href="{{ route('sp-letters.show', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">Detail</a>
|
||||||
|
<a href="{{ route('sp-letters.pdf', $sp->id) }}" class="text-red-600 hover:text-red-800 text-sm font-medium">PDF</a>
|
||||||
<a href="{{ route('sp-letters.print', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium" target="_blank">Cetak</a>
|
<a href="{{ route('sp-letters.print', $sp->id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium" target="_blank">Cetak</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -4,33 +4,37 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Cetak SP - {{ $spLetter->student->full_name }}</title>
|
<title>Cetak SP - {{ $spLetter->student->full_name }}</title>
|
||||||
<style>
|
<style>
|
||||||
@page { margin: 2cm 1cm; size: 210mm 330mm; } /* F4 — kiri/kanan 1cm */
|
@page { margin: 1cm 1.5cm 0.8cm 2cm; size: 210mm 330mm; } /* F4 — kiri 2cm, kanan 1.5cm */
|
||||||
body { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: #000; }
|
p { margin: 0; }
|
||||||
.kop-surat { margin-bottom: 24px; }
|
body { font-family: 'Times New Roman', 'DejaVu Serif', Times, serif; font-size: 11pt; line-height: 1.4; color: #000; }
|
||||||
|
.kop-surat { margin-bottom: 4px; }
|
||||||
.kop-table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
|
.kop-table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
|
||||||
.kop-table td { border: none; padding: 0; }
|
.kop-table td { border: none; padding: 0; }
|
||||||
.kop-logo { width: 112px; text-align: center; vertical-align: middle; }
|
.kop-logo { width: 84px; text-align: center; vertical-align: middle; }
|
||||||
.kop-logo img { height: 86px; width: auto; max-width: 104px; object-fit: contain; }
|
.kop-logo img { height: 70px; width: auto; max-width: 76px; object-fit: contain; }
|
||||||
.kop-text { text-align: center; vertical-align: middle; }
|
.kop-text { text-align: center; vertical-align: middle; width: 400pt; }
|
||||||
.kop-1, .kop-2 { font-size: 13.5pt; font-weight: bold; margin: 0; text-transform: uppercase; }
|
.kop-1, .kop-2 { font-size: 13pt; font-weight: bold; margin: 0; text-transform: uppercase; line-height: 1; }
|
||||||
.kop-3 { font-size: 11.5pt; font-weight: bold; margin: 1px 0; text-transform: uppercase; white-space: nowrap; }
|
.kop-3 { font-size: 13pt; font-weight: bold; margin: 1px 0; text-transform: uppercase; line-height: 1; }
|
||||||
.kop-4, .kop-5, .kop-6 { font-size: 10.5pt; margin: 0; }
|
.kop-4, .kop-5, .kop-6 { font-size: 10pt; margin: 0; }
|
||||||
.kop-surat .line { border-top: 2.5px solid #000; margin-top: 6px; }
|
.kop-surat .line { border-top: 2.5px solid #000; margin-top: 6px; }
|
||||||
.kop-surat .sub-line { border-top: 1px solid #000; margin-top: 2px; }
|
.kop-surat .sub-line { border-top: 1px solid #000; margin-top: 2px; }
|
||||||
.letter-number { text-align: center; margin: 25px 0; font-weight: bold; font-size: 13pt; }
|
.letter-number { text-align: center; margin: 0 0 1.2em; font-weight: bold; font-size: 11pt; }
|
||||||
.to-title { margin-bottom: 15px; }
|
.letter-number .letter-title { font-size: 11pt; }
|
||||||
.body-text { text-align: justify; margin-bottom: 20px; }
|
.to-title { margin-bottom: 1.2em; }
|
||||||
.violation-list { margin: 15px 0 15px 20px; }
|
.body-text { text-align: justify; margin-bottom: 0; }
|
||||||
|
.body-text p { margin-bottom: 0; }
|
||||||
|
.violation-list { margin: 0 0 1px 20px; }
|
||||||
.violation-list li { margin-bottom: 3px; }
|
.violation-list li { margin-bottom: 3px; }
|
||||||
.violation-table { width: 100%; border-collapse: collapse; margin: 12px 0; }
|
.violation-table { width: 100%; border-collapse: collapse; margin: 4px 0; line-height: 1.05; }
|
||||||
.violation-table th, .violation-table td { border: 1px solid #000; padding: 6px 10px; text-align: left; font-size: 11pt; }
|
.violation-table th { border: 1px solid #000; padding: 3px 7px; text-align: left; font-size: 11pt; font-weight: bold; }
|
||||||
|
.violation-table td { border: 1px solid #000; padding: 3px 7px; text-align: left; font-size: 11pt; }
|
||||||
.violation-table th { font-weight: bold; }
|
.violation-table th { font-weight: bold; }
|
||||||
.violation-table .total-row td { font-weight: bold; }
|
.violation-table .total-row td { font-weight: bold; }
|
||||||
.signature { margin-top: 50px; padding-left: 24em; }
|
.signature { margin-top: 1.4em; padding-left: 19em; font-size: 11pt; }
|
||||||
.signature .date { text-align: left; margin-bottom: 5px; }
|
.signature .date { text-align: left; margin-bottom: 1px; }
|
||||||
.signature .title { text-align: left; margin-bottom: 60px; }
|
.signature .title { text-align: left; margin-bottom: 2.8em; } /* enter 2x — ruang tanda tangan */
|
||||||
.signature .name { text-align: left; font-weight: bold; }
|
.signature .name { text-align: left; font-weight: bold; }
|
||||||
.signature .nip { text-align: left; font-size: 11pt; }
|
.signature .nip { text-align: left; margin-top: 4px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="letter-number">
|
<div class="letter-number">
|
||||||
<p style="margin-bottom: 0;"><u>SURAT PERINGATAN</u></p>
|
<p class="letter-title" style="margin-bottom: 0;"><u>SURAT PERINGATAN</u></p>
|
||||||
<p style="margin-top: 0;">Nomor: {{ $spLetter->letter_number }}</p>
|
<p style="margin-top: 0;">Nomor: {{ $spLetter->letter_number }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@
|
|||||||
|
|
||||||
<div class="body-text">
|
<div class="body-text">
|
||||||
<p>Total akumulasi poin pelanggaran: <strong>{{ $spLetter->total_points_at_time }} poin</strong></p>
|
<p>Total akumulasi poin pelanggaran: <strong>{{ $spLetter->total_points_at_time }} poin</strong></p>
|
||||||
<p>Berikut daftar pelanggaran :</p>
|
<p style="margin-bottom: 1.4em;">Berikut daftar pelanggaran :</p>
|
||||||
@php $violations = is_array($spLetter->violations_included) ? $spLetter->violations_included : json_decode($spLetter->violations_included, true); @endphp
|
@php $violations = is_array($spLetter->violations_included) ? $spLetter->violations_included : json_decode($spLetter->violations_included, true); @endphp
|
||||||
@if($violations)
|
@if($violations)
|
||||||
<table class="violation-table">
|
<table class="violation-table">
|
||||||
@@ -94,9 +98,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ \Carbon\Carbon::parse($v['violation_date'] ?? now())->setTimezone('Asia/Jakarta')->locale('id')->isoFormat('dddd, DD MMMM Y') }}</td>
|
<td>{{ \Carbon\Carbon::parse($v['violation_date'] ?? now())->setTimezone('Asia/Jakarta')->locale('id')->isoFormat('dddd, DD MMMM Y') }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ \App\Models\Violation::with('violationType')->find($v['id'] ?? null)?->violationType?->name ?? '-' }}
|
{{ \App\Models\Violation::withTrashed()->with('violationType')->find($v['id'] ?? null)?->violationType?->name ?? '-' }}
|
||||||
@if (! empty($v['description']))
|
@if (! empty($v['description']))
|
||||||
<div style="font-style: italic; font-size: 9pt; font-weight: normal; margin-top: 2px;">{{ $v['description'] }}</div>
|
<div style="font-style: italic; font-size: 11pt; font-weight: normal; margin-top: 2px;">{{ $v['description'] }}</div>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">{{ $v['points'] ?? 0 }}</td>
|
<td style="text-align: center;">{{ $v['points'] ?? 0 }}</td>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<a href="{{ route('sp-letters.index') }}" class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition">Kembali</a>
|
<a href="{{ route('sp-letters.index') }}" class="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition">Kembali</a>
|
||||||
|
<a href="{{ route('sp-letters.pdf', $spLetter->id) }}" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition">Unduh PDF</a>
|
||||||
<a href="{{ route('sp-letters.print', $spLetter->id) }}" target="_blank" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition">Cetak</a>
|
<a href="{{ route('sp-letters.print', $spLetter->id) }}" target="_blank" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition">Cetak</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,8 +95,10 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
Route::middleware('permission:view-sp-letters')->group(function () {
|
Route::middleware('permission:view-sp-letters')->group(function () {
|
||||||
Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index');
|
Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index');
|
||||||
|
Route::get('/sp-letters/export-pdf', [SpLetterController::class, 'exportPdf'])->name('sp-letters.export-pdf');
|
||||||
Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show');
|
Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show');
|
||||||
Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print');
|
Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print');
|
||||||
|
Route::get('/sp-letters/{spLetter}/pdf', [SpLetterController::class, 'pdf'])->name('sp-letters.pdf');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== Master Data (permission-based) =====
|
// ===== Master Data (permission-based) =====
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Attendance;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Models\Student;
|
||||||
|
use App\Models\Violation;
|
||||||
|
use App\Models\ViolationType;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIX 2026-08-31 — bug sync attendance e-jurnal → violation alpha:
|
||||||
|
* 1. violation lama tidak pernah dihapus saat push koreksi tanpa alpha ($alphaDate null)
|
||||||
|
* 2. totalAlpha dihitung untuk semua siswa memakai tanggal global → salah siswa/tanggal
|
||||||
|
*
|
||||||
|
* Kasus nyata: XII TKJ 3 26-08 HENDIK (sakit) / RAMADANI & RHEDO (hadir) dicatat bolos.
|
||||||
|
*/
|
||||||
|
class AttendanceSyncViolationFixTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Setting::setValue('ejurnal_sync_token', 'test-token');
|
||||||
|
|
||||||
|
$category = \App\Models\ViolationCategory::create([
|
||||||
|
'name' => 'Kehadiran',
|
||||||
|
'slug' => 'kehadiran',
|
||||||
|
'color' => '#EF4444',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ViolationType::create([
|
||||||
|
'category_id' => $category->id,
|
||||||
|
'name' => 'Membolos/tidak masuk sekolah tanpa keterangan',
|
||||||
|
'slug' => 'alpha',
|
||||||
|
'points' => 20, // poin per 10 JP (2 poin/JP)
|
||||||
|
'description' => 'Tidak hadir tanpa keterangan',
|
||||||
|
'is_active' => true,
|
||||||
|
'is_system' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function makeStudent(int $sourceId, string $name): Student
|
||||||
|
{
|
||||||
|
return Student::create([
|
||||||
|
'source_id' => (string) $sourceId,
|
||||||
|
'nisn' => '00' . $sourceId,
|
||||||
|
'student_number' => 'TEST/' . $sourceId,
|
||||||
|
'full_name' => $name,
|
||||||
|
'class_name' => 'XII TKJ 3',
|
||||||
|
'class_level' => 'XII',
|
||||||
|
'department_name' => 'TKJ',
|
||||||
|
'status' => 'active',
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sync(array $attendances): \Illuminate\Testing\TestResponse
|
||||||
|
{
|
||||||
|
return $this->post('/api/v1/attendance/sync', [
|
||||||
|
'token' => 'test-token',
|
||||||
|
'attendances' => $attendances,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_alpha_membuat_violation_hanya_untuk_siswa_alpha(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA ALPHA');
|
||||||
|
$s2 = $this->makeStudent(1002, 'SISWA HADIR');
|
||||||
|
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 2, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1002, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'hadir'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
// Siswa alpha → violation 2 JP = 4 poin (2 poin/JP)
|
||||||
|
$this->assertDatabaseHas('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
'points' => 4,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Siswa hadir → TIDAK boleh kena violation
|
||||||
|
$this->assertDatabaseMissing('violations', [
|
||||||
|
'student_id' => $s2->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_koreksi_tanpa_alpha_menghapus_violation_lama(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA KOREKSI');
|
||||||
|
$alphaType = ViolationType::where('slug', 'alpha')->first();
|
||||||
|
|
||||||
|
// Simulasikan violation system-generated lama (salah: siswa sebenarnya sakit)
|
||||||
|
Violation::create([
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'student_class' => 'XII TKJ 3',
|
||||||
|
'violation_type_id' => $alphaType->id,
|
||||||
|
'recorded_by' => null,
|
||||||
|
'description' => 'Alpha - Tidak hadir tanpa keterangan (4 jam pelajaran)',
|
||||||
|
'points' => 8,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
'notes' => 'Dibuat otomatis dari sinkron E-Jurnal.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Push koreksi: siswa kini SAKIT (tidak ada alpha sama sekali dalam push)
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'sakit'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 2, 'status' => 'sakit'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
// Violation lama harus TERHAPUS (bug lama: tidak terhapus karena $alphaDate null)
|
||||||
|
$this->assertNull(
|
||||||
|
Violation::where('student_id', $s1->id)->where('violation_date', '2026-08-26')->first(),
|
||||||
|
'Violation alpha lama harus terhapus setelah push koreksi tanpa alpha.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_multi_tanggal_tidak_membuat_violation_salah_tanggal(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA CAMPURAN');
|
||||||
|
|
||||||
|
// Push berisi 2 tanggal: 26-08 hadir, 27-08 alpha.
|
||||||
|
// Bug lama: $alphaDate global = 27-08 → siswa dihitung alpha di 27-08 untuk semua siswa.
|
||||||
|
// Harusnya violation HANYA untuk 27-08.
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'hadir'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-27', 'lesson_hour' => 1, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-27', 'lesson_hour' => 2, 'status' => 'alpha'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-27',
|
||||||
|
'points' => 4,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_push_siswa_yang_bukan_alpha_tidak_kena_violation_saat_push_berisi_alpha_siswa_lain(): void
|
||||||
|
{
|
||||||
|
$s1 = $this->makeStudent(1001, 'SISWA ALPHA');
|
||||||
|
$s2 = $this->makeStudent(1002, 'SISWA HADIR');
|
||||||
|
|
||||||
|
// Bug lama: totalAlpha dihitung utk SEMUA siswa dgn tanggal global → s2 ikut kena.
|
||||||
|
$this->sync([
|
||||||
|
['source_student_id' => 1001, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'alpha'],
|
||||||
|
['source_student_id' => 1002, 'date' => '2026-08-26', 'lesson_hour' => 1, 'status' => 'hadir'],
|
||||||
|
['source_student_id' => 1002, 'date' => '2026-08-26', 'lesson_hour' => 2, 'status' => 'hadir'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('violations', [
|
||||||
|
'student_id' => $s1->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('violations', [
|
||||||
|
'student_id' => $s2->id,
|
||||||
|
'violation_date' => '2026-08-26',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user