first
This commit is contained in:
152
app/Services/Public/KtmVacancyService.php
Normal file
152
app/Services/Public/KtmVacancyService.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Public;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Position;
|
||||
use App\Models\PositionQuota;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class KtmVacancyService
|
||||
{
|
||||
private const RESERVED_APPLICATION_STATUSES = [
|
||||
'submitted',
|
||||
'under_ppm_review',
|
||||
'approved',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<int, string> $roleCodes
|
||||
* @return array<string, array<int, array{id: int, label: string, remaining: int}>>
|
||||
*/
|
||||
public function optionsForRoles(PusatMengundi $pusatMengundi, array $roleCodes = ['KP', 'KPDP'], ?int $ignoreApplicationId = null): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
foreach ($roleCodes as $roleCode) {
|
||||
$options[$roleCode] = $this->availableKtmAssignments($pusatMengundi, $roleCode, $ignoreApplicationId)
|
||||
->map(function (StaffAssignment $assignment) use ($roleCode, $ignoreApplicationId): array {
|
||||
$user = $assignment->user;
|
||||
$saluranMengundi = $assignment->saluranMengundi;
|
||||
|
||||
return [
|
||||
'id' => $assignment->id,
|
||||
'label' => trim(
|
||||
($user instanceof User ? $user->name : 'KTM tanpa nama')
|
||||
.' - Saluran '
|
||||
.($saluranMengundi instanceof SaluranMengundi ? $saluranMengundi->number : '-'),
|
||||
),
|
||||
'remaining' => $this->remainingVacancy($assignment, $roleCode, $ignoreApplicationId),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function hasVacancyForAssignment(int $assignmentId, PusatMengundi $pusatMengundi, string $roleCode, ?int $ignoreApplicationId = null): bool
|
||||
{
|
||||
$ktmPosition = Position::query()->where('code', 'KTM')->first();
|
||||
|
||||
if ($ktmPosition === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$assignment = StaffAssignment::query()
|
||||
->with(['saluranMengundi', 'user'])
|
||||
->where('id', $assignmentId)
|
||||
->where('election_id', $pusatMengundi->election_id)
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->where('position_id', $ktmPosition->id)
|
||||
->where('status', 'active')
|
||||
->whereNotNull('saluran_mengundi_id')
|
||||
->first();
|
||||
|
||||
return $assignment instanceof StaffAssignment
|
||||
&& $this->remainingVacancy($assignment, $roleCode, $ignoreApplicationId) > 0;
|
||||
}
|
||||
|
||||
public function remainingForAssignment(StaffAssignment $assignment, string $roleCode, ?int $ignoreApplicationId = null): int
|
||||
{
|
||||
return $this->remainingVacancy($assignment, $roleCode, $ignoreApplicationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, StaffAssignment>
|
||||
*/
|
||||
private function availableKtmAssignments(PusatMengundi $pusatMengundi, string $roleCode, ?int $ignoreApplicationId = null): Collection
|
||||
{
|
||||
return $this->ktmAssignments($pusatMengundi)
|
||||
->filter(fn (StaffAssignment $assignment): bool => $this->remainingVacancy($assignment, $roleCode, $ignoreApplicationId) > 0)
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EloquentCollection<int, StaffAssignment>
|
||||
*/
|
||||
private function ktmAssignments(PusatMengundi $pusatMengundi): EloquentCollection
|
||||
{
|
||||
$ktmPosition = Position::query()->where('code', 'KTM')->first();
|
||||
|
||||
if ($ktmPosition === null) {
|
||||
return new EloquentCollection;
|
||||
}
|
||||
|
||||
return StaffAssignment::query()
|
||||
->with(['saluranMengundi', 'user'])
|
||||
->where('election_id', $pusatMengundi->election_id)
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->where('position_id', $ktmPosition->id)
|
||||
->where('status', 'active')
|
||||
->whereNotNull('saluran_mengundi_id')
|
||||
->whereNotNull('user_id')
|
||||
->orderBy('saluran_mengundi_id')
|
||||
->get();
|
||||
}
|
||||
|
||||
private function remainingVacancy(StaffAssignment $ktmAssignment, string $roleCode, ?int $ignoreApplicationId = null): int
|
||||
{
|
||||
$position = Position::query()->where('code', $roleCode)->first();
|
||||
|
||||
if ($position === null || $ktmAssignment->saluran_mengundi_id === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$quota = PositionQuota::query()
|
||||
->where('election_id', $ktmAssignment->election_id)
|
||||
->where('pusat_mengundi_id', $ktmAssignment->pusat_mengundi_id)
|
||||
->where('saluran_mengundi_id', $ktmAssignment->saluran_mengundi_id)
|
||||
->where('position_id', $position->id)
|
||||
->value('quota');
|
||||
|
||||
if ($quota === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$assignedCount = StaffAssignment::query()
|
||||
->where('reports_to_assignment_id', $ktmAssignment->id)
|
||||
->where('position_id', $position->id)
|
||||
->where('status', 'active')
|
||||
->count();
|
||||
|
||||
$reservedQuery = Application::query()
|
||||
->where('selected_ktm_assignment_id', $ktmAssignment->id)
|
||||
->where('requested_position_id', $position->id)
|
||||
->whereIn('status', self::RESERVED_APPLICATION_STATUSES);
|
||||
|
||||
if ($ignoreApplicationId !== null) {
|
||||
$reservedQuery->where('id', '!=', $ignoreApplicationId);
|
||||
}
|
||||
|
||||
$reservedCount = $reservedQuery->count();
|
||||
|
||||
return max(0, (int) $quota - $assignedCount - $reservedCount);
|
||||
}
|
||||
}
|
||||
106
app/Services/Public/PublicApplicationSubmissionService.php
Normal file
106
app/Services/Public/PublicApplicationSubmissionService.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Public;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
class PublicApplicationSubmissionService
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function submit(PusatMengundi $pusatMengundi, array $data): Application
|
||||
{
|
||||
return DB::transaction(function () use ($pusatMengundi, $data): Application {
|
||||
$position = Position::query()
|
||||
->where('code', $data['requested_position_code'])
|
||||
->where('is_public_applyable', true)
|
||||
->firstOrFail();
|
||||
|
||||
$application = Application::query()->create([
|
||||
'election_id' => $pusatMengundi->election_id,
|
||||
'pusat_mengundi_id' => $pusatMengundi->id,
|
||||
'selected_ktm_assignment_id' => in_array($position->code, ['KP', 'KPDP'], true)
|
||||
? $data['selected_ktm_assignment_id']
|
||||
: null,
|
||||
'public_uuid' => (string) Str::uuid(),
|
||||
'source' => 'public',
|
||||
'status' => 'submitted',
|
||||
'name' => $data['name'],
|
||||
'ic_number' => $data['ic_number'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'email' => $data['email'] ?? null,
|
||||
'address' => $data['address'],
|
||||
'requested_position_id' => $position->id,
|
||||
'bank_name' => $data['bank_name'],
|
||||
'bank_account_number' => $data['bank_account_number'],
|
||||
]);
|
||||
|
||||
$this->storeDocument($application, 'ic_document', $data['ic_document']);
|
||||
$this->storeDocument($application, 'bank_statement', $data['bank_statement']);
|
||||
|
||||
$application->statusHistories()->create([
|
||||
'to_status' => 'submitted',
|
||||
'note' => 'Public QR application submitted.',
|
||||
'metadata' => [
|
||||
'source' => 'public',
|
||||
'requested_position_code' => $position->code,
|
||||
],
|
||||
]);
|
||||
|
||||
$application->bankVerification()->create([
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
activity('public_application')
|
||||
->performedOn($application)
|
||||
->withProperties([
|
||||
'pusat_mengundi_id' => $pusatMengundi->id,
|
||||
'requested_position_code' => $position->code,
|
||||
'document_types' => ['ic_document', 'bank_statement'],
|
||||
])
|
||||
->log('application_submitted');
|
||||
|
||||
return $application;
|
||||
});
|
||||
}
|
||||
|
||||
private function storeDocument(Application $application, string $documentType, mixed $file): void
|
||||
{
|
||||
if (! $file instanceof UploadedFile) {
|
||||
throw new RuntimeException('Invalid uploaded file.');
|
||||
}
|
||||
|
||||
$path = $file->storeAs(
|
||||
'applications/'.$application->public_uuid,
|
||||
$documentType.'-'.$file->hashName(),
|
||||
'local',
|
||||
);
|
||||
|
||||
if (! is_string($path)) {
|
||||
throw new RuntimeException('Document upload failed.');
|
||||
}
|
||||
|
||||
$application->documents()->create([
|
||||
'document_type' => $documentType,
|
||||
'disk' => 'local',
|
||||
'path' => $path,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getClientMimeType() ?: $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
]);
|
||||
|
||||
activity('public_application')
|
||||
->performedOn($application)
|
||||
->withProperties([
|
||||
'document_type' => $documentType,
|
||||
])
|
||||
->log('application_document_uploaded');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user