first
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Management;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\BankVerification;
|
||||
use App\Models\Election;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Admin\OperationalAccountService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AdminApplicationManagementService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminPostCloseNoteService $postCloseNoteService,
|
||||
private readonly OperationalAccountService $operationalAccountService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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) ?? '';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Management;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Election;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Admin\OperationalAccountService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AdminAssignmentManagementService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminPostCloseNoteService $postCloseNoteService,
|
||||
private readonly OperationalAccountService $operationalAccountService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function update(StaffAssignment $assignment, User $actor, array $data): StaffAssignment
|
||||
{
|
||||
return DB::transaction(function () use ($assignment, $actor, $data): StaffAssignment {
|
||||
$assignment->loadMissing(['election', 'position']);
|
||||
$election = $assignment->election;
|
||||
if (! $election instanceof Election) {
|
||||
throw ValidationException::withMessages([
|
||||
'election_id' => 'Pilihanraya tugasan 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.',
|
||||
]);
|
||||
}
|
||||
|
||||
$before = [
|
||||
'position_id' => $assignment->position_id,
|
||||
'pusat_mengundi_id' => $assignment->pusat_mengundi_id,
|
||||
'saluran_mengundi_id' => $assignment->saluran_mengundi_id,
|
||||
'reports_to_assignment_id' => $assignment->reports_to_assignment_id,
|
||||
'status' => $assignment->status,
|
||||
];
|
||||
|
||||
$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.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) $ktmAssignment->id === (int) $assignment->id) {
|
||||
throw ValidationException::withMessages([
|
||||
'reports_to_assignment_id' => 'KTM tidak boleh melapor kepada tugasan sendiri.',
|
||||
]);
|
||||
}
|
||||
|
||||
$saluranMengundiId = $ktmAssignment->saluran_mengundi_id;
|
||||
}
|
||||
|
||||
$assignment->forceFill([
|
||||
'position_id' => $position->id,
|
||||
'pusat_mengundi_id' => $pusat->id,
|
||||
'saluran_mengundi_id' => $saluranMengundiId,
|
||||
'reports_to_assignment_id' => $reportsToAssignmentId,
|
||||
'status' => $data['status'],
|
||||
'assigned_by_user_id' => $actor->id,
|
||||
'assigned_at' => now(),
|
||||
'source' => 'admin_update',
|
||||
])->save();
|
||||
|
||||
$assignment->histories()->create([
|
||||
'from_position_id' => $before['position_id'],
|
||||
'to_position_id' => $position->id,
|
||||
'from_pusat_mengundi_id' => $before['pusat_mengundi_id'],
|
||||
'to_pusat_mengundi_id' => $pusat->id,
|
||||
'from_saluran_mengundi_id' => $before['saluran_mengundi_id'],
|
||||
'to_saluran_mengundi_id' => $saluranMengundiId,
|
||||
'from_reports_to_assignment_id' => $before['reports_to_assignment_id'],
|
||||
'to_reports_to_assignment_id' => $reportsToAssignmentId,
|
||||
'changed_by_user_id' => $actor->id,
|
||||
'note' => $data['note'] ?? 'Assignment updated by Admin.',
|
||||
]);
|
||||
|
||||
$application = $assignment->application;
|
||||
if ($application instanceof Application) {
|
||||
$application->forceFill([
|
||||
'pusat_mengundi_id' => $pusat->id,
|
||||
'approved_position_id' => $position->id,
|
||||
'selected_ktm_assignment_id' => $reportsToAssignmentId,
|
||||
'status' => $data['status'] === 'active' ? 'assigned' : $application->status,
|
||||
])->save();
|
||||
}
|
||||
|
||||
$this->operationalAccountService->provisionForAssignment($assignment, $position);
|
||||
|
||||
$this->postCloseNoteService->recordIfClosed($election, $assignment, $actor, $data['note'] ?? null, 'admin_assignment_edit_after_close');
|
||||
|
||||
activity('admin_management')
|
||||
->performedOn($assignment)
|
||||
->causedBy($actor)
|
||||
->withProperties([
|
||||
'before' => $before,
|
||||
'after' => [
|
||||
'position_id' => $assignment->position_id,
|
||||
'pusat_mengundi_id' => $assignment->pusat_mengundi_id,
|
||||
'saluran_mengundi_id' => $assignment->saluran_mengundi_id,
|
||||
'reports_to_assignment_id' => $assignment->reports_to_assignment_id,
|
||||
'status' => $assignment->status,
|
||||
],
|
||||
])
|
||||
->log('staff_assignment_updated_by_admin');
|
||||
|
||||
return $assignment;
|
||||
});
|
||||
}
|
||||
}
|
||||
46
app/Services/Admin/Management/AdminPostCloseNoteService.php
Normal file
46
app/Services/Admin/Management/AdminPostCloseNoteService.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Management;
|
||||
|
||||
use App\Models\Election;
|
||||
use App\Models\SystemNote;
|
||||
use App\Models\User;
|
||||
use App\Services\RegistrationPeriodService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AdminPostCloseNoteService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RegistrationPeriodService $registrationPeriodService,
|
||||
) {}
|
||||
|
||||
public function requireIfClosed(Election $election, ?string $note): void
|
||||
{
|
||||
if ($this->registrationPeriodService->isOpen($election)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blank($note)) {
|
||||
throw ValidationException::withMessages([
|
||||
'note' => 'Catatan wajib diisi untuk perubahan selepas tempoh pendaftaran ditutup.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function recordIfClosed(Election $election, Model $noteable, User $actor, ?string $note, string $type): ?SystemNote
|
||||
{
|
||||
if ($this->registrationPeriodService->isOpen($election) || blank($note)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SystemNote::query()->create([
|
||||
'election_id' => $election->id,
|
||||
'noteable_type' => $noteable::class,
|
||||
'noteable_id' => $noteable->getKey(),
|
||||
'note_type' => $type,
|
||||
'note' => $note,
|
||||
'created_by_user_id' => $actor->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
111
app/Services/Admin/Management/AdminRepresentativeService.php
Normal file
111
app/Services/Admin/Management/AdminRepresentativeService.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Management;
|
||||
|
||||
use App\Models\Election;
|
||||
use App\Models\JkmRepresentative;
|
||||
use App\Models\KkmRepresentative;
|
||||
use App\Models\PoliceEscort;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdminRepresentativeService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminPostCloseNoteService $postCloseNoteService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function store(User $actor, array $data): Model
|
||||
{
|
||||
return DB::transaction(function () use ($actor, $data): Model {
|
||||
$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 new \InvalidArgumentException('Pusat Mengundi tidak berada dalam pilihanraya yang sama.');
|
||||
}
|
||||
|
||||
if (! empty($data['saluran_mengundi_id'])) {
|
||||
/** @var SaluranMengundi $saluran */
|
||||
$saluran = SaluranMengundi::query()->findOrFail($data['saluran_mengundi_id']);
|
||||
if ((int) $saluran->pusat_mengundi_id !== (int) $pusat->id) {
|
||||
throw new \InvalidArgumentException('Saluran Mengundi tidak berada di bawah Pusat Mengundi yang dipilih.');
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'election_id' => $election->id,
|
||||
'pusat_mengundi_id' => $pusat->id,
|
||||
'name' => $data['name'],
|
||||
'ic_number' => $data['ic_number'] ?? null,
|
||||
'phone_number' => $data['phone_number'] ?? null,
|
||||
'notes' => $data['notes'] ?? null,
|
||||
];
|
||||
|
||||
$type = (string) $data['type'];
|
||||
|
||||
$representative = match ($type) {
|
||||
'police' => PoliceEscort::query()->create([
|
||||
...$payload,
|
||||
'saluran_mengundi_id' => $data['saluran_mengundi_id'] ?? null,
|
||||
'rank' => $data['rank'] ?? null,
|
||||
'station' => $data['station'] ?? null,
|
||||
]),
|
||||
'kkm' => KkmRepresentative::query()->create([
|
||||
...$payload,
|
||||
'agency' => $data['agency'] ?? null,
|
||||
]),
|
||||
'jkm' => JkmRepresentative::query()->create([
|
||||
...$payload,
|
||||
'agency' => $data['agency'] ?? null,
|
||||
]),
|
||||
default => throw new \InvalidArgumentException('Jenis wakil agensi tidak sah.'),
|
||||
};
|
||||
|
||||
$this->postCloseNoteService->recordIfClosed($election, $representative, $actor, $data['note'] ?? null, 'admin_representative_after_close');
|
||||
|
||||
activity('admin_management')
|
||||
->performedOn($representative)
|
||||
->causedBy($actor)
|
||||
->withProperties(['type' => $type])
|
||||
->log('representative_created_by_admin');
|
||||
|
||||
return $representative;
|
||||
});
|
||||
}
|
||||
|
||||
public function delete(Model $representative, User $actor, ?string $note = null): void
|
||||
{
|
||||
DB::transaction(function () use ($representative, $actor, $note): void {
|
||||
$election = match (true) {
|
||||
$representative instanceof PoliceEscort => $representative->election,
|
||||
$representative instanceof KkmRepresentative => $representative->election,
|
||||
$representative instanceof JkmRepresentative => $representative->election,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $election instanceof Election) {
|
||||
throw new \InvalidArgumentException('Pilihanraya wakil agensi tidak sah.');
|
||||
}
|
||||
$this->postCloseNoteService->requireIfClosed($election, $note);
|
||||
|
||||
$this->postCloseNoteService->recordIfClosed($election, $representative, $actor, $note, 'admin_representative_delete_after_close');
|
||||
|
||||
activity('admin_management')
|
||||
->performedOn($representative)
|
||||
->causedBy($actor)
|
||||
->withProperties(['note' => $note])
|
||||
->log('representative_deleted_by_admin');
|
||||
|
||||
$representative->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
131
app/Services/Admin/Management/PennempatanService.php
Normal file
131
app/Services/Admin/Management/PennempatanService.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Management;
|
||||
|
||||
use App\Models\Position;
|
||||
use App\Models\PositionQuota;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Collection as BaseCollection;
|
||||
|
||||
class PennempatanService
|
||||
{
|
||||
/**
|
||||
* Summary per pusat for the dashboard vacancy table.
|
||||
*
|
||||
* @return Collection<int, PusatMengundi>
|
||||
*/
|
||||
public function pusatsWithVacancySummary(): Collection
|
||||
{
|
||||
return PusatMengundi::query()
|
||||
->with([
|
||||
'dun',
|
||||
'election',
|
||||
'saluranMengundis',
|
||||
'positionQuotas',
|
||||
])
|
||||
->withCount(['saluranMengundis'])
|
||||
->get()
|
||||
->each(function (PusatMengundi $pusat): void {
|
||||
$totalQuota = $pusat->positionQuotas->sum('quota');
|
||||
$filled = StaffAssignment::query()
|
||||
->where('pusat_mengundi_id', $pusat->id)
|
||||
->where('status', 'active')
|
||||
->count();
|
||||
|
||||
$pusat->setAttribute('total_quota', $totalQuota);
|
||||
$pusat->setAttribute('filled', $filled);
|
||||
$pusat->setAttribute('vacant', max(0, $totalQuota - $filled));
|
||||
|
||||
// PPM name
|
||||
$ppmPosition = Position::query()->where('code', 'PPM')->first();
|
||||
$ppmAssignment = $ppmPosition
|
||||
? StaffAssignment::query()
|
||||
->with('application')
|
||||
->where('pusat_mengundi_id', $pusat->id)
|
||||
->where('position_id', $ppmPosition->id)
|
||||
->where('status', 'active')
|
||||
->whereNull('saluran_mengundi_id')
|
||||
->first()
|
||||
: null;
|
||||
|
||||
$pusat->setAttribute('ppm_name', $ppmAssignment?->application?->name ?? '-');
|
||||
})
|
||||
->filter(fn (PusatMengundi $p): bool => $p->vacant > 0)
|
||||
->sortByDesc('vacant')
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured breakdown of quotas + assignments for a specific pusat.
|
||||
*
|
||||
* @return BaseCollection<int, array{saluran: SaluranMengundi|null, label: string, entries: BaseCollection}>
|
||||
*/
|
||||
public function detailForPusat(PusatMengundi $pusat): BaseCollection
|
||||
{
|
||||
$quotas = PositionQuota::query()
|
||||
->with('position')
|
||||
->where('pusat_mengundi_id', $pusat->id)
|
||||
->get()
|
||||
->groupBy(fn (PositionQuota $q): string => $q->saluran_mengundi_id ?? 'pusat');
|
||||
|
||||
$assignments = StaffAssignment::query()
|
||||
->with(['application', 'position'])
|
||||
->where('pusat_mengundi_id', $pusat->id)
|
||||
->where('status', 'active')
|
||||
->get()
|
||||
->groupBy(fn (StaffAssignment $a): string => ($a->saluran_mengundi_id ?? 'pusat').'_'.$a->position_id);
|
||||
|
||||
$salurans = SaluranMengundi::query()
|
||||
->where('pusat_mengundi_id', $pusat->id)
|
||||
->orderBy('number')
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$sections = collect();
|
||||
|
||||
// Pusat-level section first
|
||||
if ($quotas->has('pusat')) {
|
||||
$sections->push($this->buildSection(null, 'Peringkat Pusat (Saluran 0)', $quotas->get('pusat'), $assignments));
|
||||
}
|
||||
|
||||
// Per-saluran sections
|
||||
foreach ($salurans as $saluran) {
|
||||
$saluranQuotas = $quotas->get((string) $saluran->id, collect());
|
||||
$sections->push($this->buildSection($saluran, 'Saluran '.$saluran->number, $saluranQuotas, $assignments));
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, PositionQuota> $quotas
|
||||
* @param BaseCollection<string, Collection> $assignments
|
||||
* @return array{saluran: SaluranMengundi|null, label: string, entries: BaseCollection}
|
||||
*/
|
||||
private function buildSection(?SaluranMengundi $saluran, string $label, $quotas, BaseCollection $assignments): array
|
||||
{
|
||||
$scopeKey = $saluran ? (string) $saluran->id : 'pusat';
|
||||
|
||||
$entries = $quotas->map(function (PositionQuota $quota) use ($scopeKey, $assignments) {
|
||||
$key = $scopeKey.'_'.$quota->position_id;
|
||||
$filled = $assignments->get($key, collect());
|
||||
$vacant = max(0, $quota->quota - $filled->count());
|
||||
|
||||
return [
|
||||
'quota' => $quota,
|
||||
'position' => $quota->position,
|
||||
'assignments' => $filled,
|
||||
'vacant' => $vacant,
|
||||
];
|
||||
})->values();
|
||||
|
||||
return [
|
||||
'saluran' => $saluran,
|
||||
'label' => $label,
|
||||
'entries' => $entries,
|
||||
];
|
||||
}
|
||||
}
|
||||
139
app/Services/Admin/Management/WheelchairManagementService.php
Normal file
139
app/Services/Admin/Management/WheelchairManagementService.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Management;
|
||||
|
||||
use App\Models\Election;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\User;
|
||||
use App\Models\WheelchairAllocation;
|
||||
use App\Models\WheelchairTransaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WheelchairManagementService
|
||||
{
|
||||
/**
|
||||
* @param array{election_id: int, pusat_mengundi_id: int, allocated_quantity: int, notes?: string|null} $data
|
||||
*/
|
||||
public function upsertAllocation(User $actor, array $data): WheelchairAllocation
|
||||
{
|
||||
return DB::transaction(function () use ($actor, $data): WheelchairAllocation {
|
||||
/** @var Election $election */
|
||||
$election = Election::query()->findOrFail($data['election_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.',
|
||||
]);
|
||||
}
|
||||
|
||||
$allocation = WheelchairAllocation::query()->firstOrNew([
|
||||
'election_id' => $election->id,
|
||||
'pusat_mengundi_id' => $pusat->id,
|
||||
]);
|
||||
|
||||
$outstanding = $allocation->exists ? $this->outstandingQuantity($allocation) : 0;
|
||||
|
||||
if ($data['allocated_quantity'] < $outstanding) {
|
||||
throw ValidationException::withMessages([
|
||||
'allocated_quantity' => 'Peruntukan tidak boleh kurang daripada jumlah kerusi roda yang masih belum dipulang.',
|
||||
]);
|
||||
}
|
||||
|
||||
$before = $allocation->exists ? $allocation->only(['allocated_quantity', 'notes']) : null;
|
||||
|
||||
$allocation->forceFill([
|
||||
'allocated_quantity' => $data['allocated_quantity'],
|
||||
'notes' => $data['notes'] ?? null,
|
||||
])->save();
|
||||
|
||||
activity('wheelchairs')
|
||||
->performedOn($allocation)
|
||||
->causedBy($actor)
|
||||
->withProperties([
|
||||
'before' => $before,
|
||||
'after' => $allocation->only(['allocated_quantity', 'notes']),
|
||||
])
|
||||
->log('wheelchair_allocation_upserted');
|
||||
|
||||
return $allocation;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{transaction_type: string, quantity: int, taken_at?: string|null, taken_by_name?: string|null, returned_at?: string|null, return_condition?: string|null, notes?: string|null} $data
|
||||
*/
|
||||
public function recordTransaction(WheelchairAllocation $allocation, User $actor, array $data): WheelchairTransaction
|
||||
{
|
||||
return DB::transaction(function () use ($allocation, $actor, $data): WheelchairTransaction {
|
||||
$allocation->refresh();
|
||||
$type = $data['transaction_type'];
|
||||
$quantity = $data['quantity'];
|
||||
|
||||
if ($type === 'taken' && $quantity > $this->availableQuantity($allocation)) {
|
||||
throw ValidationException::withMessages([
|
||||
'quantity' => 'Jumlah diambil melebihi baki peruntukan kerusi roda.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($type === 'returned' && $quantity > $this->outstandingQuantity($allocation)) {
|
||||
throw ValidationException::withMessages([
|
||||
'quantity' => 'Jumlah dipulang melebihi jumlah kerusi roda yang masih belum dipulang.',
|
||||
]);
|
||||
}
|
||||
|
||||
$transaction = WheelchairTransaction::query()->create([
|
||||
'wheelchair_allocation_id' => $allocation->id,
|
||||
'transaction_type' => $type,
|
||||
'quantity' => $quantity,
|
||||
'taken_at' => $type === 'taken' ? ($data['taken_at'] ?? now()) : null,
|
||||
'taken_by_name' => $type === 'taken' ? ($data['taken_by_name'] ?? null) : null,
|
||||
'returned_at' => $type === 'returned' ? ($data['returned_at'] ?? now()) : null,
|
||||
'return_condition' => $type === 'returned' ? ($data['return_condition'] ?? null) : null,
|
||||
'returned_by_name' => $type === 'returned' ? ($data['returned_by_name'] ?? null) : null,
|
||||
'recorded_by_user_id' => $actor->id,
|
||||
'notes' => $data['notes'] ?? null,
|
||||
]);
|
||||
|
||||
activity('wheelchairs')
|
||||
->performedOn($transaction)
|
||||
->causedBy($actor)
|
||||
->withProperties([
|
||||
'allocation_id' => $allocation->id,
|
||||
'transaction_type' => $type,
|
||||
'quantity' => $quantity,
|
||||
'available_after' => $this->availableQuantity($allocation->fresh()),
|
||||
'outstanding_after' => $this->outstandingQuantity($allocation->fresh()),
|
||||
])
|
||||
->log('wheelchair_transaction_recorded');
|
||||
|
||||
return $transaction;
|
||||
});
|
||||
}
|
||||
|
||||
public function takenQuantity(WheelchairAllocation $allocation): int
|
||||
{
|
||||
return (int) $allocation->transactions()
|
||||
->where('transaction_type', 'taken')
|
||||
->sum('quantity');
|
||||
}
|
||||
|
||||
public function returnedQuantity(WheelchairAllocation $allocation): int
|
||||
{
|
||||
return (int) $allocation->transactions()
|
||||
->where('transaction_type', 'returned')
|
||||
->sum('quantity');
|
||||
}
|
||||
|
||||
public function outstandingQuantity(WheelchairAllocation $allocation): int
|
||||
{
|
||||
return max(0, $this->takenQuantity($allocation) - $this->returnedQuantity($allocation));
|
||||
}
|
||||
|
||||
public function availableQuantity(WheelchairAllocation $allocation): int
|
||||
{
|
||||
return max(0, (int) $allocation->allocated_quantity - $this->outstandingQuantity($allocation));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user