$data */ public function create(User $actor, array $data): Application { return DB::transaction(function () use ($actor, $data): Application { $election = Election::query()->findOrFail($data['election_id']); $this->postCloseNoteService->requireIfClosed($election, $data['note'] ?? null); /** @var PusatMengundi $pusat */ $pusat = PusatMengundi::query()->findOrFail($data['pusat_mengundi_id']); if ((int) $pusat->election_id !== (int) $election->id) { throw ValidationException::withMessages([ 'pusat_mengundi_id' => 'Pusat Mengundi tidak berada dalam pilihanraya yang sama.', ]); } $application = Application::query()->create([ 'election_id' => $election->id, 'pusat_mengundi_id' => $pusat->id, 'selected_ktm_assignment_id' => $data['selected_ktm_assignment_id'] ?? null, 'public_uuid' => (string) Str::uuid(), 'source' => 'admin_manual', 'status' => 'submitted', 'name' => $data['name'], 'ic_number' => $this->normalizeIc($data['ic_number']), 'phone_number' => $data['phone_number'], 'email' => $data['email'] ?? null, 'address' => $data['address'] ?? null, 'requested_position_id' => $data['requested_position_id'], 'bank_name' => $data['bank_name'] ?? null, 'bank_account_number' => $data['bank_account_number'] ?? null, 'created_by_user_id' => $actor->id, ]); $application->statusHistories()->create([ 'from_status' => null, 'to_status' => 'submitted', 'changed_by_user_id' => $actor->id, 'note' => $data['note'] ?? 'Manual entry by Admin.', ]); BankVerification::query()->firstOrCreate( ['application_id' => $application->id], ['status' => 'pending'], ); $this->postCloseNoteService->recordIfClosed($election, $application, $actor, $data['note'] ?? null, 'admin_manual_entry_after_close'); activity('admin_management') ->performedOn($application) ->causedBy($actor) ->withProperties(['source' => 'admin_manual']) ->log('application_created_by_admin'); return $application; }); } /** * @param array $data */ public function update(Application $application, User $actor, array $data): Application { return DB::transaction(function () use ($application, $actor, $data): Application { $application->loadMissing('election'); $election = $application->election; if (! $election instanceof Election) { throw ValidationException::withMessages([ 'election_id' => 'Pilihanraya permohonan tidak sah.', ]); } $this->postCloseNoteService->requireIfClosed($election, $data['note'] ?? null); $before = $application->only([ 'pusat_mengundi_id', 'selected_ktm_assignment_id', 'name', 'ic_number', 'phone_number', 'email', 'address', 'requested_position_id', 'approved_position_id', 'bank_name', 'bank_account_number', 'status', ]); $application->forceFill([ 'pusat_mengundi_id' => $data['pusat_mengundi_id'], 'selected_ktm_assignment_id' => $data['selected_ktm_assignment_id'] ?? null, 'name' => $data['name'], 'ic_number' => $this->normalizeIc($data['ic_number']), 'phone_number' => $data['phone_number'], 'email' => $data['email'] ?? null, 'address' => $data['address'] ?? null, 'requested_position_id' => $data['requested_position_id'], 'approved_position_id' => $data['approved_position_id'] ?? null, 'bank_name' => $data['bank_name'] ?? null, 'bank_account_number' => $data['bank_account_number'] ?? null, 'status' => $data['status'], ])->save(); $after = $application->only(array_keys($before)); $application->statusHistories()->create([ 'from_status' => $before['status'], 'to_status' => $application->status, 'changed_by_user_id' => $actor->id, 'note' => $data['note'] ?? 'Application updated by Admin.', 'metadata' => [ 'before' => $before, 'after' => $after, ], ]); $this->postCloseNoteService->recordIfClosed($election, $application, $actor, $data['note'] ?? null, 'admin_application_edit_after_close'); activity('admin_management') ->performedOn($application) ->causedBy($actor) ->withProperties(['before' => $before, 'after' => $after]) ->log('application_updated_by_admin'); return $application; }); } /** * @param array $data */ public function assign(Application $application, User $actor, array $data): StaffAssignment { return DB::transaction(function () use ($application, $actor, $data): StaffAssignment { $application->loadMissing('election'); $election = $application->election; if (! $election instanceof Election) { throw ValidationException::withMessages([ 'election_id' => 'Pilihanraya permohonan tidak sah.', ]); } $this->postCloseNoteService->requireIfClosed($election, $data['note'] ?? null); /** @var Position $position */ $position = Position::query()->findOrFail($data['position_id']); /** @var PusatMengundi $pusat */ $pusat = PusatMengundi::query()->findOrFail($data['pusat_mengundi_id']); if ((int) $pusat->election_id !== (int) $election->id) { throw ValidationException::withMessages([ 'pusat_mengundi_id' => 'Pusat Mengundi tidak berada dalam pilihanraya yang sama.', ]); } $reportsToAssignmentId = $data['reports_to_assignment_id'] ?? null; $saluranMengundiId = $data['saluran_mengundi_id'] ?? null; if ($saluranMengundiId) { /** @var SaluranMengundi $saluran */ $saluran = SaluranMengundi::query()->findOrFail($saluranMengundiId); if ((int) $saluran->pusat_mengundi_id !== (int) $pusat->id) { throw ValidationException::withMessages([ 'saluran_mengundi_id' => 'Saluran Mengundi tidak berada di bawah Pusat Mengundi yang dipilih.', ]); } } if ($reportsToAssignmentId) { /** @var StaffAssignment $ktmAssignment */ $ktmAssignment = StaffAssignment::query() ->with('position') ->whereKey($reportsToAssignmentId) ->where('election_id', $election->id) ->where('pusat_mengundi_id', $pusat->id) ->where('status', 'active') ->firstOrFail(); $ktmPosition = $ktmAssignment->position; if (! $ktmPosition instanceof Position || $ktmPosition->code !== 'KTM') { throw ValidationException::withMessages([ 'reports_to_assignment_id' => 'Ketua rujukan mestilah tugasan KTM aktif.', ]); } $saluranMengundiId = $ktmAssignment->saluran_mengundi_id; } $previousStatus = $application->status; $previousPositionId = $application->approved_position_id ?: $application->requested_position_id; $application->forceFill([ 'pusat_mengundi_id' => $pusat->id, 'selected_ktm_assignment_id' => $reportsToAssignmentId, 'approved_position_id' => $position->id, 'approved_by_user_id' => $actor->id, 'approved_at' => now(), 'status' => 'assigned', ])->save(); $assignment = StaffAssignment::query()->updateOrCreate( ['application_id' => $application->id], [ 'election_id' => $election->id, 'user_id' => $application->user_id, 'position_id' => $position->id, 'pusat_mengundi_id' => $pusat->id, 'saluran_mengundi_id' => $saluranMengundiId, 'reports_to_assignment_id' => $reportsToAssignmentId, 'status' => $data['status'] ?? 'active', 'assigned_by_user_id' => $actor->id, 'assigned_at' => now(), 'source' => 'admin_direct', ], ); $application->statusHistories()->create([ 'from_status' => $previousStatus, 'to_status' => 'assigned', 'changed_by_user_id' => $actor->id, 'note' => $data['note'] ?? 'Direct assignment by Admin.', 'metadata' => [ 'approved_position_id' => $position->id, 'previous_position_id' => $previousPositionId, 'pusat_mengundi_id' => $pusat->id, 'saluran_mengundi_id' => $saluranMengundiId, 'reports_to_assignment_id' => $reportsToAssignmentId, ], ]); $assignment->histories()->create([ 'from_position_id' => $previousPositionId, 'to_position_id' => $position->id, 'to_pusat_mengundi_id' => $pusat->id, 'to_saluran_mengundi_id' => $saluranMengundiId, 'to_reports_to_assignment_id' => $reportsToAssignmentId, 'changed_by_user_id' => $actor->id, 'note' => $data['note'] ?? 'Direct assignment by Admin.', ]); $this->operationalAccountService->provisionForApplication($application, $position); $this->postCloseNoteService->recordIfClosed($election, $assignment, $actor, $data['note'] ?? null, 'admin_assignment_after_close'); activity('admin_management') ->performedOn($assignment) ->causedBy($actor) ->withProperties([ 'application_id' => $application->id, 'position_code' => $position->code, 'pusat_mengundi_id' => $pusat->id, 'saluran_mengundi_id' => $saluranMengundiId, 'reports_to_assignment_id' => $reportsToAssignmentId, ]) ->log('staff_assigned_by_admin'); return $assignment; }); } private function normalizeIc(mixed $icNumber): string { return preg_replace('/\D+/', '', (string) $icNumber) ?? ''; } }