Allowance check
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllowanceClaimCode extends Model
|
||||
{
|
||||
protected $table = 'allowance_claim_codes';
|
||||
|
||||
protected $fillable = [
|
||||
'election_id',
|
||||
'voter_id',
|
||||
'method',
|
||||
'code_hash',
|
||||
'code_last4',
|
||||
'expires_at',
|
||||
'used_at',
|
||||
'used_by_admin_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllowancePayout extends Model
|
||||
{
|
||||
protected $table = 'allowance_payouts';
|
||||
|
||||
protected $fillable = [
|
||||
'election_id',
|
||||
'voter_id',
|
||||
'method',
|
||||
'status',
|
||||
'amount_cents',
|
||||
'currency',
|
||||
'paid_by_admin_id',
|
||||
'paid_at',
|
||||
'reference',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ImportCsv extends Command
|
||||
class ImportCSV extends Command
|
||||
{
|
||||
protected $signature = 'csv:import';
|
||||
protected $description = 'Import kewangan anggota CSV';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\ActivityLog;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class IndexController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$perPage = (int) $request->query('per_page', 25);
|
||||
if ($perPage < 1) $perPage = 25;
|
||||
if ($perPage > 200) $perPage = 200;
|
||||
|
||||
$q = Activity::query()
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($request->filled('description')) {
|
||||
$q->where('description', 'like', '%' . $request->query('description') . '%');
|
||||
}
|
||||
|
||||
if ($request->filled('subject_type')) {
|
||||
$q->where('subject_type', (string) $request->query('subject_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('causer_id')) {
|
||||
$q->where('causer_id', (int) $request->query('causer_id'));
|
||||
}
|
||||
|
||||
$p = $q->paginate($perPage);
|
||||
|
||||
$items = collect($p->items())->map(function (Activity $a) {
|
||||
return [
|
||||
'id' => $a->id,
|
||||
'log_name' => $a->log_name,
|
||||
'description' => $a->description,
|
||||
'subject_type' => $a->subject_type,
|
||||
'subject_id' => $a->subject_id,
|
||||
'causer_type' => $a->causer_type,
|
||||
'causer_id' => $a->causer_id,
|
||||
'properties' => $a->properties,
|
||||
'created_at' => optional($a->created_at)->toDateTimeString(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $items,
|
||||
'meta' => [
|
||||
'current_page' => $p->currentPage(),
|
||||
'per_page' => $p->perPage(),
|
||||
'last_page' => $p->lastPage(),
|
||||
'total' => $p->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,20 @@ class AddController extends Controller
|
||||
public function __invoke (Request $request)
|
||||
{
|
||||
$this->validateRequest($request);
|
||||
$this->insertAdmin($request);
|
||||
$user = $this->insertAdmin($request);
|
||||
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
'admin_id' => $user->id,
|
||||
'admin_name' => $user->name,
|
||||
'admin_email' => $user->email,
|
||||
'admin_role' => $user->role,
|
||||
'created_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("create admin: {$user->name}");
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Admin added successfully'
|
||||
@@ -22,7 +35,7 @@ class AddController extends Controller
|
||||
{
|
||||
$admin = $request->all();
|
||||
$admin['password'] = bcrypt($request->password);
|
||||
User::create($admin);
|
||||
return User::create($admin);
|
||||
}
|
||||
|
||||
private function validateRequest($request)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\Allowance;
|
||||
|
||||
use App\AllowanceClaimCode;
|
||||
use App\AllowancePayout;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Result;
|
||||
use App\Voter;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class PayoutController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'voter_id' => 'required|integer',
|
||||
'method' => 'nullable|string|in:cash,bank',
|
||||
'amount_cents' => 'nullable|integer|min:0',
|
||||
'reference' => 'nullable|string|max:80',
|
||||
'note' => 'nullable|string|max:2000',
|
||||
'claim_code' => 'nullable|string|min:4|max:20',
|
||||
]);
|
||||
|
||||
$electionId = Util::getCurrentElection();
|
||||
$method = $request->input('method', 'cash');
|
||||
$voterId = (int) $request->input('voter_id');
|
||||
|
||||
$voter = Voter::where('election_id', $electionId)->where('id', $voterId)->first();
|
||||
if (!$voter) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Voter not found for current election.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Enforce method by attendance type (paperless replacement for coupon rules)
|
||||
if ($method === 'cash' && (int) $voter->kehadiran !== 1) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Cash payout only allowed for Fizikal attendance.',
|
||||
], 422);
|
||||
}
|
||||
if ($method === 'bank' && (int) $voter->kehadiran !== 2) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Bank payout only allowed for Maya attendance.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Enforce "must have voted" (officer check)
|
||||
$hasVoted = Result::where('election_id', $electionId)->where('voter_id', $voterId)->exists();
|
||||
if (!$hasVoted) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Voter has not voted yet.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Default amounts (can be overridden by passing amount_cents)
|
||||
$defaultAmount = $method === 'cash' ? 30000 : 15000;
|
||||
$amountCents = $request->filled('amount_cents') ? (int) $request->input('amount_cents') : $defaultAmount;
|
||||
|
||||
$now = Carbon::now();
|
||||
$adminId = $request->user() ? (int) $request->user()->id : null;
|
||||
|
||||
$claimCode = $request->input('claim_code');
|
||||
|
||||
// For fizikal cash payout, require claim code (paperless coupon).
|
||||
if ($method === 'cash' && (!$claimCode || trim($claimCode) === '')) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Sila masukkan kod tuntutan (claim code) sebelum bayaran tunai dibuat.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payout = DB::transaction(function () use ($electionId, $voterId, $method, $claimCode, $amountCents, $adminId, $now, $request) {
|
||||
// Consume claim code if provided
|
||||
if ($claimCode && trim($claimCode) !== '') {
|
||||
$row = AllowanceClaimCode::where('election_id', $electionId)
|
||||
->where('voter_id', $voterId)
|
||||
->where('method', $method)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan tidak dijumpai.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ($row->used_at) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan telah digunakan.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($row->expires_at && Carbon::now()->greaterThan($row->expires_at)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan telah tamat tempoh.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (!Hash::check(trim((string) $claimCode), $row->code_hash)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan tidak sah.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$row->used_at = Carbon::now();
|
||||
$row->used_by_admin_id = $adminId;
|
||||
$row->save();
|
||||
}
|
||||
|
||||
// Idempotent: if already exists (unique election_id+voter_id+method) we keep it as paid.
|
||||
return AllowancePayout::updateOrCreate(
|
||||
[
|
||||
'election_id' => $electionId,
|
||||
'voter_id' => $voterId,
|
||||
'method' => $method,
|
||||
],
|
||||
[
|
||||
'status' => 'paid',
|
||||
'amount_cents' => $amountCents,
|
||||
'currency' => 'MYR',
|
||||
'paid_by_admin_id' => $adminId,
|
||||
'paid_at' => $now,
|
||||
'reference' => $request->input('reference'),
|
||||
'note' => $request->input('note'),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// In case the transaction returned a JSON response (validation failure), return it directly.
|
||||
if ($payout instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $payout;
|
||||
}
|
||||
|
||||
activity()
|
||||
->performedOn($payout)
|
||||
->withProperties([
|
||||
'election_id' => $electionId,
|
||||
'voter_id' => $voterId,
|
||||
'voter_name' => $voter->name,
|
||||
'attendance' => (int) $voter->kehadiran,
|
||||
'method' => $method,
|
||||
'amount_cents' => (int) $payout->amount_cents,
|
||||
'currency' => $payout->currency,
|
||||
'paid_by_admin_id' => $adminId,
|
||||
'reference' => $payout->reference,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("allowance payout ({$method}): {$voter->name}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Allowance payout recorded.',
|
||||
'payout' => $payout,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\Allowance;
|
||||
|
||||
use App\AllowanceClaimCode;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class VerifyCodeController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'voter_id' => 'required|integer',
|
||||
'method' => 'nullable|string|in:cash,bank',
|
||||
'code' => 'required|string|min:4|max:20',
|
||||
]);
|
||||
|
||||
$electionId = Util::getCurrentElection();
|
||||
$method = $request->input('method', 'cash');
|
||||
$voterId = (int) $request->input('voter_id');
|
||||
$code = trim((string) $request->input('code'));
|
||||
|
||||
$row = AllowanceClaimCode::where('election_id', $electionId)
|
||||
->where('voter_id', $voterId)
|
||||
->where('method', $method)
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan tidak dijumpai.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if ($row->used_at) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan telah digunakan.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($row->expires_at && Carbon::now()->greaterThan($row->expires_at)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan telah tamat tempoh.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (!Hash::check($code, $row->code_hash)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan tidak sah.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Mark used (consumed) here so it cannot be reused
|
||||
$row->used_at = Carbon::now();
|
||||
$row->used_by_admin_id = $request->user() ? (int) $request->user()->id : null;
|
||||
$row->save();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Kod tuntutan disahkan.',
|
||||
'claim' => [
|
||||
'id' => $row->id,
|
||||
'voter_id' => $row->voter_id,
|
||||
'method' => $row->method,
|
||||
'expires_at' => $row->expires_at,
|
||||
'used_at' => $row->used_at,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\Allowance;
|
||||
|
||||
use App\AllowancePayout;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class VoidController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'note' => 'nullable|string|max:2000',
|
||||
]);
|
||||
|
||||
$payout = AllowancePayout::find($id);
|
||||
if (!$payout) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Payout not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$note = trim((string) $request->input('note', ''));
|
||||
if ($note !== '') {
|
||||
$payout->note = $payout->note ? ($payout->note . "\n\nVOID NOTE: " . $note) : ("VOID NOTE: " . $note);
|
||||
}
|
||||
|
||||
$payout->status = 'void';
|
||||
$payout->save();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Payout voided.',
|
||||
'payout' => $payout,
|
||||
'voided_at' => Carbon::now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\Impersonate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LeaveController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$current = $request->user();
|
||||
if (! $current) {
|
||||
return response()->json(['error' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
$token = null;
|
||||
try {
|
||||
if (method_exists($current, 'token')) {
|
||||
$token = $current->token();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$token = null;
|
||||
}
|
||||
|
||||
$tokenName = $token ? (string) $token->name : '';
|
||||
if (strpos($tokenName, 'impersonate:') !== 0) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Anda tidak sedang impersonate.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$impersonatorId = (int) substr($tokenName, strlen('impersonate:'));
|
||||
if ($impersonatorId < 1) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Token impersonate tidak sah.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$impersonator = User::find($impersonatorId);
|
||||
if (! $impersonator) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Akaun admin asal tidak dijumpai.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Revoke impersonated token.
|
||||
try {
|
||||
if ($token) {
|
||||
$token->revoke();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Best effort.
|
||||
}
|
||||
|
||||
$accessToken = $impersonator->createToken('My app', ['admin'])->accessToken;
|
||||
|
||||
activity()
|
||||
->performedOn($impersonator)
|
||||
->withProperties([
|
||||
'admin_id' => $impersonator->id,
|
||||
'admin_name' => $impersonator->name,
|
||||
'admin_email' => $impersonator->email,
|
||||
'from_user_id' => $current->id,
|
||||
'from_user_email' => $current->email,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("admin leave impersonation: {$impersonator->email}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Berjaya kembali ke akaun asal.',
|
||||
'user' => $impersonator,
|
||||
'token' => $accessToken,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Admin\Impersonate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class StartController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
]);
|
||||
|
||||
$impersonator = Auth::user();
|
||||
$target = User::findOrFail((int) $request->input('user_id'));
|
||||
|
||||
if (! $impersonator || ! method_exists($impersonator, 'tokenCan') || ! $impersonator->tokenCan('admin')) {
|
||||
return response()->json(['error' => 'Unauthenticated.'], 401);
|
||||
}
|
||||
|
||||
if ((int) $impersonator->id === (int) $target->id) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Tidak boleh impersonate akaun sendiri.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (method_exists($impersonator, 'canImpersonate') && ! $impersonator->canImpersonate()) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Akses ditolak (impersonate).',
|
||||
], 403);
|
||||
}
|
||||
|
||||
if (method_exists($target, 'canBeImpersonated') && ! $target->canBeImpersonated()) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Akaun ini tidak boleh di-impersonate.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Revoke current token to reduce risk of dual active sessions.
|
||||
try {
|
||||
if (method_exists($impersonator, 'token') && $impersonator->token()) {
|
||||
$impersonator->token()->revoke();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Best effort.
|
||||
}
|
||||
|
||||
// Encode impersonator id in token name for later "leave" action.
|
||||
$tokenName = 'impersonate:' . (int) $impersonator->id;
|
||||
$accessToken = $target->createToken($tokenName, ['admin'])->accessToken;
|
||||
|
||||
activity()
|
||||
->performedOn($target)
|
||||
->withProperties([
|
||||
'admin_id' => $impersonator->id,
|
||||
'admin_name' => $impersonator->name,
|
||||
'admin_email' => $impersonator->email,
|
||||
'impersonated_user_id' => $target->id,
|
||||
'impersonated_user_name' => $target->name,
|
||||
'impersonated_user_email' => $target->email,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("admin impersonate: {$impersonator->email} -> {$target->email}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Impersonate berjaya.',
|
||||
'user' => $target,
|
||||
'impersonator' => $impersonator,
|
||||
'token' => $accessToken,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,29 @@ class LoginController extends Controller
|
||||
$result['user'] = $user;
|
||||
$result['election_status'] = Util::getElectionStatus();
|
||||
$result['token'] = $user->createToken('My app', ['admin'])->accessToken;
|
||||
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
'admin_id' => $user->id,
|
||||
'admin_name' => $user->name,
|
||||
'admin_email' => $user->email,
|
||||
'admin_role' => $user->role,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("admin login: {$user->email}");
|
||||
} else {
|
||||
$result['status'] = 'failed';
|
||||
$result['message'] = 'Wrong email or password';
|
||||
|
||||
activity()
|
||||
->withProperties([
|
||||
'admin_email' => (string) $request->input('email'),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('admin login failed');
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
|
||||
@@ -8,8 +8,31 @@ use App\Http\Controllers\Controller;
|
||||
|
||||
class LogoutController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user) {
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
'admin_id' => $user->id,
|
||||
'admin_name' => $user->name,
|
||||
'admin_email' => $user->email,
|
||||
'admin_role' => $user->role,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("admin logout: {$user->email}");
|
||||
} else {
|
||||
activity()
|
||||
->withProperties([
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('admin logout');
|
||||
}
|
||||
|
||||
Auth::guard('web')->logout();
|
||||
Auth::guard('voter')->logout();
|
||||
return response()->json([
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Allowance;
|
||||
|
||||
use App\AllowanceClaimCode;
|
||||
use App\AllowancePayout;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Result;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class ClaimCodeController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'method' => 'nullable|string|in:cash,bank',
|
||||
]);
|
||||
|
||||
$electionId = Util::getCurrentElection();
|
||||
$method = $request->input('method', 'cash');
|
||||
|
||||
// voter auth guard; Auth::id() should be voter id under voterAPI
|
||||
$voterId = Auth::id();
|
||||
|
||||
$voter = $request->user();
|
||||
if (!$voter || (int) $voter->id !== (int) $voterId) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Unauthenticated.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Enforce method by attendance
|
||||
if ($method === 'cash' && (int) $voter->kehadiran !== 1) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan tunai hanya untuk kehadiran Fizikal.',
|
||||
], 422);
|
||||
}
|
||||
if ($method === 'bank' && (int) $voter->kehadiran !== 2) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod tuntutan bank hanya untuk kehadiran Maya.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Must have voted
|
||||
$hasVoted = Result::where('election_id', $electionId)->where('voter_id', $voterId)->exists();
|
||||
if (!$hasVoted) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Sila undi terlebih dahulu sebelum menuntut elaun.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// If already paid, don't generate codes
|
||||
$alreadyPaid = AllowancePayout::where('election_id', $electionId)
|
||||
->where('voter_id', $voterId)
|
||||
->where('method', $method)
|
||||
->where('status', 'paid')
|
||||
->exists();
|
||||
|
||||
if ($alreadyPaid) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Elaun anda telah direkodkan sebagai sudah dibayar.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Generate a 6-digit numeric code
|
||||
$codeInt = random_int(0, 999999);
|
||||
$code = str_pad((string) $codeInt, 6, '0', STR_PAD_LEFT);
|
||||
|
||||
$expiresAt = Carbon::now()->addHours(6);
|
||||
|
||||
$row = AllowanceClaimCode::updateOrCreate(
|
||||
[
|
||||
'election_id' => $electionId,
|
||||
'voter_id' => $voterId,
|
||||
'method' => $method,
|
||||
],
|
||||
[
|
||||
'code_hash' => Hash::make($code),
|
||||
'code_last4' => substr($code, -4),
|
||||
'expires_at' => $expiresAt,
|
||||
'used_at' => null,
|
||||
'used_by_admin_id' => null,
|
||||
]
|
||||
);
|
||||
|
||||
activity()
|
||||
->performedOn($row)
|
||||
->withProperties([
|
||||
'election_id' => $electionId,
|
||||
'voter_id' => (int) $voterId,
|
||||
'voter_name' => $voter->name,
|
||||
'attendance' => (int) $voter->kehadiran,
|
||||
'method' => $method,
|
||||
'expires_at' => $expiresAt->toDateTimeString(),
|
||||
'code_last4' => $row->code_last4,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("generate claim code ({$method}): {$voter->name}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Kod tuntutan berjaya dijana.',
|
||||
'code' => $code, // return plaintext ONCE to voter
|
||||
'expires_at' => $expiresAt,
|
||||
'method' => $method,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@ class StartController extends Controller
|
||||
'name'=>$request->name
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
activity()
|
||||
->withProperties([
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'election_name' => (string) $request->input('name'),
|
||||
'started_by_admin_id' => $user ? (int) $user->id : null,
|
||||
'started_by_admin_email' => $user ? $user->email : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('election start');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -21,6 +21,18 @@ class StopController extends Controller
|
||||
|
||||
$this->stopElection($request);
|
||||
|
||||
$user = $request->user();
|
||||
activity()
|
||||
->withProperties([
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'election_name' => (string) $request->input('name'),
|
||||
'stopped_by_admin_id' => $user ? (int) $user->id : null,
|
||||
'stopped_by_admin_email' => $user ? $user->email : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('election stop');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Election has finished.',
|
||||
|
||||
@@ -16,13 +16,38 @@ class VoteController extends Controller
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
|
||||
if ($this->validateRequest($request)['status'] == 'failed') {
|
||||
return response()->json($this->validateRequest($request));
|
||||
$validation = $this->validateRequest($request);
|
||||
if (($validation['status'] ?? 'failed') === 'failed') {
|
||||
return response()->json($validation);
|
||||
}
|
||||
|
||||
$this->insertVote($request);
|
||||
|
||||
$user = Auth::guard('voter')->user() ?: $request->user();
|
||||
$vote = is_array($request->vote) ? $request->vote : [];
|
||||
$positionsVoted = 0;
|
||||
$totalSelections = 0;
|
||||
foreach ($vote as $value) {
|
||||
$nomineeIds = $value['nominee_id'] ?? null;
|
||||
if (is_array($nomineeIds) && count($nomineeIds)) {
|
||||
$positionsVoted++;
|
||||
$totalSelections += count($nomineeIds);
|
||||
}
|
||||
}
|
||||
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'voter_id' => Auth::id(),
|
||||
'voter_name' => $user ? $user->name : null,
|
||||
'positions_voted' => $positionsVoted,
|
||||
'total_selections' => $totalSelections,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('submit vote');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Voted successfully',
|
||||
@@ -32,34 +57,31 @@ class VoteController extends Controller
|
||||
|
||||
private function insertVote($request)
|
||||
{
|
||||
// dd($request->vote[0]['nominee_id']);
|
||||
$votes = array_map(function($nominee_id) use($request){
|
||||
return[
|
||||
'voter_id' => Auth::id(),
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'position_id' => $request->vote[0]['position_id'],
|
||||
'nominee_id' => $nominee_id
|
||||
];
|
||||
$vote = $request->vote;
|
||||
if (!is_array($vote)) return;
|
||||
|
||||
},$request->vote[0]['nominee_id']);
|
||||
foreach ($vote as $value) {
|
||||
$positionId = $value['position_id'] ?? null;
|
||||
$nomineeIds = $value['nominee_id'] ?? [];
|
||||
if (!$positionId || !is_array($nomineeIds) || !count($nomineeIds)) continue;
|
||||
|
||||
foreach($votes as $vote){
|
||||
Result::updateOrCreate([
|
||||
'voter_id' => $vote['voter_id'],
|
||||
'election_id' => $vote['election_id'],
|
||||
'position_id' => $vote['position_id'],
|
||||
'nominee_id' => $vote['nominee_id'],
|
||||
]);
|
||||
foreach ($nomineeIds as $nomineeId) {
|
||||
Result::updateOrCreate([
|
||||
'voter_id' => Auth::id(),
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'position_id' => $positionId,
|
||||
'nominee_id' => $nomineeId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function validNominee($id, $position_id)
|
||||
{
|
||||
try{
|
||||
$nominee = Nominee::where('position_id', '=', $position_id)->whereIn('id',$id)->count();
|
||||
}catch(Exception $e){
|
||||
dd($id);
|
||||
}catch(\Exception $e){
|
||||
return 0;
|
||||
}
|
||||
return $nominee;
|
||||
}
|
||||
@@ -68,8 +90,8 @@ class VoteController extends Controller
|
||||
{
|
||||
try{
|
||||
return Position::where('id', $id)->count();
|
||||
}catch(Exception $e){
|
||||
dd($id);
|
||||
}catch(\Exception $e){
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +109,8 @@ class VoteController extends Controller
|
||||
$result['status'] = 'failed';
|
||||
$vote = $request->vote;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user vote on all position
|
||||
*/
|
||||
if (!$this->voteAllPosition($request)) {
|
||||
dd('test');
|
||||
$result['message'] = 'You must vote on all position.';
|
||||
if (!is_array($vote) || !count($vote)) {
|
||||
$result['message'] = 'You must vote on at least one position.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -103,19 +119,25 @@ class VoteController extends Controller
|
||||
* Check if position_id and nominee_id has a value
|
||||
*/
|
||||
// dd($vote,$value);
|
||||
if (empty($value['position_id']) || empty($value['nominee_id'])) {
|
||||
|
||||
dd($value,empty($value['position_id']),empty($value['nominee_id']));
|
||||
$result['message'] = 'You must vote on all position.';
|
||||
$positionId = $value['position_id'] ?? null;
|
||||
$nomineeIds = $value['nominee_id'] ?? null;
|
||||
if (empty($positionId)) {
|
||||
$result['message'] = 'Invalid Position.';
|
||||
return $result;
|
||||
}
|
||||
if (!is_array($nomineeIds) || !count($nomineeIds)) {
|
||||
// Skip empty positions; allow partial voting setups (UI may send all positions with empty arrays)
|
||||
continue;
|
||||
}
|
||||
if (count($nomineeIds) > 1) {
|
||||
$result['message'] = 'You can only vote for one nominee.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the Position you vote exists
|
||||
*/
|
||||
if (!$this->isPositionExist($value['position_id'])) {
|
||||
dd('test');
|
||||
if (!$this->isPositionExist($positionId)) {
|
||||
$result['message'] = 'Invalid Position.';
|
||||
return $result;
|
||||
}
|
||||
@@ -124,14 +146,28 @@ class VoteController extends Controller
|
||||
* Check if the Nominee you vote on certain position exists
|
||||
*/
|
||||
// elseif (!$this->validNominee($value['nominee_id'], $value['position_id'])) {
|
||||
elseif($this->validNominee($value['nominee_id'],$value['position_id']) <= 0){
|
||||
elseif($this->validNominee($nomineeIds, $positionId) <= 0){
|
||||
|
||||
$result['message'] = 'Invalid Nominee for '.Position::find($value['position_id'])->name.' position.';
|
||||
$result['message'] = 'Invalid Nominee for '.Position::find($positionId)->name.' position.';
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return ['status'=>'success'];
|
||||
// Ensure at least one actual vote exists
|
||||
$hasAny = false;
|
||||
foreach ($vote as $value) {
|
||||
$nomineeIds = $value['nominee_id'] ?? null;
|
||||
if (is_array($nomineeIds) && count($nomineeIds)) {
|
||||
$hasAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasAny) {
|
||||
$result['message'] = 'You must vote on at least one position.';
|
||||
return $result;
|
||||
}
|
||||
|
||||
return ['status' => 'success'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,21 @@ class AddController extends Controller
|
||||
public function __invoke (Request $request)
|
||||
{
|
||||
$this->validateRequest($request);
|
||||
$this->insertNominee($request);
|
||||
$nominee = $this->insertNominee($request);
|
||||
|
||||
activity()
|
||||
->performedOn($nominee)
|
||||
->withProperties([
|
||||
'nominee_id' => $nominee->id,
|
||||
'nominee_name' => $nominee->name,
|
||||
'election_id' => $nominee->election_id,
|
||||
'position_id' => $nominee->position_id,
|
||||
'partylist_id' => $nominee->partylist_id,
|
||||
'created_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("create nominee: {$nominee->name}");
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message'=> 'Nominee added successfully'
|
||||
@@ -39,7 +53,11 @@ class AddController extends Controller
|
||||
'unit' => 'required',
|
||||
'position_id' => 'required|exists:position,id',
|
||||
'partylist_id' => 'nullable|exists:partylist,id',
|
||||
'image' => 'nullable|image'
|
||||
'photo' => 'nullable|image',
|
||||
'umur' => 'nullable|integer|min:1|max:120',
|
||||
'jawatan_sekarang' => 'nullable|string|max:255',
|
||||
'experience' => 'nullable|string',
|
||||
'education' => 'nullable|string',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -53,9 +71,6 @@ class AddController extends Controller
|
||||
$nominee['photo'] = $imageData;
|
||||
}
|
||||
|
||||
Nominee::create($nominee);
|
||||
return response()->json(['message' => 'Photo updated successfully']);
|
||||
|
||||
|
||||
return Nominee::create($nominee);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,23 @@ use App\Nominee;
|
||||
|
||||
class DeleteController extends Controller
|
||||
{
|
||||
public function __invoke ($id)
|
||||
public function __invoke (Request $request, $id)
|
||||
{
|
||||
$nominee = Nominee::findOrFail($id);
|
||||
Util::deleteImage($nominee->image);
|
||||
$nominee->delete();
|
||||
|
||||
activity()
|
||||
->withProperties([
|
||||
'nominee_id' => $nominee->id,
|
||||
'nominee_name' => $nominee->name,
|
||||
'election_id' => $nominee->election_id,
|
||||
'deleted_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("delete nominee: {$nominee->name}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message'=> 'Nominee deleted successfully'
|
||||
|
||||
@@ -5,14 +5,21 @@ namespace App\Http\Controllers\API\v1\Nominee;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Nominee;
|
||||
|
||||
class GetController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
$nominee = \App\Nominee::where('election_id', Util::getCurrentElection())
|
||||
->orderBy('position_id')
|
||||
->get();
|
||||
return $nominee;
|
||||
$q = Nominee::query()
|
||||
->where('election_id', Util::getCurrentElection())
|
||||
->orderBy('position_id');
|
||||
|
||||
$positionId = request('position_id');
|
||||
if ($positionId !== null && $positionId !== '' && (int) $positionId !== 0) {
|
||||
$q->where('position_id', (int) $positionId);
|
||||
}
|
||||
|
||||
return $q->get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,20 @@ class UpdateController extends Controller
|
||||
public function __invoke (Request $request, $id)
|
||||
{
|
||||
$this->validateRequest($request, $id);
|
||||
$this->updateNominee($request, $id);
|
||||
$nominee = $this->updateNominee($request, $id);
|
||||
|
||||
activity()
|
||||
->performedOn($nominee)
|
||||
->withProperties([
|
||||
'nominee_id' => $nominee->id,
|
||||
'nominee_name' => $nominee->name,
|
||||
'election_id' => $nominee->election_id,
|
||||
'changed_keys' => array_keys($nominee->getChanges()),
|
||||
'updated_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("update nominee: {$nominee->name}");
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Nominee updated successfully'
|
||||
@@ -43,7 +56,7 @@ class UpdateController extends Controller
|
||||
|
||||
private function updateNominee ($request, $id)
|
||||
{
|
||||
$nominee = Nominee::find($id);
|
||||
$nominee = Nominee::findOrFail($id);
|
||||
|
||||
$nominee->name = $request->name;
|
||||
$nominee->unit = $request->unit;
|
||||
@@ -52,6 +65,8 @@ class UpdateController extends Controller
|
||||
$nominee->partylist_id = $request->partylist_id;
|
||||
$nominee->education = $request->education;
|
||||
$nominee->experience = $request->experience;
|
||||
$nominee->umur = $request->umur;
|
||||
$nominee->jawatan_sekarang = $request->jawatan_sekarang;
|
||||
|
||||
if ($request->hasFile('photo')) {
|
||||
$imageData = file_get_contents($request->file('photo'));
|
||||
@@ -59,6 +74,7 @@ class UpdateController extends Controller
|
||||
}
|
||||
|
||||
$nominee->save();
|
||||
return $nominee;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Services\PhysicalAttendanceGateCode;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PhysicalAttendanceGateController extends Controller
|
||||
{
|
||||
/**
|
||||
* Current rotating code for the registration counter display.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \App\Services\PhysicalAttendanceGateCode $gate
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function __invoke(Request $request, PhysicalAttendanceGateCode $gate)
|
||||
{
|
||||
$electionId = Util::getCurrentElection();
|
||||
|
||||
return response()->json($gate->currentPayload($electionId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Voter;
|
||||
|
||||
/**
|
||||
* Unauthenticated list of physical (Fizikal) attendees for roulette / display.
|
||||
*/
|
||||
class RouletteFizikalVotersController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
$electionId = Util::getCurrentElection();
|
||||
|
||||
$voters = Voter::query()
|
||||
->where('election_id', $electionId)
|
||||
->where('kehadiran', 1)
|
||||
->orderBy('no_anggota')
|
||||
->get(['id', 'name', 'no_anggota']);
|
||||
|
||||
return response()->json([
|
||||
'voters' => $voters,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,22 @@ class AddController extends Controller
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->validateRequest($request);
|
||||
$this->insertVoter($request);
|
||||
$voter = $this->insertVoter($request);
|
||||
|
||||
activity()
|
||||
->performedOn($voter)
|
||||
->withProperties([
|
||||
'election_id' => $voter->election_id,
|
||||
'voter_id' => $voter->id,
|
||||
'voter_name' => $voter->name,
|
||||
'no_kp' => $voter->no_kp,
|
||||
'no_anggota' => $voter->no_anggota,
|
||||
'unit' => $voter->unit,
|
||||
'created_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("create voter: {$voter->name}");
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Voter added successfully'
|
||||
@@ -24,7 +39,7 @@ class AddController extends Controller
|
||||
{
|
||||
$voter = $request->all();
|
||||
$voter['election_id'] = Util::getCurrentElection();
|
||||
Voter::create($voter);
|
||||
return Voter::create($voter);
|
||||
}
|
||||
|
||||
private function validateRequest($request)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Voter;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Voter;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Lists every distinct member (by no_kp) found anywhere in voter,
|
||||
* for picking who is applicable to the current election.
|
||||
*/
|
||||
class AllMembersCatalogController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
$electionId = Util::getCurrentElection();
|
||||
|
||||
$maxIds = DB::table('voter')
|
||||
->select(DB::raw('MAX(id) as id'))
|
||||
->whereNotNull('no_kp')
|
||||
->where('no_kp', '!=', '')
|
||||
->groupBy('no_kp')
|
||||
->pluck('id');
|
||||
|
||||
$rows = Voter::whereIn('id', $maxIds)
|
||||
->orderBy('name')
|
||||
->get(['no_kp', 'no_anggota', 'name', 'unit']);
|
||||
|
||||
$inCurrent = Voter::where('election_id', $electionId)
|
||||
->pluck('no_kp')
|
||||
->filter(function ($kp) {
|
||||
return $kp !== null && $kp !== '';
|
||||
})
|
||||
->flip();
|
||||
|
||||
$catalog = $rows->map(function ($row) use ($inCurrent) {
|
||||
return [
|
||||
'no_kp' => $row->no_kp,
|
||||
'no_anggota' => $row->no_anggota,
|
||||
'name' => $row->name,
|
||||
'unit' => $row->unit,
|
||||
'in_current_election' => $inCurrent->has($row->no_kp),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'election_id' => $electionId,
|
||||
'catalog' => $catalog->values()->all(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Http\Controllers\API\v1\Voter;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Services\PhysicalAttendanceGateCode;
|
||||
use App\Voter;
|
||||
|
||||
class AttendanceController extends Controller
|
||||
@@ -14,6 +16,7 @@ class AttendanceController extends Controller
|
||||
public function __invoke(Request $request, $id)
|
||||
{
|
||||
$this->validateRequest($request, $id);
|
||||
$this->assertPhysicalGateCode($request);
|
||||
$this->updateAttendance($request, $id);
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
@@ -23,7 +26,7 @@ class AttendanceController extends Controller
|
||||
|
||||
private function validateRequest($request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
$rules = [
|
||||
'name' => [
|
||||
'required',
|
||||
Rule::unique('voter')->ignore($id)->where(function($query){
|
||||
@@ -46,15 +49,72 @@ class AttendanceController extends Controller
|
||||
})
|
||||
],
|
||||
|
||||
'kehadiran' => 'required'
|
||||
'kehadiran' => 'required',
|
||||
];
|
||||
|
||||
]);
|
||||
if (config('physical_attendance_gate.enabled')) {
|
||||
$rules['physical_gate_code'] = 'required_if:kehadiran,1|string|max:64';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* When gate is enabled, Fizikal (kehadiran = 1) must match the rotating counter code.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
private function assertPhysicalGateCode(Request $request)
|
||||
{
|
||||
if (!config('physical_attendance_gate.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) $request->input('kehadiran') !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$gate = app(PhysicalAttendanceGateCode::class);
|
||||
if (!$gate->isConfigured()) {
|
||||
throw new HttpResponseException(response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Pengesahan kod fizikal tidak dikonfigurasi dengan betul.',
|
||||
], 503));
|
||||
}
|
||||
|
||||
if (!$gate->validates(Util::getCurrentElection(), (string) $request->input('physical_gate_code'))) {
|
||||
throw new HttpResponseException(response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod kaunter tidak sah atau telah tamat tempoh. Sila semak skrin pendaftaran dan cuba lagi.',
|
||||
], 422));
|
||||
}
|
||||
}
|
||||
|
||||
private function updateAttendance($request, $id)
|
||||
{
|
||||
$voter = $request->all();
|
||||
Voter::find($id)->update($voter);
|
||||
$voter = Voter::findOrFail($id);
|
||||
$voter->fill($request->only(['name', 'no_kp', 'no_anggota', 'unit', 'kehadiran']));
|
||||
|
||||
if ((int) $voter->kehadiran === 1) {
|
||||
$voter->fizikal_registration_verified_at = null;
|
||||
}
|
||||
|
||||
$voter->save();
|
||||
|
||||
$mode = ((int) $voter->kehadiran === 1) ? 'physical' : 'online';
|
||||
|
||||
activity()
|
||||
->performedOn($voter)
|
||||
->withProperties([
|
||||
'voter_id' => $voter->id,
|
||||
'voter_name' => $voter->name,
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'attendance_mode' => $mode,
|
||||
'ip' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
])
|
||||
->log("confirm attendance ({$mode}): {$voter->name}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,12 +5,105 @@ namespace App\Http\Controllers\API\v1\Voter;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Voter;
|
||||
|
||||
class GetController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return Voter::where('election_id', Util::getCurrentElection())->paginate(50);
|
||||
$electionId = Util::getCurrentElection();
|
||||
|
||||
$q = Voter::query()
|
||||
->where('election_id', $electionId)
|
||||
// Ensure we keep all voter columns when adding computed selects
|
||||
->select('voter.*')
|
||||
// Computed: whether voter has any vote record for this election
|
||||
->selectSub(function ($sq) use ($electionId) {
|
||||
$sq->from('result')
|
||||
->selectRaw('COUNT(*)')
|
||||
->whereColumn('result.voter_id', 'voter.id')
|
||||
->where('result.election_id', $electionId);
|
||||
}, 'votes_count');
|
||||
|
||||
// Computed: payout timestamps (paperless coupon replacement state)
|
||||
$q->selectSub(function ($sq) use ($electionId) {
|
||||
$sq->from('allowance_payouts')
|
||||
->selectRaw('MAX(paid_at)')
|
||||
->whereColumn('allowance_payouts.voter_id', 'voter.id')
|
||||
->where('allowance_payouts.election_id', $electionId)
|
||||
->where('allowance_payouts.method', 'cash')
|
||||
->where('allowance_payouts.status', 'paid');
|
||||
}, 'cash_paid_at');
|
||||
|
||||
$q->selectSub(function ($sq) use ($electionId) {
|
||||
$sq->from('allowance_payouts')
|
||||
->selectRaw('MAX(id)')
|
||||
->whereColumn('allowance_payouts.voter_id', 'voter.id')
|
||||
->where('allowance_payouts.election_id', $electionId)
|
||||
->where('allowance_payouts.method', 'cash')
|
||||
->where('allowance_payouts.status', 'paid');
|
||||
}, 'cash_payout_id');
|
||||
|
||||
$q->selectSub(function ($sq) use ($electionId) {
|
||||
$sq->from('allowance_payouts')
|
||||
->selectRaw('MAX(paid_at)')
|
||||
->whereColumn('allowance_payouts.voter_id', 'voter.id')
|
||||
->where('allowance_payouts.election_id', $electionId)
|
||||
->where('allowance_payouts.method', 'bank')
|
||||
->where('allowance_payouts.status', 'paid');
|
||||
}, 'bank_paid_at');
|
||||
|
||||
$q->selectSub(function ($sq) use ($electionId) {
|
||||
$sq->from('allowance_payouts')
|
||||
->selectRaw('MAX(id)')
|
||||
->whereColumn('allowance_payouts.voter_id', 'voter.id')
|
||||
->where('allowance_payouts.election_id', $electionId)
|
||||
->where('allowance_payouts.method', 'bank')
|
||||
->where('allowance_payouts.status', 'paid');
|
||||
}, 'bank_payout_id');
|
||||
|
||||
// Optional search filters
|
||||
$noAnggota = request('no_anggota');
|
||||
if ($noAnggota !== null && $noAnggota !== '') {
|
||||
$q->where('no_anggota', 'like', '%' . $noAnggota . '%');
|
||||
}
|
||||
|
||||
$noKp = request('no_kp');
|
||||
if ($noKp !== null && $noKp !== '') {
|
||||
$q->where('no_kp', 'like', '%' . $noKp . '%');
|
||||
}
|
||||
|
||||
$name = request('name');
|
||||
if ($name !== null && $name !== '') {
|
||||
$q->where('name', 'like', '%' . $name . '%');
|
||||
}
|
||||
|
||||
// Optional attendance filter: 1 = Fizikal, 2 = Maya
|
||||
$kehadiran = request('kehadiran');
|
||||
if ($kehadiran !== null && $kehadiran !== '') {
|
||||
if ($kehadiran === 'unset') {
|
||||
$q->where(function ($qq) {
|
||||
$qq->whereNull('kehadiran')->orWhere('kehadiran', 0);
|
||||
});
|
||||
} else {
|
||||
$k = (int) $kehadiran;
|
||||
if (in_array($k, [1, 2], true)) {
|
||||
$q->where('kehadiran', $k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fizikal registration queue: pending = fizikal but not admin-verified yet
|
||||
$fizikalReg = request('fizikal_reg');
|
||||
if ($fizikalReg !== null && $fizikalReg !== '') {
|
||||
if ($fizikalReg === 'pending') {
|
||||
$q->where('kehadiran', 1)->whereNull('fizikal_registration_verified_at');
|
||||
} elseif ($fizikalReg === 'verified') {
|
||||
$q->where('kehadiran', 1)->whereNotNull('fizikal_registration_verified_at');
|
||||
}
|
||||
}
|
||||
|
||||
return $q->paginate(50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,201 +16,249 @@ class LoginController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
//return response()->json(Auth::guard('user'));
|
||||
/* Request OTP */
|
||||
try {
|
||||
$currentDateTime = Carbon::now();
|
||||
|
||||
/* Request OTP */
|
||||
try{
|
||||
$currentDateTime = Carbon::now();
|
||||
$expired_at = Carbon::now()->addMinutes(config('onewaysms.minutes'));
|
||||
$created_at = $currentDateTime->toDateTimeString();
|
||||
|
||||
$expired_at = Carbon::now()->addMinutes(config('onewaysms.minutes'));
|
||||
$created_at = $currentDateTime->toDateTimeString();
|
||||
$voter = Voter::where('no_kp', $request->no_kp)->firstOrFail();
|
||||
|
||||
$voter = Voter::where('no_kp',$request->no_kp)->firstOrFail();
|
||||
if ($voter) {
|
||||
$expiration = $this->checkExpirationTAC($request->no_kp);
|
||||
|
||||
if($voter){
|
||||
$expiration = $this->checkExpirationTAC($request->no_kp);
|
||||
|
||||
if($expiration['notexpired'] == true){
|
||||
throw new \Exception('Expired TAC : ' . $expiration['remaining']);
|
||||
}
|
||||
|
||||
$sms_token = $this->getTokenSMS($voter->telefon,$currentDateTime);
|
||||
|
||||
if($sms_token == 'error'){
|
||||
throw new \Exception('error in getting the data from SMSToken');
|
||||
}
|
||||
|
||||
$user_otp = new UserOTP();
|
||||
$user_otp->nokp = $voter->no_kp;
|
||||
$user_otp->telefon = $voter->telefon;
|
||||
$user_otp->token = $sms_token;
|
||||
$user_otp->created_at = $created_at;
|
||||
$user_otp->expired_at = $expired_at;
|
||||
$user_otp->save();
|
||||
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'generated SMS Token',
|
||||
'notel' => $voter->telefon,
|
||||
'nokp' => $voter->no_kp,
|
||||
];
|
||||
|
||||
if (app()->environment(['local', 'development'])) {
|
||||
$response['debug_otp'] = $sms_token;
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
|
||||
}else{
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Does not found NO KP.'
|
||||
]);
|
||||
}
|
||||
}catch(Exception $e){
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Error: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkExpirationTAC($nokp) : Array{
|
||||
$currentDateTime = Carbon::now();
|
||||
|
||||
$expiretac = UserOTP::where('nokp',$nokp)
|
||||
->whereDate('created_at','=',$currentDateTime)
|
||||
->whereDate('expired_at','=',$currentDateTime)
|
||||
->whereTime('expired_at','>=',$currentDateTime->toTimeString())
|
||||
->whereTime('created_at','<=',$currentDateTime->toTimeString())
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if($expiretac){
|
||||
$remaining = $this->getRemainingTime($expiretac->expired_at);
|
||||
return ['notexpired' => true,'remaining' => $remaining];
|
||||
}else{
|
||||
return ['notexpired' => false];
|
||||
}
|
||||
}
|
||||
|
||||
public function verifyTAC(Request $request){
|
||||
|
||||
$authenticateOTP = $this->getTACModel($request->no_kp,$request->token);
|
||||
|
||||
if(!$authenticateOTP['condition']){
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => $authenticateOTP['message']
|
||||
]);
|
||||
}
|
||||
|
||||
if (Auth::guard('voter')->attempt(['no_kp' => $request->no_kp, 'password' => 'admin', 'election_id' => Util::getCurrentElection()])) {
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Login successfully.',
|
||||
'user' => Auth::guard('voter')->user(),
|
||||
'election_status' => Util::getElectionStatus(),
|
||||
'token' => Auth::guard('voter')->user()->createToken('My Token', ['vote'])->accessToken
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'No. KP tidak wujud. Atau sesi mengundi telah tamat'
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Login successfully.',
|
||||
]);
|
||||
}
|
||||
|
||||
private function getTACModel($no_kp,$token){
|
||||
$currentDateTime = Carbon::now();
|
||||
|
||||
$result = UserOTP::where('nokp',$no_kp)
|
||||
->whereDate('created_at','=',$currentDateTime)
|
||||
->whereDate('expired_at','=',$currentDateTime)
|
||||
->whereTime('expired_at','>=',$currentDateTime->toTimeString())
|
||||
->whereTime('created_at','<=',$currentDateTime->toTimeString())
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if(!$result){
|
||||
return ['condition' => false, 'message' => 'Token Expired'];
|
||||
}
|
||||
|
||||
if($result['token'] == $token){
|
||||
return ['condition' => true];
|
||||
}else{
|
||||
return ['condition' => false, 'message' => 'Invalid Token'];
|
||||
}
|
||||
}
|
||||
|
||||
private function getTokenSMS($tele,$date){
|
||||
$tac_token = $this->randum_number(6,2,false);
|
||||
if(env("APP_ENV") == 'production'){
|
||||
$mobileno = '6' . $tele;
|
||||
$messages = sprintf(config('onewaysms.message'),$tac_token,$date);
|
||||
|
||||
$parameter = [
|
||||
'apiusername' => env("ONEWAY_SMS_USERNAME"),
|
||||
'apipassword' => env("ONEWAY_SMS_PASSWORD"),
|
||||
'senderid' => env("ONEWAY_SMS_SENDERID",'INFO'),
|
||||
'mobileno' => $mobileno,
|
||||
'message'=> $messages,
|
||||
'languagetype'=> env("ONEWAY_SMS_LANG",1)
|
||||
];
|
||||
|
||||
$client = new Client(['verify' => false]);
|
||||
$status = $client->get('http://gateway.onewaysms.com.my:10001/api.aspx',[
|
||||
'query' => $parameter
|
||||
]);
|
||||
|
||||
if($status){
|
||||
return $tac_token;
|
||||
}else{
|
||||
return 'error';
|
||||
}
|
||||
|
||||
}else{
|
||||
return $tac_token;
|
||||
}
|
||||
}
|
||||
|
||||
private function randum_number($len = 6,$dup = 1, $sort = false){
|
||||
if($dup < 1)
|
||||
throw new \InvalidArgumentException('Second argument is < 1');
|
||||
|
||||
$num = range(0,9);
|
||||
shuffle($num);
|
||||
|
||||
$num = array_slice($num, 0, ($len-$dup) + 1);
|
||||
|
||||
if($dup > 0){
|
||||
$k = array_rand($num, 1);
|
||||
for($i=0;$i<($dup-1);$i++)
|
||||
{
|
||||
$num[] = $num[$k];
|
||||
if ($expiration['notexpired'] == true) {
|
||||
throw new \Exception('Expired TAC : ' . $expiration['remaining']);
|
||||
}
|
||||
|
||||
$sms_token = $this->getTokenSMS($voter->telefon, $currentDateTime);
|
||||
|
||||
if ($sms_token == 'error') {
|
||||
throw new \Exception('error in getting the data from SMSToken');
|
||||
}
|
||||
|
||||
$user_otp = new UserOTP();
|
||||
$user_otp->nokp = $voter->no_kp;
|
||||
$user_otp->telefon = $voter->telefon;
|
||||
$user_otp->token = $sms_token;
|
||||
$user_otp->created_at = $created_at;
|
||||
$user_otp->expired_at = $expired_at;
|
||||
$user_otp->save();
|
||||
|
||||
activity()
|
||||
->performedOn($voter)
|
||||
->withProperties([
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'voter_id' => $voter->id,
|
||||
'voter_name' => $voter->name,
|
||||
'no_kp' => $voter->no_kp,
|
||||
'telefon' => $voter->telefon,
|
||||
'otp_expires_at' => $expired_at->toDateTimeString(),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('voter request tac');
|
||||
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'generated SMS Token',
|
||||
'notel' => $voter->telefon,
|
||||
'nokp' => $voter->no_kp,
|
||||
];
|
||||
|
||||
if (app()->environment(['local', 'development'])) {
|
||||
$response['debug_otp'] = $sms_token;
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
if($sort){
|
||||
sort($num);
|
||||
}
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Does not found NO KP.',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
activity()
|
||||
->withProperties([
|
||||
'no_kp' => (string) $request->input('no_kp'),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'error' => $e->getMessage(),
|
||||
])
|
||||
->log('voter request tac failed');
|
||||
|
||||
return implode('',$num);
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Error: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getRemainingTime($expired_at){
|
||||
public function checkExpirationTAC($nokp): array
|
||||
{
|
||||
$currentDateTime = Carbon::now();
|
||||
$expiredAt = Carbon::parse($expired_at);
|
||||
|
||||
$remainingSeconds = $currentDateTime->diffInSeconds($expiredAt);
|
||||
$expiretac = UserOTP::where('nokp', $nokp)
|
||||
->whereDate('created_at', '=', $currentDateTime)
|
||||
->whereDate('expired_at', '=', $currentDateTime)
|
||||
->whereTime('expired_at', '>=', $currentDateTime->toTimeString())
|
||||
->whereTime('created_at', '<=', $currentDateTime->toTimeString())
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
$remainingFormatted = gmdate('H:i:s', $remainingSeconds);
|
||||
if ($expiretac) {
|
||||
$remaining = $this->getRemainingTime($expiretac->expired_at);
|
||||
|
||||
return $remainingFormatted;
|
||||
}
|
||||
return ['notexpired' => true, 'remaining' => $remaining];
|
||||
}
|
||||
|
||||
return ['notexpired' => false];
|
||||
}
|
||||
|
||||
public function verifyTAC(Request $request)
|
||||
{
|
||||
$authenticateOTP = $this->getTACModel($request->no_kp, $request->token);
|
||||
|
||||
if (! $authenticateOTP['condition']) {
|
||||
activity()
|
||||
->withProperties([
|
||||
'no_kp' => (string) $request->input('no_kp'),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'reason' => $authenticateOTP['message'] ?? 'invalid',
|
||||
])
|
||||
->log('voter verify tac failed');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => $authenticateOTP['message'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (Auth::guard('voter')->attempt(['no_kp' => $request->no_kp, 'password' => 'admin', 'election_id' => Util::getCurrentElection()])) {
|
||||
$user = Auth::guard('voter')->user();
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'voter_id' => $user->id,
|
||||
'voter_name' => $user->name,
|
||||
'no_kp' => $user->no_kp,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("voter login: {$user->name}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Login successfully.',
|
||||
'user' => Auth::guard('voter')->user(),
|
||||
'election_status' => Util::getElectionStatus(),
|
||||
'token' => Auth::guard('voter')->user()->createToken('My Token', ['vote'])->accessToken,
|
||||
]);
|
||||
}
|
||||
|
||||
activity()
|
||||
->withProperties([
|
||||
'no_kp' => (string) $request->input('no_kp'),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('voter login failed');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'No. KP tidak wujud. Atau sesi mengundi telah tamat',
|
||||
]);
|
||||
}
|
||||
|
||||
private function getTACModel($no_kp, $token)
|
||||
{
|
||||
$currentDateTime = Carbon::now();
|
||||
|
||||
$result = UserOTP::where('nokp', $no_kp)
|
||||
->whereDate('created_at', '=', $currentDateTime)
|
||||
->whereDate('expired_at', '=', $currentDateTime)
|
||||
->whereTime('expired_at', '>=', $currentDateTime->toTimeString())
|
||||
->whereTime('created_at', '<=', $currentDateTime->toTimeString())
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (! $result) {
|
||||
return ['condition' => false, 'message' => 'Token Expired'];
|
||||
}
|
||||
|
||||
if ($result['token'] == $token) {
|
||||
return ['condition' => true];
|
||||
}
|
||||
|
||||
return ['condition' => false, 'message' => 'Invalid Token'];
|
||||
}
|
||||
|
||||
private function getTokenSMS($tele, $date)
|
||||
{
|
||||
$tac_token = $this->randum_number(6, 2, false);
|
||||
if (env('APP_ENV') == 'production') {
|
||||
$mobileno = '6' . $tele;
|
||||
$messages = sprintf(config('onewaysms.message'), $tac_token, $date);
|
||||
|
||||
$parameter = [
|
||||
'apiusername' => env('ONEWAY_SMS_USERNAME'),
|
||||
'apipassword' => env('ONEWAY_SMS_PASSWORD'),
|
||||
'senderid' => env('ONEWAY_SMS_SENDERID', 'INFO'),
|
||||
'mobileno' => $mobileno,
|
||||
'message' => $messages,
|
||||
'languagetype' => env('ONEWAY_SMS_LANG', 1),
|
||||
];
|
||||
|
||||
$client = new Client(['verify' => false]);
|
||||
$status = $client->get('http://gateway.onewaysms.com.my:10001/api.aspx', [
|
||||
'query' => $parameter,
|
||||
]);
|
||||
|
||||
if ($status) {
|
||||
return $tac_token;
|
||||
}
|
||||
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return $tac_token;
|
||||
}
|
||||
|
||||
private function randum_number($len = 6, $dup = 1, $sort = false)
|
||||
{
|
||||
if ($dup < 1) {
|
||||
throw new \InvalidArgumentException('Second argument is < 1');
|
||||
}
|
||||
|
||||
$num = range(0, 9);
|
||||
shuffle($num);
|
||||
|
||||
$num = array_slice($num, 0, ($len - $dup) + 1);
|
||||
|
||||
if ($dup > 0) {
|
||||
$k = array_rand($num, 1);
|
||||
for ($i = 0; $i < ($dup - 1); $i++) {
|
||||
$num[] = $num[$k];
|
||||
}
|
||||
}
|
||||
|
||||
if ($sort) {
|
||||
sort($num);
|
||||
}
|
||||
|
||||
return implode('', $num);
|
||||
}
|
||||
|
||||
private function getRemainingTime($expired_at)
|
||||
{
|
||||
$currentDateTime = Carbon::now();
|
||||
$expiredAt = Carbon::parse($expired_at);
|
||||
|
||||
$remainingSeconds = $currentDateTime->diffInSeconds($expiredAt);
|
||||
|
||||
return gmdate('H:i:s', $remainingSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Voter;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LogoutController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user) {
|
||||
activity()
|
||||
->performedOn($user)
|
||||
->withProperties([
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'voter_id' => $user->id,
|
||||
'voter_name' => $user->name,
|
||||
'no_kp' => $user->no_kp,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("voter logout: {$user->name}");
|
||||
|
||||
// Revoke current access token if using Passport.
|
||||
try {
|
||||
if (method_exists($user, 'token') && $user->token()) {
|
||||
$user->token()->revoke();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Logging should not block logout.
|
||||
}
|
||||
} else {
|
||||
activity()
|
||||
->withProperties([
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('voter logout');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Logout successfully',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Voter;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Voter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Applies admin selection: voters ticked exist for current election (copied from latest row per no_kp);
|
||||
* unticked are removed from the current election only.
|
||||
*/
|
||||
class SyncApplicableVotersController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'selected_no_kp' => 'present|array',
|
||||
'selected_no_kp.*' => 'nullable|string|max:60',
|
||||
]);
|
||||
|
||||
$electionId = Util::getCurrentElection();
|
||||
$selected = collect($request->input('selected_no_kp', []))
|
||||
->map(function ($v) {
|
||||
return trim((string) $v);
|
||||
})
|
||||
->filter(function ($v) {
|
||||
return $v !== '';
|
||||
})
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$stats = [
|
||||
'deleted' => 0,
|
||||
'added' => 0,
|
||||
'selected_count' => $selected->count(),
|
||||
];
|
||||
|
||||
DB::transaction(function () use ($electionId, $selected) {
|
||||
$selectedSet = $selected->flip();
|
||||
|
||||
$current = Voter::where('election_id', $electionId)->get();
|
||||
foreach ($current as $voter) {
|
||||
$kp = $voter->no_kp;
|
||||
if ($kp === null || $kp === '') {
|
||||
continue;
|
||||
}
|
||||
if (!$selectedSet->has($kp)) {
|
||||
$voter->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$existingKp = Voter::where('election_id', $electionId)->pluck('no_kp')->all();
|
||||
$existingFlip = array_flip($existingKp);
|
||||
|
||||
foreach ($selected as $noKp) {
|
||||
if (isset($existingFlip[$noKp])) {
|
||||
continue;
|
||||
}
|
||||
$template = Voter::where('no_kp', $noKp)->orderBy('id', 'desc')->first();
|
||||
if (!$template) {
|
||||
continue;
|
||||
}
|
||||
$new = $template->replicate();
|
||||
$new->election_id = $electionId;
|
||||
$new->kehadiran = null;
|
||||
$new->fizikal_registration_verified_at = null;
|
||||
$new->persetujuan = null;
|
||||
$new->status_penyata = 'DRAF';
|
||||
$new->tarikh_sah = null;
|
||||
$new->cadangan = null;
|
||||
$new->save();
|
||||
}
|
||||
});
|
||||
|
||||
// Log after transaction succeeds (no joins; data from request & current election).
|
||||
activity()
|
||||
->withProperties([
|
||||
'election_id' => $electionId,
|
||||
'selected_count' => $selected->count(),
|
||||
'selected_no_kp' => $selected->take(50)->values()->all(),
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log('sync applicable voters');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Senarai pengundi untuk pilihan raya semasa dikemas kini.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,24 @@ class UpdateController extends Controller
|
||||
|
||||
private function updateVoter($request, $id)
|
||||
{
|
||||
$voter = $request->all();
|
||||
Voter::find($id)->update($voter);
|
||||
$voter = Voter::findOrFail($id);
|
||||
|
||||
$old = $voter->toArray();
|
||||
$voter->fill($request->all());
|
||||
$voter->save();
|
||||
|
||||
$dirtyKeys = array_keys($voter->getChanges());
|
||||
|
||||
activity()
|
||||
->performedOn($voter)
|
||||
->withProperties([
|
||||
'voter_id' => $voter->id,
|
||||
'voter_name' => $voter->name,
|
||||
'election_id' => Util::getCurrentElection(),
|
||||
'changed_keys' => $dirtyKeys,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("update voter: {$voter->name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API\v1\Voter;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Util;
|
||||
use App\Voter;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VerifyFizikalRegistrationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Admin confirms physical registration so the voter may vote (fizikal_registration_verified_at).
|
||||
*/
|
||||
public function __invoke(Request $request, $id)
|
||||
{
|
||||
$electionId = Util::getCurrentElection();
|
||||
|
||||
$voter = Voter::where('election_id', $electionId)
|
||||
->where('id', $id)
|
||||
->firstOrFail();
|
||||
|
||||
if ((int) $voter->kehadiran !== 1) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Pengesahan pendaftaran fizikal hanya untuk pengundi Fizikal.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($voter->fizikal_registration_verified_at !== null) {
|
||||
activity()
|
||||
->performedOn($voter)
|
||||
->withProperties([
|
||||
'election_id' => $electionId,
|
||||
'voter_id' => $voter->id,
|
||||
'voter_name' => $voter->name,
|
||||
'already_verified' => true,
|
||||
'verified_at' => (string) $voter->fizikal_registration_verified_at,
|
||||
'verified_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("verify fizikal registration: {$voter->name}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Pendaftaran telah disahkan sebelum ini.',
|
||||
'voter' => $voter,
|
||||
]);
|
||||
}
|
||||
|
||||
$voter->fizikal_registration_verified_at = now();
|
||||
$voter->save();
|
||||
|
||||
activity()
|
||||
->performedOn($voter)
|
||||
->withProperties([
|
||||
'election_id' => $electionId,
|
||||
'voter_id' => $voter->id,
|
||||
'voter_name' => $voter->name,
|
||||
'already_verified' => false,
|
||||
'verified_at' => (string) $voter->fizikal_registration_verified_at,
|
||||
'verified_by_admin_id' => $request->user() ? (int) $request->user()->id : null,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
])
|
||||
->log("verify fizikal registration: {$voter->name}");
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Pendaftaran fizikal telah disahkan.',
|
||||
'voter' => $voter->fresh(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,9 @@ class Kernel extends HttpKernel
|
||||
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
|
||||
'isvoted' => \App\Http\Middleware\IsVotedMiddleware::class,
|
||||
'main_admin' => \App\Http\Middleware\MainAdminMiddleware::class,
|
||||
'attendance_committee_role' => \App\Http\Middleware\AttendanceCommitteeRoleMiddleware::class,
|
||||
'has_voted' => \App\Http\Middleware\HasVotedMiddleware::class,
|
||||
'physical_gate_display' => \App\Http\Middleware\PhysicalGateDisplayKeyMiddleware::class,
|
||||
'fizikal_registration_verified' => \App\Http\Middleware\FizikalRegistrationVerifiedMiddleware::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Role 3 = Jawatankuasa Kehadiran: only counter/kehadiran-related APIs.
|
||||
* Other roles pass through unchanged.
|
||||
*/
|
||||
class AttendanceCommitteeRoleMiddleware
|
||||
{
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user || (int) $user->role !== 3) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($this->attendanceCommitteeMayAccess($request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Akses ditolak. Akaun ini hanya untuk halaman kehadiran.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return bool
|
||||
*/
|
||||
private function attendanceCommitteeMayAccess($request)
|
||||
{
|
||||
if ($request->isMethod('get') && $request->is('api/v1/admin/information')) {
|
||||
return true;
|
||||
}
|
||||
if ($request->isMethod('get') && $request->is('api/v1/admin/logout')) {
|
||||
return true;
|
||||
}
|
||||
if ($request->isMethod('post') && $request->is('api/v1/admin/impersonate/leave')) {
|
||||
return true;
|
||||
}
|
||||
if ($request->isMethod('get') && $request->is('api/v1/voter')) {
|
||||
return true;
|
||||
}
|
||||
if ($request->isMethod('post') && $request->is('api/v1/voter/*/verify-fizikal-registration')) {
|
||||
return true;
|
||||
}
|
||||
if ($request->is('api/v1/allowance/*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class ElectionMiddleware
|
||||
if ($this->isElection($request)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'You can not add, update, or delete during the election.'
|
||||
'message' => 'Anda tidak boleh menambah, mengemas kini, atau menghapus semasa sesi mengundi.'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,21 @@ class ElectionMiddleware
|
||||
|
||||
private function isElection($request)
|
||||
{
|
||||
return !$request->isMethod('get') && Util::getElectionStatus() == 2 && !$request->is('api/v1/election/*');
|
||||
return
|
||||
!$request->isMethod('get')
|
||||
&& Util::getElectionStatus() == 2
|
||||
&& !$request->is('api/v1/election/*')
|
||||
// Allow paperless allowance flows during election (cash counter needs this)
|
||||
&& !$request->is('api/v1/allowance/*')
|
||||
// Allow admin to update voter info during election (edit screen uses PUT /api/v1/voter/{id})
|
||||
&& !($request->is('api/v1/voter/*') && $request->isMethod('put'))
|
||||
// Allow admin to add voter during election (manual add uses POST /api/v1/voter)
|
||||
&& !($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'))
|
||||
&& !$request->is('api/v1/voter/*/verify-fizikal-registration')
|
||||
// Pentadbir utama: urus akaun (tambah/kemaskini/padam) semasa mengundi
|
||||
&& !$request->is('api/v1/admin')
|
||||
&& !$request->is('api/v1/admin/*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Voter;
|
||||
use Closure;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class FizikalRegistrationVerifiedMiddleware
|
||||
{
|
||||
/**
|
||||
* Block voting until fizikal attendance is confirmed by admin (kehadiran = 1 + verified_at set).
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user instanceof Voter) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Sesi tidak sah.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if (!$user->isFizikalRegistrationVerifiedForVoting()) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Pendaftaran fizikal anda belum disahkan oleh pentadbir. Sila tunggu pengesahan di kaunter.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class PhysicalGateDisplayKeyMiddleware
|
||||
{
|
||||
/**
|
||||
* Only clients that know the display key may read the current gate code.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (!config('physical_attendance_gate.enabled')) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Ciri kod kaunter fizikal tidak diaktifkan.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$expected = (string) config('physical_attendance_gate.display_key');
|
||||
if ($expected === '') {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Kod paparan kaunter belum dikonfigurasi.',
|
||||
], 503);
|
||||
}
|
||||
|
||||
$provided = (string) $request->header('X-Physical-Gate-Display-Key', '');
|
||||
if ($provided === '' && $request->bearerToken()) {
|
||||
$provided = (string) $request->bearerToken();
|
||||
}
|
||||
|
||||
if ($provided === '' || !hash_equals($expected, $provided)) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'Akses ditolak.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$service = app(\App\Services\PhysicalAttendanceGateCode::class);
|
||||
if (!$service->isConfigured()) {
|
||||
return response()->json([
|
||||
'status' => 'failed',
|
||||
'message' => 'PHYSICAL_ATTENDANCE_GATE_SECRET belum ditetapkan.',
|
||||
], 503);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -8,7 +8,6 @@ class Nominee extends Model
|
||||
{
|
||||
protected $table = 'nominee';
|
||||
protected $fillable = [
|
||||
|
||||
'name',
|
||||
'unit',
|
||||
'no_anggota',
|
||||
@@ -18,6 +17,8 @@ class Nominee extends Model
|
||||
'experience',
|
||||
'education',
|
||||
'photo',
|
||||
'umur',
|
||||
'jawatan_sekarang',
|
||||
];
|
||||
public $timestamp = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class PhysicalAttendanceGateCode
|
||||
{
|
||||
/** @var string */
|
||||
protected $secret;
|
||||
|
||||
/** @var int */
|
||||
protected $period;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->secret = (string) config('physical_attendance_gate.secret');
|
||||
$this->period = max(30, (int) config('physical_attendance_gate.period_seconds', 60));
|
||||
}
|
||||
|
||||
public function isConfigured()
|
||||
{
|
||||
return $this->secret !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Current code and countdown for the counter display.
|
||||
*
|
||||
* @param int $electionId
|
||||
* @return array{code:string,seconds_remaining:int,period_seconds:int}
|
||||
*/
|
||||
public function currentPayload($electionId)
|
||||
{
|
||||
$slot = $this->currentSlot();
|
||||
$code = $this->codeForSlot((int) $electionId, $slot);
|
||||
$expiresAt = ($slot + 1) * $this->period;
|
||||
$secondsRemaining = max(0, $expiresAt - time());
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'seconds_remaining' => $secondsRemaining,
|
||||
'period_seconds' => $this->period,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept current, previous, or next slot to tolerate minor clock skew.
|
||||
*
|
||||
* @param int $electionId
|
||||
* @param string $input
|
||||
* @return bool
|
||||
*/
|
||||
public function validates($electionId, $input)
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalized = strtoupper(preg_replace('/\s+/', '', (string) $input));
|
||||
|
||||
if (strlen($normalized) < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$slot = $this->currentSlot();
|
||||
|
||||
foreach ([$slot - 1, $slot, $slot + 1] as $s) {
|
||||
if ($s < 0) {
|
||||
continue;
|
||||
}
|
||||
if (hash_equals($this->codeForSlot((int) $electionId, $s), $normalized)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function currentSlot()
|
||||
{
|
||||
return intdiv(time(), $this->period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic code for one election and time slot.
|
||||
*
|
||||
* @param int $electionId
|
||||
* @param int $slot
|
||||
* @return string
|
||||
*/
|
||||
protected function codeForSlot($electionId, $slot)
|
||||
{
|
||||
$material = (string) $electionId.'|'.(string) $slot;
|
||||
$hex = hash_hmac('sha256', $material, $this->secret);
|
||||
|
||||
return strtoupper(substr($hex, 0, 8));
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -5,10 +5,11 @@ namespace App;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Lab404\Impersonate\Models\Impersonate;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, Notifiable;
|
||||
use HasApiTokens, Notifiable, Impersonate;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -27,4 +28,16 @@ class User extends Authenticatable
|
||||
protected $hidden = [
|
||||
'password', 'remember_token',
|
||||
];
|
||||
|
||||
public function canImpersonate()
|
||||
{
|
||||
// Limit to main admin to avoid privilege escalation.
|
||||
return (int) $this->id === 1;
|
||||
}
|
||||
|
||||
public function canBeImpersonated()
|
||||
{
|
||||
// Do not allow impersonating main admin.
|
||||
return (int) $this->id !== 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,22 @@ class Voter extends Authenticatable
|
||||
use HasApiTokens;
|
||||
|
||||
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','tergempar','barangan','peribadi','berjamin_yuran','roadtax','pelbagai'];
|
||||
|
||||
protected $casts = [
|
||||
'fizikal_registration_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Fizikal attendees must be counter-verified by admin; Maya does not use this gate.
|
||||
*/
|
||||
public function isFizikalRegistrationVerifiedForVoting(): bool
|
||||
{
|
||||
if ((int) $this->kehadiran !== 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->fizikal_registration_verified_at !== null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user