MIF_E31230892/sim-pkpps/app/Http/Controllers/Admin/ImportMesinController.php

425 lines
18 KiB
PHP

<?php
// app/Http/Controllers/Admin/ImportMesinController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AbsensiKegiatan;
use App\Models\ImportMesinLog;
use App\Models\Kegiatan;
use App\Models\Kepulangan;
use App\Models\MesinSantriMapping;
use App\Services\EpposGLogParser;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ImportMesinController extends Controller
{
public function __construct(private EpposGLogParser $parser) {}
// ──────────────────────────────────────────────────────────
// INDEX
// ──────────────────────────────────────────────────────────
public function index()
{
$belumMapping = MesinSantriMapping::where('is_active', true)
->where(function ($q) {
$q->whereNull('id_santri')->orWhere('id_santri', '');
})->count();
$riwayat = ImportMesinLog::with('user')->latest()->take(10)->get();
return view('admin.mesin.import.index', compact('belumMapping', 'riwayat'));
}
// ──────────────────────────────────────────────────────────
// PREVIEW — POST
// Proses file GLog, simpan hasil ke session, redirect ke
// showPreview (GET). PRG pattern agar refresh tidak error.
// ──────────────────────────────────────────────────────────
public function preview(Request $request)
{
$request->validate([
'file_glog' => 'required|file|max:20480',
'tol_sebelum' => 'nullable|integer|min:0|max:60',
'tol_sesudah' => 'nullable|integer|min:0|max:60',
'isi_alpa' => 'nullable',
'conflict_strategy' => 'nullable|in:mesin,exist,manual',
]);
// ── Baca toleransi PERSIS dari form (bukan hardcode) ──
// Jika field tidak dikirim (misal: disable JS), fallback ke nilai
// default form (15 dan 10) yang sama persis dengan default di index.blade.php
$tolSebelum = (int)($request->input('tol_sebelum', 15));
$tolSesudah = (int)($request->input('tol_sesudah', 10));
$isiAlpa = $request->has('isi_alpa');
$conflictStrategy = $request->input('conflict_strategy', 'mesin');
// ── Parse GLog ────────────────────────────────────────
try {
$glogRecords = $this->parser->parseGLog(
$request->file('file_glog')->getPathname()
);
} catch (\Throwable $e) {
return back()->with('error', 'Gagal membaca file GLog: ' . $e->getMessage());
}
if (empty($glogRecords)) {
return back()->with('error',
'File GLog tidak mengandung data scan yang valid. ' .
'Pastikan file yang diupload benar (format GLog dari Eppos).'
);
}
// ── Bangun infoData dari mapping yang sudah ada ───────
$mappingAll = MesinSantriMapping::where('is_active', true)->get();
$infoData = ['shifts' => [], 'jadwal' => []];
foreach ($mappingAll as $m) {
$infoData['jadwal'][$m->id_mesin] = [
'nama' => $m->nama_mesin ?? '',
'dept' => $m->dept_mesin ?? '',
'shift' => 1,
];
}
// ── Kegiatan dari DB ──────────────────────────────────
$kegiatans = Kegiatan::orderBy('hari')->orderBy('waktu_mulai')
->get()
->map(function ($k) {
$rawMulai = $k->getRawOriginal('waktu_mulai');
$rawSelesai = $k->getRawOriginal('waktu_selesai');
$mulai = $rawMulai ? substr($rawMulai, 0, 5) : '00:00';
$selesai = $rawSelesai ? substr($rawSelesai, 0, 5) : $mulai;
return [
'kegiatan_id' => $k->kegiatan_id,
'nama' => $k->nama_kegiatan,
'hari' => $k->hari,
'waktu_mulai' => $mulai,
'waktu_selesai' => $selesai,
];
})->toArray();
if (empty($kegiatans)) {
return back()->with('error',
'Tidak ada kegiatan tersimpan di database. ' .
'Tambahkan kegiatan terlebih dahulu di menu Kegiatan.'
);
}
// ── Match scan ke kegiatan (pakai toleransi dari form) ─
$glogGrouped = $this->parser->groupGLogByDay($glogRecords);
$rawHasil = $this->parser->matchToKegiatan(
$glogGrouped,
$infoData,
$kegiatans,
$tolSebelum, // ← dari form, bukan hardcode
$tolSesudah // ← dari form, bukan hardcode
);
// ── Enrich: santri web + kepulangan + deteksi konflik ─
$kepulanganCache = [];
$hasilEnriched = [];
foreach ($rawHasil as $dayData) {
$tanggal = $dayData['tanggal'];
$idMesin = $dayData['id_mesin'];
$mapping = MesinSantriMapping::where('id_mesin', $idMesin)
->where('is_active', true)
->with('santri')
->first();
$idSantri = $mapping?->santri?->id_santri;
$namaWeb = $mapping?->santri?->nama_lengkap;
$kelas = $mapping?->santri?->kelasPrimary?->kelas?->nama_kelas ?? '-';
// Cache kepulangan per tanggal
if (!isset($kepulanganCache[$tanggal])) {
$kepulanganCache[$tanggal] = Kepulangan::where('status', 'Disetujui')
->where('tanggal_pulang', '<=', $tanggal)
->where('tanggal_kembali', '>=', $tanggal)
->pluck('id_santri')->toArray();
}
$isPulang = $idSantri && in_array($idSantri, $kepulanganCache[$tanggal]);
$rows = array_map(
function ($row) use ($idSantri, $tanggal, $isPulang, $isiAlpa) {
$statusFinal = $isPulang ? 'Pulang' : $row['status'];
if (!$isiAlpa && $statusFinal === 'Alpa' && !$row['matched']) {
$statusFinal = null;
}
$existing = null;
$isConflict = false;
if ($idSantri) {
$rec = AbsensiKegiatan::where('kegiatan_id', $row['kegiatan_id'])
->where('id_santri', $idSantri)
->whereDate('tanggal', $tanggal)
->first();
if ($rec) {
$rawWaktu = $rec->getRawOriginal('waktu_absen');
$existing = [
'status' => $rec->status,
'waktu' => $rawWaktu ? substr($rawWaktu, 0, 5) : null,
'metode' => $rec->metode_absen ?? 'Manual',
];
if (!$row['matched'] && $statusFinal === 'Alpa') {
// Tidak ada scan = tidak override data manual
$statusFinal = $rec->status;
$isConflict = false;
} else {
$isConflict = ($rec->metode_absen !== 'Import_Mesin')
&& ($rec->status !== $statusFinal)
&& $statusFinal !== null;
}
}
}
return array_merge($row, [
'status_final' => $statusFinal,
'existing' => $existing,
'is_conflict' => $isConflict,
]);
},
$dayData['rows']
);
$hasilEnriched[] = array_merge($dayData, [
'id_santri' => $idSantri,
'nama_web' => $namaWeb,
'kelas' => $kelas,
'match_status' => $mapping
? ($idSantri ? 'OK' : 'NO_SANTRI')
: 'NOT_MAPPED',
'is_pulang' => $isPulang,
'rows' => $rows,
]);
}
// Urutkan: tanggal → nama
usort($hasilEnriched, fn($a, $b) =>
[$a['tanggal'], $a['nama_web'] ?? $a['nama_mesin']]
<=> [$b['tanggal'], $b['nama_web'] ?? $b['nama_mesin']]
);
// ── Simpan ke session lalu redirect (PRG pattern) ─────
session([
'eppos_hasil' => $hasilEnriched,
'tol_sebelum' => $tolSebelum, // nilai dari form
'tol_sesudah' => $tolSesudah, // nilai dari form
'isi_alpa' => $isiAlpa,
'conflict_strategy' => $conflictStrategy,
]);
return redirect()->route('admin.mesin.import.show-preview');
}
// ──────────────────────────────────────────────────────────
// SHOW PREVIEW — GET (aman di-refresh)
// ──────────────────────────────────────────────────────────
public function showPreview()
{
$hasilEnriched = session('eppos_hasil');
if (empty($hasilEnriched)) {
return redirect()->route('admin.mesin.import.index')
->with('error', 'Tidak ada data preview. Silakan upload file GLog terlebih dahulu.');
}
// Ambil toleransi dari session (nilai yang dipakai saat matching)
$tolSebelum = session('tol_sebelum', 15);
$tolSesudah = session('tol_sesudah', 10);
$isiAlpa = session('isi_alpa', true);
$conflictStrategy = session('conflict_strategy', 'mesin');
$tanggalList = array_unique(array_column($hasilEnriched, 'tanggal'));
sort($tanggalList);
$debugScans = [];
foreach ($hasilEnriched as $h) {
if (!empty($h['unmatched_scans'])) {
$debugScans[] = [
'nama' => $h['nama_web'] ?? $h['nama_mesin'],
'tanggal' => $h['tanggal'],
'id_mesin' => $h['id_mesin'],
'scans' => $h['all_scans'],
'unmatched' => $h['unmatched_scans'],
];
}
}
$allRows = collect($hasilEnriched)->flatMap(fn($h) => $h['rows']);
$stats = [
'total_santri' => count($hasilEnriched),
'ok' => collect($hasilEnriched)->where('match_status', 'OK')->count(),
'not_mapped' => collect($hasilEnriched)->where('match_status', 'NOT_MAPPED')->count(),
'hadir' => $allRows->where('status_final', 'Hadir')->count(),
'terlambat' => $allRows->where('status_final', 'Terlambat')->count(),
'alpa' => $allRows->where('status_final', 'Alpa')->count(),
'konflik' => $allRows->where('is_conflict', true)->count(),
];
return view('admin.mesin.import.preview', compact(
'hasilEnriched', 'tanggalList', 'stats',
'tolSebelum', 'tolSesudah', 'isiAlpa',
'debugScans', 'conflictStrategy'
));
}
// ──────────────────────────────────────────────────────────
// STORE — simpan ke database
// ──────────────────────────────────────────────────────────
public function store(Request $request)
{
$hasilEnriched = session('eppos_hasil', []);
if (empty($hasilEnriched)) {
return redirect()->route('admin.mesin.import.index')
->with('error', 'Sesi expired. Silakan upload ulang file GLog.');
}
$bulkStrategy = $request->input('conflict_strategy', 'manual');
$choices = $request->input('conflict_choices', []);
$counters = [
'created' => 0,
'updated' => 0,
'kept' => 0,
'skipped' => 0,
'no_santri' => 0,
'null_skip' => 0,
];
DB::beginTransaction();
try {
foreach ($hasilEnriched as $dayData) {
if (!$dayData['id_santri']) {
$counters['no_santri']++;
continue;
}
foreach ($dayData['rows'] as $row) {
// Status null = tidak perlu disimpan
if ($row['status_final'] === null) {
$counters['null_skip']++;
continue;
}
// Alpa tanpa scan + sudah ada data existing → pertahankan
if (!$row['matched'] && $row['status_final'] === 'Alpa'
&& !empty($row['existing'])) {
$counters['skipped']++;
continue;
}
// Status sama dengan existing → tidak perlu update
if (!$row['matched'] && !empty($row['existing'])
&& $row['status_final'] === $row['existing']['status']) {
$counters['skipped']++;
continue;
}
$key = "{$row['kegiatan_id']}_{$dayData['id_santri']}_{$dayData['tanggal']}";
$hasExisting = !empty($row['existing']);
$isConflict = $row['is_conflict'] ?? false;
if (!$hasExisting) {
// Belum ada data → buat baru
AbsensiKegiatan::create([
'kegiatan_id' => $row['kegiatan_id'],
'id_santri' => $dayData['id_santri'],
'tanggal' => $dayData['tanggal'],
'status' => $row['status_final'],
'metode_absen' => 'Import_Mesin',
'waktu_absen' => $row['jam_scan']
? Carbon::parse(
$dayData['tanggal'] . ' ' . $row['jam_scan']
)->format('H:i:s')
: Carbon::parse($dayData['tanggal'])->format('H:i:s'),
]);
$counters['created']++;
continue;
}
// Ada existing tapi tidak konflik → skip
if (!$isConflict) {
$counters['skipped']++;
continue;
}
// Ada konflik → cek strategi
$choice = ($bulkStrategy !== 'manual')
? $bulkStrategy
: ($choices[$key] ?? null);
if ($choice === 'mesin') {
AbsensiKegiatan::where('kegiatan_id', $row['kegiatan_id'])
->where('id_santri', $dayData['id_santri'])
->whereDate('tanggal', $dayData['tanggal'])
->update([
'status' => $row['status_final'],
'metode_absen' => 'Import_Mesin',
'waktu_absen' => $row['jam_scan']
? Carbon::parse(
$dayData['tanggal'] . ' ' . $row['jam_scan']
)->format('H:i:s')
: null,
'konflik_catatan' => 'Ditimpa import mesin '
. now()->format('d/m/Y H:i')
. ' (sebelumnya: '
. $row['existing']['status']
. ' via '
. ($row['existing']['metode'] ?? 'Manual')
. ')',
]);
$counters['updated']++;
} else {
// Pertahankan data lama
$counters['kept']++;
}
}
}
// Catat ke log
ImportMesinLog::create([
'user_id' => auth()->id(),
'jumlah_scan' => collect($hasilEnriched)
->flatMap(fn($h) => $h['all_scans'])->count(),
'berhasil' => $counters['created'],
'konflik_selesai' => $counters['updated'],
'dilewati' => $counters['skipped'],
'no_santri' => $counters['no_santri'],
]);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
return back()->with('error', 'Import gagal: ' . $e->getMessage());
}
// Hapus session setelah berhasil
session()->forget([
'eppos_hasil', 'tol_sebelum', 'tol_sesudah',
'isi_alpa', 'conflict_strategy',
]);
$msg = "Import selesai! "
. "{$counters['created']} data baru tersimpan, "
. "{$counters['updated']} konflik (pilih mesin), "
. "{$counters['kept']} konflik (pertahankan data lama), "
. "{$counters['skipped']} duplikat dilewati.";
if ($counters['no_santri'] > 0) {
$msg .= " | {$counters['no_santri']} santri belum ada mapping (tidak tersimpan).";
}
return redirect()->route('admin.riwayat-kegiatan.index')
->with('success', $msg);
}
}