diff --git a/app/Services/MemberImportService.php b/app/Services/MemberImportService.php index 4909a71..8946f3f 100644 --- a/app/Services/MemberImportService.php +++ b/app/Services/MemberImportService.php @@ -2,14 +2,17 @@ namespace App\Services; +use App\Models\DrawResult; use App\Models\ImportLog; use App\Models\Member; +use Illuminate\Support\Facades\DB; class MemberImportService { /** - * Import baris anggota. Tidak crash bila ada duplicate/ralat — - * pulangkan ringkasan + simpan ImportLog. + * Import baris anggota. Hanya 3 medan diproses: nama, no_anggota, no_kp. + * Setiap import akan BUANG semua anggota lama dahulu (ganti penuh). + * Tidak crash bila ada duplicate/ralat — pulangkan ringkasan + simpan ImportLog. * * @param array> $rows */ @@ -20,73 +23,73 @@ class MemberImportService $failed = 0; $errors = []; + // Lindung integriti cabutan: jangan buang anggota jika sudah ada pemenang disahkan. + if (DrawResult::where('status', DrawResult::STATUS_CONFIRMED)->exists()) { + return ImportLog::create([ + 'type' => 'members', + 'filename' => $filename, + 'total_rows' => count($rows), + 'success_count' => 0, + 'duplicate_count' => 0, + 'failed_count' => count($rows), + 'errors' => ['Import dibatalkan: sudah ada pemenang cabutan yang disahkan. Data anggota tidak boleh dibuang.'], + 'imported_by' => $userId, + ]); + } + // Set pra-muat untuk kesan duplicate dalam fail itu sendiri $seenKp = []; - $seenPekerja = []; + $seenAnggota = []; - foreach ($rows as $i => $row) { - $line = $i + 2; // baris fail (header = 1) + DB::transaction(function () use ($rows, &$success, &$duplicate, &$failed, &$errors, &$seenKp, &$seenAnggota) { + // Buang semua anggota lama (ganti penuh setiap import) + Member::query()->delete(); - $nama = trim($row['nama'] ?? ''); - $noKp = $this->clean($row['no_kp'] ?? ''); - $noPekerja = $this->clean($row['no_pekerja'] ?? ''); - $noAnggota = $this->clean($row['no_anggota'] ?? ''); + foreach ($rows as $i => $row) { + $line = $i + 2; // baris fail (header = 1) - if ($nama === '') { - $failed++; - $errors[] = "Baris {$line}: nama wajib diisi."; - continue; - } + $nama = trim($row['nama'] ?? ''); + $noKp = $this->clean($row['no_kp'] ?? ''); + $noAnggota = $this->clean($row['no_anggota'] ?? ''); - // Duplicate dalam fail - if ($noKp && isset($seenKp[$noKp])) { - $duplicate++; - $errors[] = "Baris {$line}: no_kp '{$noKp}' duplicate dalam fail."; - continue; - } - if ($noPekerja && isset($seenPekerja[$noPekerja])) { - $duplicate++; - $errors[] = "Baris {$line}: no_pekerja '{$noPekerja}' duplicate dalam fail."; - continue; - } - - // Duplicate dalam DB - $exists = Member::query() - ->when($noKp, fn ($q) => $q->orWhere('no_kp', $noKp)) - ->when($noPekerja, fn ($q) => $q->orWhere('no_pekerja', $noPekerja)) - ->when($noKp || $noPekerja, fn ($q) => $q, fn ($q) => $q->whereRaw('1 = 0')) - ->exists(); - - if ($exists) { - $duplicate++; - $errors[] = "Baris {$line}: anggota '{$nama}' sudah wujud (no_kp/no_pekerja sepadan)."; - continue; - } - - try { - Member::create([ - 'no_anggota' => $noAnggota ?: null, - 'no_pekerja' => $noPekerja ?: null, - 'no_kp' => $noKp ?: null, - 'nama' => $nama, - 'jabatan' => $this->clean($row['jabatan'] ?? '') ?: null, - 'bahagian' => $this->clean($row['bahagian'] ?? '') ?: null, - 'telefon' => $this->clean($row['telefon'] ?? '') ?: null, - 'status_aktif' => $this->parseAktif($row['status_aktif'] ?? ''), - ]); - - if ($noKp) { - $seenKp[$noKp] = true; + if ($nama === '') { + $failed++; + $errors[] = "Baris {$line}: nama wajib diisi."; + continue; } - if ($noPekerja) { - $seenPekerja[$noPekerja] = true; + + // Duplicate dalam fail + if ($noKp && isset($seenKp[$noKp])) { + $duplicate++; + $errors[] = "Baris {$line}: no_kp '{$noKp}' duplicate dalam fail."; + continue; + } + if ($noAnggota && isset($seenAnggota[$noAnggota])) { + $duplicate++; + $errors[] = "Baris {$line}: no_anggota '{$noAnggota}' duplicate dalam fail."; + continue; + } + + try { + Member::create([ + 'no_anggota' => $noAnggota ?: null, + 'no_kp' => $noKp ?: null, + 'nama' => $nama, + ]); + + if ($noKp) { + $seenKp[$noKp] = true; + } + if ($noAnggota) { + $seenAnggota[$noAnggota] = true; + } + $success++; + } catch (\Throwable $e) { + $failed++; + $errors[] = "Baris {$line}: {$e->getMessage()}"; } - $success++; - } catch (\Throwable $e) { - $failed++; - $errors[] = "Baris {$line}: {$e->getMessage()}"; } - } + }); return ImportLog::create([ 'type' => 'members', @@ -104,14 +107,4 @@ class MemberImportService { return trim($value); } - - private function parseAktif(string $value): bool - { - $value = strtolower(trim($value)); - if ($value === '') { - return true; // default aktif - } - - return in_array($value, ['1', 'aktif', 'active', 'ya', 'yes', 'true', 'y'], true); - } } diff --git a/public/samples/contoh-anggota.csv b/public/samples/contoh-anggota.csv index 316b275..6df0119 100644 --- a/public/samples/contoh-anggota.csv +++ b/public/samples/contoh-anggota.csv @@ -1,6 +1,6 @@ -no_anggota,no_pekerja,no_kp,nama,jabatan,bahagian,telefon,status_aktif -A00001,MBIP00001,850101-01-5523,Ahmad bin Ali,Kewangan,Unit A,012-3456789,aktif -A00002,MBIP00002,880215-14-6644,Siti binti Yusof,Khidmat Pengurusan,Pentadbiran,013-2233445,aktif -A00003,MBIP00003,790320-10-1122,Lim Chee Keong,Teknologi Maklumat,Operasi,014-9988776,aktif -A00004,MBIP00004,910707-08-3344,Nurul Huda binti Razak,Perancangan,Unit B,011-22334455,aktif -A00005,MBIP00005,830512-12-7788,Rajesh a/l Kumar,Kejuruteraan,Sokongan,019-8765432,tidak aktif +nama,no_anggota,no_kp +Ahmad bin Ali,A00001,850101-01-5523 +Siti binti Yusof,A00002,880215-14-6644 +Lim Chee Keong,A00003,790320-10-1122 +Nurul Huda binti Razak,A00004,910707-08-3344 +Rajesh a/l Kumar,A00005,830512-12-7788 diff --git a/resources/views/admin/members/import.blade.php b/resources/views/admin/members/import.blade.php index e240544..002b21d 100644 --- a/resources/views/admin/members/import.blade.php +++ b/resources/views/admin/members/import.blade.php @@ -7,7 +7,8 @@
Muat Naik Fail
-

Format CSV atau XLSX. Lajur: no_anggota, no_pekerja, no_kp, nama, jabatan, bahagian, telefon, status_aktif. Hanya nama wajib.

+

Format CSV atau XLSX. Lajur: nama, no_anggota, no_kp. Hanya nama wajib. + Amaran: setiap import akan membuang semua data anggota lama dan menggantikannya dengan fail ini.

@csrf diff --git a/tests/Feature/ImportTest.php b/tests/Feature/ImportTest.php index 6702dce..f2b2f2e 100644 --- a/tests/Feature/ImportTest.php +++ b/tests/Feature/ImportTest.php @@ -16,10 +16,10 @@ class ImportTest extends TestCase public function test_member_import_handles_duplicates_and_missing_name(): void { $rows = [ - ['no_pekerja' => 'P001', 'no_kp' => '900101-01-1111', 'nama' => 'Ali'], - ['no_pekerja' => 'P001', 'no_kp' => '900101-01-2222', 'nama' => 'Ali Duplicate Pekerja'], // dup dalam fail - ['no_pekerja' => 'P002', 'no_kp' => '', 'nama' => ''], // gagal: nama kosong - ['no_pekerja' => 'P003', 'no_kp' => '900101-01-3333', 'nama' => 'Siti'], + ['no_anggota' => 'A001', 'no_kp' => '900101-01-1111', 'nama' => 'Ali'], + ['no_anggota' => 'A001', 'no_kp' => '900101-01-2222', 'nama' => 'Ali Duplicate Anggota'], // dup dalam fail + ['no_anggota' => 'A002', 'no_kp' => '', 'nama' => ''], // gagal: nama kosong + ['no_anggota' => 'A003', 'no_kp' => '900101-01-3333', 'nama' => 'Siti'], ]; $log = (new MemberImportService())->import($rows, 'test.csv'); @@ -30,10 +30,31 @@ class ImportTest extends TestCase $this->assertEquals(2, Member::count()); } - public function test_member_import_default_status_aktif(): void + public function test_member_import_only_stores_three_fields(): void { - (new MemberImportService())->import([['nama' => 'Tiada Status']], 'test.csv'); - $this->assertTrue(Member::first()->status_aktif); + (new MemberImportService())->import([ + ['nama' => 'Ali', 'no_anggota' => 'A001', 'no_kp' => '900101-01-1111', 'no_pekerja' => 'P001', 'jabatan' => 'IT'], + ], 'test.csv'); + + $this->assertDatabaseHas('members', [ + 'nama' => 'Ali', + 'no_anggota' => 'A001', + 'no_kp' => '900101-01-1111', + 'no_pekerja' => null, + 'jabatan' => null, + ]); + } + + public function test_member_import_wipes_old_members(): void + { + Member::create(['nama' => 'Anggota Lama', 'no_anggota' => 'OLD1']); + Member::create(['nama' => 'Anggota Lama 2', 'no_anggota' => 'OLD2']); + + (new MemberImportService())->import([['nama' => 'Anggota Baru', 'no_anggota' => 'NEW1']], 'baru.csv'); + + $this->assertEquals(1, Member::count()); + $this->assertDatabaseHas('members', ['nama' => 'Anggota Baru']); + $this->assertDatabaseMissing('members', ['nama' => 'Anggota Lama']); } public function test_prize_import_expands_quantity(): void