import anggota
This commit is contained in:
@@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\DrawResult;
|
||||||
use App\Models\ImportLog;
|
use App\Models\ImportLog;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class MemberImportService
|
class MemberImportService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Import baris anggota. Tidak crash bila ada duplicate/ralat —
|
* Import baris anggota. Hanya 3 medan diproses: nama, no_anggota, no_kp.
|
||||||
* pulangkan ringkasan + simpan ImportLog.
|
* Setiap import akan BUANG semua anggota lama dahulu (ganti penuh).
|
||||||
|
* Tidak crash bila ada duplicate/ralat — pulangkan ringkasan + simpan ImportLog.
|
||||||
*
|
*
|
||||||
* @param array<int, array<string, string>> $rows
|
* @param array<int, array<string, string>> $rows
|
||||||
*/
|
*/
|
||||||
@@ -20,16 +23,33 @@ class MemberImportService
|
|||||||
$failed = 0;
|
$failed = 0;
|
||||||
$errors = [];
|
$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
|
// Set pra-muat untuk kesan duplicate dalam fail itu sendiri
|
||||||
$seenKp = [];
|
$seenKp = [];
|
||||||
$seenPekerja = [];
|
$seenAnggota = [];
|
||||||
|
|
||||||
|
DB::transaction(function () use ($rows, &$success, &$duplicate, &$failed, &$errors, &$seenKp, &$seenAnggota) {
|
||||||
|
// Buang semua anggota lama (ganti penuh setiap import)
|
||||||
|
Member::query()->delete();
|
||||||
|
|
||||||
foreach ($rows as $i => $row) {
|
foreach ($rows as $i => $row) {
|
||||||
$line = $i + 2; // baris fail (header = 1)
|
$line = $i + 2; // baris fail (header = 1)
|
||||||
|
|
||||||
$nama = trim($row['nama'] ?? '');
|
$nama = trim($row['nama'] ?? '');
|
||||||
$noKp = $this->clean($row['no_kp'] ?? '');
|
$noKp = $this->clean($row['no_kp'] ?? '');
|
||||||
$noPekerja = $this->clean($row['no_pekerja'] ?? '');
|
|
||||||
$noAnggota = $this->clean($row['no_anggota'] ?? '');
|
$noAnggota = $this->clean($row['no_anggota'] ?? '');
|
||||||
|
|
||||||
if ($nama === '') {
|
if ($nama === '') {
|
||||||
@@ -44,42 +64,24 @@ class MemberImportService
|
|||||||
$errors[] = "Baris {$line}: no_kp '{$noKp}' duplicate dalam fail.";
|
$errors[] = "Baris {$line}: no_kp '{$noKp}' duplicate dalam fail.";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($noPekerja && isset($seenPekerja[$noPekerja])) {
|
if ($noAnggota && isset($seenAnggota[$noAnggota])) {
|
||||||
$duplicate++;
|
$duplicate++;
|
||||||
$errors[] = "Baris {$line}: no_pekerja '{$noPekerja}' duplicate dalam fail.";
|
$errors[] = "Baris {$line}: no_anggota '{$noAnggota}' 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Member::create([
|
Member::create([
|
||||||
'no_anggota' => $noAnggota ?: null,
|
'no_anggota' => $noAnggota ?: null,
|
||||||
'no_pekerja' => $noPekerja ?: null,
|
|
||||||
'no_kp' => $noKp ?: null,
|
'no_kp' => $noKp ?: null,
|
||||||
'nama' => $nama,
|
'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) {
|
if ($noKp) {
|
||||||
$seenKp[$noKp] = true;
|
$seenKp[$noKp] = true;
|
||||||
}
|
}
|
||||||
if ($noPekerja) {
|
if ($noAnggota) {
|
||||||
$seenPekerja[$noPekerja] = true;
|
$seenAnggota[$noAnggota] = true;
|
||||||
}
|
}
|
||||||
$success++;
|
$success++;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -87,6 +89,7 @@ class MemberImportService
|
|||||||
$errors[] = "Baris {$line}: {$e->getMessage()}";
|
$errors[] = "Baris {$line}: {$e->getMessage()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return ImportLog::create([
|
return ImportLog::create([
|
||||||
'type' => 'members',
|
'type' => 'members',
|
||||||
@@ -104,14 +107,4 @@ class MemberImportService
|
|||||||
{
|
{
|
||||||
return trim($value);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
no_anggota,no_pekerja,no_kp,nama,jabatan,bahagian,telefon,status_aktif
|
nama,no_anggota,no_kp
|
||||||
A00001,MBIP00001,850101-01-5523,Ahmad bin Ali,Kewangan,Unit A,012-3456789,aktif
|
Ahmad bin Ali,A00001,850101-01-5523
|
||||||
A00002,MBIP00002,880215-14-6644,Siti binti Yusof,Khidmat Pengurusan,Pentadbiran,013-2233445,aktif
|
Siti binti Yusof,A00002,880215-14-6644
|
||||||
A00003,MBIP00003,790320-10-1122,Lim Chee Keong,Teknologi Maklumat,Operasi,014-9988776,aktif
|
Lim Chee Keong,A00003,790320-10-1122
|
||||||
A00004,MBIP00004,910707-08-3344,Nurul Huda binti Razak,Perancangan,Unit B,011-22334455,aktif
|
Nurul Huda binti Razak,A00004,910707-08-3344
|
||||||
A00005,MBIP00005,830512-12-7788,Rajesh a/l Kumar,Kejuruteraan,Sokongan,019-8765432,tidak aktif
|
Rajesh a/l Kumar,A00005,830512-12-7788
|
||||||
|
|||||||
|
@@ -7,7 +7,8 @@
|
|||||||
<div class="col-lg-5">
|
<div class="col-lg-5">
|
||||||
<div class="card"><div class="card-body">
|
<div class="card"><div class="card-body">
|
||||||
<h5 class="fw-bold"><i class="bi bi-upload text-teal"></i> Muat Naik Fail</h5>
|
<h5 class="fw-bold"><i class="bi bi-upload text-teal"></i> Muat Naik Fail</h5>
|
||||||
<p class="text-muted small">Format CSV atau XLSX. Lajur: <code>no_anggota, no_pekerja, no_kp, nama, jabatan, bahagian, telefon, status_aktif</code>. Hanya <b>nama</b> wajib.</p>
|
<p class="text-muted small">Format CSV atau XLSX. Lajur: <code>nama, no_anggota, no_kp</code>. Hanya <b>nama</b> wajib.
|
||||||
|
<span class="text-danger d-block mt-1"><i class="bi bi-exclamation-triangle-fill"></i> Amaran: setiap import akan <b>membuang semua data anggota lama</b> dan menggantikannya dengan fail ini.</span></p>
|
||||||
<form method="POST" action="{{ route('admin.members.import.store') }}" enctype="multipart/form-data">
|
<form method="POST" action="{{ route('admin.members.import.store') }}" enctype="multipart/form-data">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="file" name="file" class="form-control mb-2 @error('file') is-invalid @enderror" accept=".csv,.xlsx,.xls" required>
|
<input type="file" name="file" class="form-control mb-2 @error('file') is-invalid @enderror" accept=".csv,.xlsx,.xls" required>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ class ImportTest extends TestCase
|
|||||||
public function test_member_import_handles_duplicates_and_missing_name(): void
|
public function test_member_import_handles_duplicates_and_missing_name(): void
|
||||||
{
|
{
|
||||||
$rows = [
|
$rows = [
|
||||||
['no_pekerja' => 'P001', 'no_kp' => '900101-01-1111', 'nama' => 'Ali'],
|
['no_anggota' => 'A001', 'no_kp' => '900101-01-1111', 'nama' => 'Ali'],
|
||||||
['no_pekerja' => 'P001', 'no_kp' => '900101-01-2222', 'nama' => 'Ali Duplicate Pekerja'], // dup dalam fail
|
['no_anggota' => 'A001', 'no_kp' => '900101-01-2222', 'nama' => 'Ali Duplicate Anggota'], // dup dalam fail
|
||||||
['no_pekerja' => 'P002', 'no_kp' => '', 'nama' => ''], // gagal: nama kosong
|
['no_anggota' => 'A002', 'no_kp' => '', 'nama' => ''], // gagal: nama kosong
|
||||||
['no_pekerja' => 'P003', 'no_kp' => '900101-01-3333', 'nama' => 'Siti'],
|
['no_anggota' => 'A003', 'no_kp' => '900101-01-3333', 'nama' => 'Siti'],
|
||||||
];
|
];
|
||||||
|
|
||||||
$log = (new MemberImportService())->import($rows, 'test.csv');
|
$log = (new MemberImportService())->import($rows, 'test.csv');
|
||||||
@@ -30,10 +30,31 @@ class ImportTest extends TestCase
|
|||||||
$this->assertEquals(2, Member::count());
|
$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');
|
(new MemberImportService())->import([
|
||||||
$this->assertTrue(Member::first()->status_aktif);
|
['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
|
public function test_prize_import_expands_quantity(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user