Merge branch 'feature/add-dividen-share-display' into 'main'

DONE: add dividen for yuran and saham by injecting json

See merge request erahn/voting!18
This commit is contained in:
ISMAIL MASSERAN
2026-06-23 05:13:19 +00:00
9 changed files with 3374 additions and 6 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -0,0 +1,171 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class UpdateVoterDividenFromJson extends Command
{
protected $signature = 'voter:update-dividen-from-json
{--file=database/seeds/inject/dividen_yuransaham.json : Relative path from project root}
{--election_id= : Override election_id (default: latest in voter table)}
{--dry-run : Show what would change without updating}';
protected $description = 'Update voter saham/yuran and populate dividen_saham/dividen_yuran from JSON for latest election only';
public function handle()
{
$fileOpt = (string) $this->option('file');
$filePath = $this->resolvePath($fileOpt);
if (!is_file($filePath)) {
$this->error("❌ JSON file not found: {$filePath}");
return 1;
}
$electionId = $this->option('election_id');
$electionId = $electionId !== null && $electionId !== '' ? (int) $electionId : (int) DB::table('voter')->max('election_id');
if ($electionId <= 0) {
$this->error("❌ Cannot determine latest election_id (got {$electionId}).");
return 1;
}
$json = file_get_contents($filePath);
$rows = json_decode($json, true);
if (!is_array($rows)) {
$this->error("❌ Invalid JSON: expected array of rows.");
return 1;
}
$map = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$noAnggota = isset($row['no_anggota']) ? trim((string) $row['no_anggota']) : '';
if ($noAnggota === '') {
continue;
}
$map[$noAnggota] = [
'saham' => $this->cleanNumber($row['saham'] ?? 0),
'yuran' => $this->cleanNumber($row['yuran'] ?? 0),
'dividen_saham' => $this->cleanNumber($row['dividen_saham'] ?? 0),
'dividen_yuran' => $this->cleanNumber($row['dividen_yuran'] ?? 0),
];
}
if (count($map) === 0) {
$this->error("❌ No usable rows found in JSON (missing no_anggota?).");
return 1;
}
$dryRun = (bool) $this->option('dry-run');
$this->info("Target election_id: {$electionId}");
$this->info("JSON members loaded: " . count($map));
$this->info($dryRun ? "Mode: DRY RUN (no DB updates)" : "Mode: UPDATE");
$totalInElection = (int) DB::table('voter')->where('election_id', $electionId)->count();
$this->info("Voters in election: {$totalInElection}");
$updated = 0;
$matched = 0;
$missingInJson = 0;
$unchanged = 0;
$bar = $this->output->createProgressBar(max(1, $totalInElection));
$bar->start();
DB::table('voter')
->select(['id', 'no_anggota', 'saham', 'yuran', 'dividen_saham', 'dividen_yuran'])
->where('election_id', $electionId)
->orderBy('id')
->chunkById(500, function ($chunk) use ($map, $dryRun, &$updated, &$matched, &$missingInJson, &$unchanged, $bar) {
foreach ($chunk as $voter) {
$noAnggota = $voter->no_anggota !== null ? trim((string) $voter->no_anggota) : '';
if ($noAnggota === '' || !array_key_exists($noAnggota, $map)) {
$missingInJson++;
$bar->advance();
continue;
}
$matched++;
$jsonRow = $map[$noAnggota];
$oldSaham = $voter->saham !== null ? (float) $voter->saham : 0.0;
$oldYuran = $voter->yuran !== null ? (float) $voter->yuran : 0.0;
$oldDividenSaham = $voter->dividen_saham !== null ? (float) $voter->dividen_saham : 0.0;
$oldDividenYuran = $voter->dividen_yuran !== null ? (float) $voter->dividen_yuran : 0.0;
if (
abs($oldSaham - $jsonRow['saham']) < 0.00001
&& abs($oldYuran - $jsonRow['yuran']) < 0.00001
&& abs($oldDividenSaham - $jsonRow['dividen_saham']) < 0.00001
&& abs($oldDividenYuran - $jsonRow['dividen_yuran']) < 0.00001
) {
$unchanged++;
$bar->advance();
continue;
}
if (!$dryRun) {
DB::table('voter')
->where('id', $voter->id)
->update([
'saham' => $jsonRow['saham'],
'yuran' => $jsonRow['yuran'],
'dividen_saham' => $jsonRow['dividen_saham'],
'dividen_yuran' => $jsonRow['dividen_yuran'],
'updated_at' => now(),
]);
}
$updated++;
$bar->advance();
}
}, 'id');
$bar->finish();
$this->line('');
$this->info("✅ Matched in JSON: {$matched}");
$this->info("✅ Updated: {$updated}" . ($dryRun ? " (would update)" : ""));
$this->info("✅ Unchanged: {$unchanged}");
$this->info("⚠️ Missing in JSON: {$missingInJson}");
return 0;
}
private function resolvePath(string $path): string
{
if (strpos($path, DIRECTORY_SEPARATOR) === 0) {
return $path;
}
return base_path($path);
}
private function cleanNumber($value): float
{
if ($value === null) {
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;
}
}
+1
View File
@@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel
//
Commands\ImportCSV::class,
Commands\UpdateVoterPelaburanFromJson::class,
Commands\UpdateVoterDividenFromJson::class,
];
/**
+1 -1
View File
@@ -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', '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', 'kehadiran','status_penyata','persetujuan','tarikh_sah','cadangan','pelaburan', 'accumulated_amount', 'tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
protected $casts = [
'fizikal_registration_verified_at' => 'datetime',
@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddDividenSahamAndDividenYuranToVoterTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('voter', function (Blueprint $table) {
$table->decimal('dividen_saham', 12, 2)->default(0)->after('yuran');
$table->decimal('dividen_yuran', 12, 2)->default(0)->after('dividen_saham');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('voter', function (Blueprint $table) {
$table->dropColumn(['dividen_saham', 'dividen_yuran']);
});
}
}
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -57,7 +57,10 @@
<span class="member-field-icon" aria-hidden="true"><i class="fa" :class="row.icon"></i></span>
<div class="member-field-body">
<span class="member-field-label">{{ row.label }}</span>
<span class="member-field-value">{{ row.value }}</span>
<div class="member-field-value-wrap">
<span class="member-field-value">{{ row.value }}</span>
<span v-if="row.subvalue" class="member-field-subvalue">{{ row.subvalue }}</span>
</div>
</div>
</div>
</div>
@@ -386,13 +389,27 @@ export default {
if (n === null || n === undefined || n === "") return "—";
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const dividendSub = (balance, dividend, percent) => {
if (balance === null || balance === undefined || balance === "") return null;
return "Dividen (" + percent + "%): RM " + fmt(dividend);
};
return [
{ icon: "fa-user", label: "Nama", value: u.name || "—" },
{ icon: "fa-credit-card", label: "No. Kad Pengenalan", value: u.no_kp || "—" },
{ icon: "fa-barcode", label: "No. Anggota", value: u.no_anggota || "—" },
{ icon: "fa-building", label: "Unit", value: u.unit || "—" },
{ icon: "fa-line-chart", label: "Saham", value: "RM " + fmt(u.saham) },
{ icon: "fa-money", label: "Yuran", value: "RM " + fmt(u.yuran) },
{
icon: "fa-line-chart",
label: "Saham",
value: "RM " + fmt(u.saham),
subvalue: dividendSub(u.saham, u.dividen_saham, 40)
},
{
icon: "fa-money",
label: "Yuran",
value: "RM " + fmt(u.yuran),
subvalue: dividendSub(u.yuran, u.dividen_yuran, 8)
},
{ icon: "fa-bank", label: "Simpanan Khas (Tabung 1) - (Sehingga Dis 2025)", value: "RM " + fmt(u.accumulated_amount) },
{ icon: "fa-percent", label: "Dividen Simpanan Khas (Tabung 1) Tahun 2025", value: "RM " + fmt(u.pelaburan) }
];
@@ -1017,8 +1034,17 @@ export default {
line-height: 1.35;
}
.member-field-value {
.member-field-value-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.member-field-value {
width: 100%;
text-align: right;
font-size: 1.45rem;
font-weight: 700;
@@ -1027,6 +1053,16 @@ export default {
word-break: break-word;
}
.member-field-subvalue {
width: 100%;
text-align: right;
font-size: 1.05rem;
font-weight: 600;
color: #2563eb;
line-height: 1.35;
word-break: break-word;
}
/* —— Claim —— */
.home-claim-card {
padding: 20px 22px;
@@ -1387,6 +1423,13 @@ export default {
line-height: 1.25;
}
.home-member-card--in-section .member-field-value-wrap {
flex: none;
width: 100%;
align-items: flex-start;
gap: 3px;
}
.home-member-card--in-section .member-field-value {
flex: none;
width: 100%;
@@ -1399,6 +1442,12 @@ export default {
overflow-wrap: anywhere;
}
.home-member-card--in-section .member-field-subvalue {
width: 100%;
text-align: left;
font-size: 1.1rem;
}
.home-claim-code {
font-size: 2.2rem;
letter-spacing: 0.14em;