DONE: add flexible json update saham, yuran, and pelaburan ui
This commit is contained in:
@@ -12,7 +12,7 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
{--election_id= : Override election_id (default: latest in voter table)}
|
||||
{--dry-run : Show what would change without updating}';
|
||||
|
||||
protected $description = 'Update voter pelaburan (dividen) from list-pelabur.json for latest election only';
|
||||
protected $description = 'Update voter dividen_pelaburan (from JSON pelaburan) and accumulated_amount from list-pelabur.json for latest election only';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -51,8 +51,9 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
// JSON key "pelaburan" is the dividend amount → voter.dividen_pelaburan
|
||||
$map[$noAnggota] = [
|
||||
'pelaburan' => $this->cleanNumber($row['pelaburan'] ?? 0),
|
||||
'dividen_pelaburan' => $this->cleanNumber($row['pelaburan'] ?? 0),
|
||||
'accumulated_amount' => $this->cleanNumber($row['accumulated_amount'] ?? 0),
|
||||
];
|
||||
}
|
||||
@@ -67,6 +68,7 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
$this->info("Target election_id: {$electionId}");
|
||||
$this->info("JSON members loaded: " . count($map));
|
||||
$this->info($dryRun ? "Mode: DRY RUN (no DB updates)" : "Mode: UPDATE");
|
||||
$this->info("Mapping: JSON.pelaburan → voter.dividen_pelaburan, JSON.accumulated_amount → voter.accumulated_amount");
|
||||
|
||||
$totalInElection = (int) DB::table('voter')->where('election_id', $electionId)->count();
|
||||
$this->info("Voters in election: {$totalInElection}");
|
||||
@@ -80,7 +82,7 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
$bar->start();
|
||||
|
||||
DB::table('voter')
|
||||
->select(['id', 'no_anggota', 'pelaburan', 'accumulated_amount'])
|
||||
->select(['id', 'no_anggota', 'dividen_pelaburan', 'accumulated_amount'])
|
||||
->where('election_id', $electionId)
|
||||
->orderBy('id')
|
||||
->chunkById(500, function ($chunk) use ($map, $dryRun, &$updated, &$matched, &$missingInJson, &$unchanged, $bar) {
|
||||
@@ -96,11 +98,11 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
$matched++;
|
||||
$jsonRow = $map[$noAnggota];
|
||||
|
||||
$oldPelaburan = $voter->pelaburan !== null ? (float) $voter->pelaburan : 0.0;
|
||||
$oldDividen = $voter->dividen_pelaburan !== null ? (float) $voter->dividen_pelaburan : 0.0;
|
||||
$oldAccumulated = $voter->accumulated_amount !== null ? (float) $voter->accumulated_amount : 0.0;
|
||||
|
||||
if (
|
||||
abs($oldPelaburan - $jsonRow['pelaburan']) < 0.00001
|
||||
abs($oldDividen - $jsonRow['dividen_pelaburan']) < 0.00001
|
||||
&& abs($oldAccumulated - $jsonRow['accumulated_amount']) < 0.00001
|
||||
) {
|
||||
$unchanged++;
|
||||
@@ -112,7 +114,7 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
DB::table('voter')
|
||||
->where('id', $voter->id)
|
||||
->update([
|
||||
'pelaburan' => $jsonRow['pelaburan'],
|
||||
'dividen_pelaburan' => $jsonRow['dividen_pelaburan'],
|
||||
'accumulated_amount' => $jsonRow['accumulated_amount'],
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
@@ -145,7 +147,7 @@ class UpdateVoterPelaburanFromJson extends Command
|
||||
|
||||
private function cleanNumber($value): float
|
||||
{
|
||||
if ($value === null) {
|
||||
if ($value === null || $value === '') {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Voter;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Import keanggotaan JSON (match by no_anggota) into voter money columns for the current election.
|
||||
* Expected row keys: no_anggota, pelaburan, pelaburan_2, saham, yuran. Null/empty → 0.
|
||||
*/
|
||||
class ImportKeanggotaanJsonController extends Controller
|
||||
{
|
||||
private const UPDATABLE_FIELDS = [
|
||||
'pelaburan',
|
||||
'pelaburan_2',
|
||||
'saham',
|
||||
'yuran',
|
||||
];
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'file' => 'required|file|max:10240',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$extension = strtolower($file->getClientOriginalExtension() ?: '');
|
||||
if ($extension !== 'json') {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Fail mestilah format .json.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$raw = file_get_contents($file->getRealPath());
|
||||
$rows = json_decode($raw, true);
|
||||
|
||||
if (!is_array($rows)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'JSON tidak sah. Jangkaan array objek.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$noAnggota = isset($row['no_anggota']) ? trim((string) $row['no_anggota']) : '';
|
||||
if ($noAnggota === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
foreach (self::UPDATABLE_FIELDS as $field) {
|
||||
if (array_key_exists($field, $row)) {
|
||||
$payload[$field] = $this->cleanNumber($row[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($payload) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$noAnggota] = $payload;
|
||||
}
|
||||
|
||||
if (count($map) === 0) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Tiada baris sah dalam JSON (perlu no_anggota dan sekurang-kurangnya satu medan).',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$electionId = (int) Util::getCurrentElection();
|
||||
if ($electionId <= 0) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Tidak dapat mengenal pasti election semasa.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'election_id' => $electionId,
|
||||
'json_rows' => count($map),
|
||||
'matched' => 0,
|
||||
'updated' => 0,
|
||||
'unchanged' => 0,
|
||||
'missing_in_voter' => 0,
|
||||
'missing_in_json' => 0,
|
||||
];
|
||||
|
||||
$matchedKeys = [];
|
||||
|
||||
DB::table('voter')
|
||||
->select(array_merge(['id', 'no_anggota'], self::UPDATABLE_FIELDS))
|
||||
->where('election_id', $electionId)
|
||||
->orderBy('id')
|
||||
->chunkById(500, function ($chunk) use ($map, &$stats, &$matchedKeys) {
|
||||
foreach ($chunk as $voter) {
|
||||
$noAnggota = $voter->no_anggota !== null ? trim((string) $voter->no_anggota) : '';
|
||||
|
||||
if ($noAnggota === '' || !array_key_exists($noAnggota, $map)) {
|
||||
$stats['missing_in_json']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$stats['matched']++;
|
||||
$matchedKeys[$noAnggota] = true;
|
||||
$jsonRow = $map[$noAnggota];
|
||||
|
||||
$update = [];
|
||||
foreach ($jsonRow as $field => $newValue) {
|
||||
$oldValue = $voter->{$field} !== null ? (float) $voter->{$field} : 0.0;
|
||||
if (abs($oldValue - $newValue) >= 0.00001) {
|
||||
$update[$field] = $newValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($update) === 0) {
|
||||
$stats['unchanged']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$update['updated_at'] = now();
|
||||
DB::table('voter')->where('id', $voter->id)->update($update);
|
||||
$stats['updated']++;
|
||||
}
|
||||
}, 'id');
|
||||
|
||||
foreach ($map as $noAnggota => $_payload) {
|
||||
if (!isset($matchedKeys[$noAnggota])) {
|
||||
$stats['missing_in_voter']++;
|
||||
}
|
||||
}
|
||||
|
||||
activity()
|
||||
->withProperties([
|
||||
'election_id' => $electionId,
|
||||
'filename' => $file->getClientOriginalName(),
|
||||
'stats' => $stats,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('import keanggotaan json');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Import keanggotaan berjaya.',
|
||||
'data' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
private function cleanNumber($value): float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$value = (string) $value;
|
||||
$value = str_replace(['RM', ',', ' '], '', $value);
|
||||
|
||||
if (preg_match('/^\((.*)\)$/', $value, $matches)) {
|
||||
$value = $matches[1];
|
||||
}
|
||||
|
||||
$value = preg_replace('/[^0-9\.\-]/', '', $value);
|
||||
|
||||
return is_numeric($value) ? (float) $value : 0.0;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ class ElectionMiddleware
|
||||
&& !($request->is('api/v1/voter') && $request->isMethod('post'))
|
||||
// Allow updating applicable voters list during election (add page uses POST /api/v1/voter/sync-applicable)
|
||||
&& !($request->is('api/v1/voter/sync-applicable') && $request->isMethod('post'))
|
||||
// Allow keanggotaan JSON import during election
|
||||
&& !($request->is('api/v1/voter/import-keanggotaan-json') && $request->isMethod('post'))
|
||||
&& !$request->is('api/v1/voter/*/verify-fizikal-registration')
|
||||
// Pentadbir utama: urus akaun (tambah/kemaskini/padam) semasa mengundi
|
||||
&& !$request->is('api/v1/admin')
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ class Voter extends Authenticatable
|
||||
|
||||
protected $table = 'voter';
|
||||
|
||||
protected $fillable = ['name', 'no_kp', 'no_anggota','unit', 'election_id', 'alamat', 'telefon', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran', 'saham_terkini', 'yuran_terkini', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
|
||||
protected $fillable = ['name', 'no_kp', 'no_anggota','unit', 'election_id', 'alamat', 'telefon', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran', 'saham_terkini', 'yuran_terkini', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'dividen_pelaburan', 'pelaburan_terkini', 'pelaburan_2', 'dividen_pelaburan_2', 'pelaburan_terkini_2', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
|
||||
|
||||
protected $casts = [
|
||||
'fizikal_registration_verified_at' => 'datetime',
|
||||
|
||||
Reference in New Issue
Block a user