425 lines
18 KiB
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);
|
|
}
|
|
} |