first
This commit is contained in:
68
app/Console/Commands/ExportPurgeCommand.php
Normal file
68
app/Console/Commands/ExportPurgeCommand.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ExportLog;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ExportPurgeCommand extends Command
|
||||
{
|
||||
protected $signature = 'exports:purge {--days=30 : Minimum age in days before an export file is purged}';
|
||||
|
||||
protected $description = 'Purge export files older than the configured retention period and mark export_logs records as purged.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
|
||||
if ($days < 1) {
|
||||
$this->error('--days must be at least 1.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$cutoff = now()->subDays($days);
|
||||
|
||||
$logs = ExportLog::query()
|
||||
->whereNull('purged_at')
|
||||
->where('created_at', '<', $cutoff)
|
||||
->get();
|
||||
|
||||
if ($logs->isEmpty()) {
|
||||
$this->info("No export files eligible for purge (older than {$days} days).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$purged = 0;
|
||||
$missing = 0;
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$disk = $log->disk ?? 'local';
|
||||
$path = $log->path;
|
||||
|
||||
if ($path && Storage::disk($disk)->exists($path)) {
|
||||
Storage::disk($disk)->delete($path);
|
||||
$purged++;
|
||||
} else {
|
||||
$missing++;
|
||||
}
|
||||
|
||||
$log->forceFill(['purged_at' => now()])->save();
|
||||
|
||||
activity('exports')
|
||||
->performedOn($log)
|
||||
->withProperties([
|
||||
'report_type' => $log->report_type,
|
||||
'path' => $path,
|
||||
'retention_days' => $days,
|
||||
])
|
||||
->log('export_file_purged');
|
||||
}
|
||||
|
||||
$this->info("Purged {$purged} export file(s). {$missing} record(s) had no file on disk.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
61
app/Exports/AttendanceDetailExport.php
Normal file
61
app/Exports/AttendanceDetailExport.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use OpenSpout\Common\Entity\Row;
|
||||
use OpenSpout\Writer\XLSX\Writer;
|
||||
|
||||
class AttendanceDetailExport
|
||||
{
|
||||
/**
|
||||
* @param Collection<int, StaffAssignment> $assignments
|
||||
*/
|
||||
public function write(Collection $assignments, string $absolutePath): void
|
||||
{
|
||||
$writer = new Writer;
|
||||
$writer->openToFile($absolutePath);
|
||||
|
||||
$writer->addRow(Row::fromValues([
|
||||
'Pusat Mengundi',
|
||||
'Saluran',
|
||||
'Nama',
|
||||
'Jawatan',
|
||||
'Status Kehadiran',
|
||||
'Masa Check-in',
|
||||
'Catatan',
|
||||
]));
|
||||
|
||||
foreach ($assignments as $assignment) {
|
||||
$application = $assignment->application;
|
||||
$user = $assignment->user;
|
||||
$pusat = $assignment->pusatMengundi;
|
||||
$position = $assignment->position;
|
||||
$saluran = $assignment->saluranMengundi;
|
||||
$attendance = $assignment->attendance;
|
||||
$checkInTime = $attendance instanceof Attendance && filled($attendance->check_in_time)
|
||||
? Carbon::parse($attendance->check_in_time)->format('d/m/Y H:i')
|
||||
: '';
|
||||
|
||||
$writer->addRow(Row::fromValues([
|
||||
$pusat instanceof PusatMengundi ? $pusat->code.' - '.$pusat->name : '',
|
||||
$saluran instanceof SaluranMengundi ? $saluran->number : '',
|
||||
($application instanceof Application ? $application->name : null) ?: ($user instanceof User ? $user->name : ''),
|
||||
$position instanceof Position ? $position->code : '',
|
||||
$attendance instanceof Attendance ? $attendance->status : 'not_recorded',
|
||||
$checkInTime,
|
||||
$attendance instanceof Attendance ? ($attendance->note ?? '') : '',
|
||||
]));
|
||||
}
|
||||
|
||||
$writer->close();
|
||||
}
|
||||
}
|
||||
68
app/Exports/FinanceVerificationExport.php
Normal file
68
app/Exports/FinanceVerificationExport.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\BankVerification;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use Illuminate\Support\Collection;
|
||||
use OpenSpout\Common\Entity\Row;
|
||||
use OpenSpout\Writer\XLSX\Writer;
|
||||
|
||||
class FinanceVerificationExport
|
||||
{
|
||||
/**
|
||||
* @param Collection<int, BankVerification> $verifications
|
||||
*/
|
||||
public function write(Collection $verifications, string $absolutePath): void
|
||||
{
|
||||
$writer = new Writer;
|
||||
$writer->openToFile($absolutePath);
|
||||
|
||||
$writer->addRow(Row::fromValues([
|
||||
'Nama',
|
||||
'No IC',
|
||||
'Telefon',
|
||||
'Email',
|
||||
'Pusat Mengundi',
|
||||
'Jawatan',
|
||||
'Bank',
|
||||
'No Akaun',
|
||||
'Status Bank',
|
||||
'Penyata Bank',
|
||||
'Nota Kewangan',
|
||||
]));
|
||||
|
||||
foreach ($verifications as $verification) {
|
||||
$application = $verification->application;
|
||||
if (! $application instanceof Application) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$position = $application->approvedPosition ?: $application->requestedPosition;
|
||||
$positionCode = $position instanceof Position ? $position->code : null;
|
||||
$pusat = $application->pusatMengundi;
|
||||
$pusatLabel = $pusat instanceof PusatMengundi
|
||||
? trim($pusat->code.' - '.$pusat->name)
|
||||
: '';
|
||||
$hasBankStatement = $application->documents->contains('document_type', 'bank_statement') === true;
|
||||
|
||||
$writer->addRow(Row::fromValues([
|
||||
$application->name,
|
||||
$application->ic_number,
|
||||
$application->phone_number,
|
||||
$application->email,
|
||||
$pusatLabel,
|
||||
$positionCode,
|
||||
$application->bank_name,
|
||||
$application->bank_account_number,
|
||||
$verification->status,
|
||||
$hasBankStatement ? 'Ada' : 'Tiada',
|
||||
$verification->finance_note,
|
||||
]));
|
||||
}
|
||||
|
||||
$writer->close();
|
||||
}
|
||||
}
|
||||
27
app/Http/Concerns/SortsQuery.php
Normal file
27
app/Http/Concerns/SortsQuery.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
trait SortsQuery
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $allowed
|
||||
*/
|
||||
protected function sortedQuery(Builder $query, array $allowed, string $default, string $defaultDir = 'asc'): Builder
|
||||
{
|
||||
$sort = request()->input('sort', $default);
|
||||
$dir = request()->input('direction', $defaultDir);
|
||||
|
||||
if (! in_array($sort, $allowed, true)) {
|
||||
$sort = $default;
|
||||
}
|
||||
|
||||
if (! in_array($dir, ['asc', 'desc'], true)) {
|
||||
$dir = $defaultDir;
|
||||
}
|
||||
|
||||
return $query->orderBy($sort, $dir);
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/Admin/AttendanceController.php
Normal file
57
app/Http/Controllers/Admin/AttendanceController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Attendance\AttendanceExportService;
|
||||
use App\Services\Attendance\AttendanceSummaryService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AttendanceController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(AttendanceSummaryService $summaryService): View
|
||||
{
|
||||
return view('admin.attendance.index', [
|
||||
'pusats' => $this->sortedQuery(
|
||||
PusatMengundi::query()->with(['election.settings']),
|
||||
['code', 'name'],
|
||||
'name'
|
||||
)->paginate(15)->withQueryString(),
|
||||
'summaryService' => $summaryService,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(PusatMengundi $pusatMengundi, AttendanceSummaryService $summaryService): View
|
||||
{
|
||||
$assignments = StaffAssignment::query()
|
||||
->with(['application', 'user', 'position', 'saluranMengundi', 'attendance.recordedBy'])
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->where('status', 'active')
|
||||
->orderBy('position_id')
|
||||
->orderBy('saluran_mengundi_id')
|
||||
->get();
|
||||
|
||||
return view('admin.attendance.show', [
|
||||
'pusat' => $pusatMengundi->load('election.settings'),
|
||||
'assignments' => $assignments,
|
||||
'summary' => $summaryService->totalsForPusat($pusatMengundi),
|
||||
'roleSummaries' => $summaryService->roleTotalsForPusat($pusatMengundi),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(PusatMengundi $pusatMengundi, Request $request, AttendanceExportService $exportService): StreamedResponse
|
||||
{
|
||||
$exportLog = $exportService->exportPusat($request->user(), $pusatMengundi);
|
||||
|
||||
return Storage::disk($exportLog->disk ?? 'local')
|
||||
->download($exportLog->path, $exportLog->file_name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Management;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\JkmRepresentative;
|
||||
use App\Models\KkmRepresentative;
|
||||
use App\Models\PoliceEscort;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Models\WheelchairAllocation;
|
||||
use App\Services\Admin\Management\WheelchairManagementService;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AdminManagementController extends Controller
|
||||
{
|
||||
public function index(WheelchairManagementService $wheelchairService): View
|
||||
{
|
||||
$allocations = WheelchairAllocation::query()->with('transactions')->get();
|
||||
|
||||
return view('admin.management.index', [
|
||||
'counts' => [
|
||||
'applications' => Application::query()->count(),
|
||||
'assigned' => Application::query()->where('status', 'assigned')->count(),
|
||||
'staffAssignments' => StaffAssignment::query()->where('status', 'active')->count(),
|
||||
'representatives' => PoliceEscort::query()->count() + KkmRepresentative::query()->count() + JkmRepresentative::query()->count(),
|
||||
'missingDocuments' => Application::query()
|
||||
->whereDoesntHave('documents', fn ($query) => $query->where('document_type', 'ic_document'))
|
||||
->orWhereDoesntHave('documents', fn ($query) => $query->where('document_type', 'bank_statement'))
|
||||
->count(),
|
||||
'incompleteBank' => Application::query()
|
||||
->where(fn ($query) => $query->whereNull('bank_name')->orWhereNull('bank_account_number'))
|
||||
->count(),
|
||||
'wheelchairAllocated' => $allocations->sum('allocated_quantity'),
|
||||
'wheelchairOutstanding' => $allocations->sum(fn (WheelchairAllocation $allocation): int => $wheelchairService->outstandingQuantity($allocation)),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Management;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Management\AssignApplicationRequest;
|
||||
use App\Http\Requests\Admin\Management\StoreManualApplicationRequest;
|
||||
use App\Http\Requests\Admin\Management\UpdateManagedApplicationRequest;
|
||||
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\Management\AdminApplicationManagementService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ManagedApplicationController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Application::query()
|
||||
->with(['election', 'pusatMengundi', 'requestedPosition', 'approvedPosition', 'selectedKtmAssignment.user'])
|
||||
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
|
||||
->when($request->filled('pusat_mengundi_id'), fn ($q) => $q->where('pusat_mengundi_id', $request->integer('pusat_mengundi_id')))
|
||||
->when($request->filled('position_id'), function ($q) use ($request): void {
|
||||
$q->where(function ($nested) use ($request): void {
|
||||
$nested->where('requested_position_id', $request->integer('position_id'))
|
||||
->orWhere('approved_position_id', $request->integer('position_id'));
|
||||
});
|
||||
})
|
||||
->when($request->filled('q'), function ($q) use ($request): void {
|
||||
$term = '%'.$request->string('q')->toString().'%';
|
||||
$q->where(fn ($nested) => $nested
|
||||
->where('name', 'like', $term)
|
||||
->orWhere('ic_number', 'like', $term)
|
||||
->orWhere('email', 'like', $term));
|
||||
});
|
||||
|
||||
$applications = $this->sortedQuery($query, ['name', 'status', 'created_at'], 'created_at', 'desc')
|
||||
->paginate(15)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.management.applications.index', [
|
||||
'applications' => $applications,
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'pusats' => PusatMengundi::query()->orderBy('name')->get(),
|
||||
'filters' => $request->only(['status', 'pusat_mengundi_id', 'position_id', 'q']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.management.applications.create', $this->formData([
|
||||
'application' => new Application,
|
||||
]));
|
||||
}
|
||||
|
||||
public function store(StoreManualApplicationRequest $request, AdminApplicationManagementService $service): RedirectResponse
|
||||
{
|
||||
$application = $service->create($request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('admin.management.applications.edit', $application->public_uuid)
|
||||
->with('status', 'Permohonan manual telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(Application $application): View
|
||||
{
|
||||
return view('admin.management.applications.edit', $this->formData([
|
||||
'application' => $application->load(['election.settings', 'pusatMengundi', 'requestedPosition', 'approvedPosition', 'documents', 'staffAssignments.position', 'staffAssignments.saluranMengundi', 'statusHistories']),
|
||||
]));
|
||||
}
|
||||
|
||||
public function update(UpdateManagedApplicationRequest $request, Application $application, AdminApplicationManagementService $service): RedirectResponse
|
||||
{
|
||||
$service->update($application, $request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('admin.management.applications.edit', $application->public_uuid)
|
||||
->with('status', 'Permohonan telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function assign(AssignApplicationRequest $request, Application $application, AdminApplicationManagementService $service): RedirectResponse
|
||||
{
|
||||
$service->assign($application, $request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('admin.management.applications.edit', $application->public_uuid)
|
||||
->with('status', 'Tugasan staf telah dikemas kini.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extra
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formData(array $extra = []): array
|
||||
{
|
||||
$ktmPosition = Position::query()->where('code', 'KTM')->first();
|
||||
|
||||
return [
|
||||
...$extra,
|
||||
'elections' => Election::query()->with('settings')->orderByDesc('id')->get(),
|
||||
'pusats' => PusatMengundi::query()->with('election')->orderBy('name')->get(),
|
||||
'salurans' => SaluranMengundi::query()->with('pusatMengundi')->orderBy('number')->get(),
|
||||
'positions' => Position::query()->where('is_assignable', true)->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'ktmAssignments' => $ktmPosition
|
||||
? StaffAssignment::query()
|
||||
->with(['user', 'pusatMengundi', 'saluranMengundi'])
|
||||
->where('position_id', $ktmPosition->id)
|
||||
->where('status', 'active')
|
||||
->whereNotNull('saluran_mengundi_id')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
: collect(),
|
||||
];
|
||||
}
|
||||
}
|
||||
107
app/Http/Controllers/Admin/Management/PennempatanController.php
Normal file
107
app/Http/Controllers/Admin/Management/PennempatanController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Management;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Admin\Management\PennempatanService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PennempatanController extends Controller
|
||||
{
|
||||
public function show(PusatMengundi $pusatMengundi, PennempatanService $service): View
|
||||
{
|
||||
$pusatMengundi->load(['dun', 'election', 'saluranMengundis']);
|
||||
|
||||
$ppmPosition = Position::query()->where('code', 'PPM')->first();
|
||||
$ppmAssignment = $ppmPosition
|
||||
? StaffAssignment::query()
|
||||
->with('application')
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->where('position_id', $ppmPosition->id)
|
||||
->where('status', 'active')
|
||||
->whereNull('saluran_mengundi_id')
|
||||
->first()
|
||||
: null;
|
||||
|
||||
return view('admin.management.penempatan.show', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'ppm' => $ppmAssignment,
|
||||
'sections' => $service->detailForPusat($pusatMengundi),
|
||||
]);
|
||||
}
|
||||
|
||||
public function lepas(Request $request, PusatMengundi $pusatMengundi, StaffAssignment $assignment): RedirectResponse
|
||||
{
|
||||
abort_unless($assignment->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
$assignment->update([
|
||||
'status' => 'inactive',
|
||||
'assigned_by_user_id' => $request->user()->id,
|
||||
]);
|
||||
|
||||
activity('admin_management')
|
||||
->performedOn($assignment)
|
||||
->causedBy($request->user())
|
||||
->log('assignment_removed_by_admin');
|
||||
|
||||
return redirect()->route('admin.management.penempatan.show', $pusatMengundi)
|
||||
->with('status', 'Petugas telah dilepaskan dari tugasan.');
|
||||
}
|
||||
|
||||
public function pindahForm(PusatMengundi $pusatMengundi, StaffAssignment $assignment): View
|
||||
{
|
||||
abort_unless($assignment->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
return view('admin.management.penempatan.pindah', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'assignment' => $assignment->load(['application', 'position', 'saluranMengundi']),
|
||||
'pusats' => PusatMengundi::query()->with('election')->orderBy('code')->get(),
|
||||
'salurans' => SaluranMengundi::query()
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->orderBy('number')
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function pindah(Request $request, PusatMengundi $pusatMengundi, StaffAssignment $assignment): RedirectResponse
|
||||
{
|
||||
abort_unless($assignment->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
$data = $request->validate([
|
||||
'pusat_mengundi_id' => ['required', 'exists:pusat_mengundis,id'],
|
||||
'saluran_mengundi_id' => ['nullable', 'exists:saluran_mengundis,id'],
|
||||
]);
|
||||
|
||||
$newPusat = PusatMengundi::query()->findOrFail($data['pusat_mengundi_id']);
|
||||
|
||||
$assignment->update([
|
||||
'pusat_mengundi_id' => $newPusat->id,
|
||||
'saluran_mengundi_id' => $data['saluran_mengundi_id'] ?? null,
|
||||
'assigned_by_user_id' => $request->user()->id,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
|
||||
// Update linked application's pusat too
|
||||
if ($assignment->application) {
|
||||
$assignment->application->forceFill(['pusat_mengundi_id' => $newPusat->id])->save();
|
||||
}
|
||||
|
||||
activity('admin_management')
|
||||
->performedOn($assignment)
|
||||
->causedBy($request->user())
|
||||
->withProperties([
|
||||
'from_pusat_id' => $pusatMengundi->id,
|
||||
'to_pusat_id' => $newPusat->id,
|
||||
])
|
||||
->log('assignment_transferred_by_admin');
|
||||
|
||||
return redirect()->route('admin.management.penempatan.show', $newPusat)
|
||||
->with('status', 'Petugas telah dipindahkan ke '.$newPusat->name.'.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Management;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Management\DeleteRepresentativeRequest;
|
||||
use App\Http\Requests\Admin\Management\StoreRepresentativeRequest;
|
||||
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\Services\Admin\Management\AdminRepresentativeService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RepresentativeController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.management.representatives.index', [
|
||||
'policeEscorts' => PoliceEscort::query()->with(['election', 'pusatMengundi', 'saluranMengundi'])->orderBy('name')->paginate(10, ['*'], 'police_page'),
|
||||
'kkmRepresentatives' => KkmRepresentative::query()->with(['election', 'pusatMengundi'])->orderBy('name')->paginate(10, ['*'], 'kkm_page'),
|
||||
'jkmRepresentatives' => JkmRepresentative::query()->with(['election', 'pusatMengundi'])->orderBy('name')->paginate(10, ['*'], 'jkm_page'),
|
||||
'elections' => Election::query()->orderByDesc('id')->get(),
|
||||
'pusats' => PusatMengundi::query()->orderBy('name')->get(),
|
||||
'salurans' => SaluranMengundi::query()->with('pusatMengundi')->orderBy('number')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreRepresentativeRequest $request, AdminRepresentativeService $service): RedirectResponse
|
||||
{
|
||||
$service->store($request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('admin.management.representatives.index')
|
||||
->with('status', 'Wakil agensi telah disimpan.');
|
||||
}
|
||||
|
||||
public function destroy(DeleteRepresentativeRequest $request, string $type, int $id, AdminRepresentativeService $service): RedirectResponse
|
||||
{
|
||||
$service->delete($this->resolveRepresentative($type, $id), $request->user(), $request->validated('note'));
|
||||
|
||||
return redirect()->route('admin.management.representatives.index')
|
||||
->with('status', 'Wakil agensi telah dipadam.');
|
||||
}
|
||||
|
||||
private function resolveRepresentative(string $type, int $id): Model
|
||||
{
|
||||
return match ($type) {
|
||||
'police' => PoliceEscort::query()->findOrFail($id),
|
||||
'kkm' => KkmRepresentative::query()->findOrFail($id),
|
||||
'jkm' => JkmRepresentative::query()->findOrFail($id),
|
||||
default => abort(404),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Management;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Management\UpdateStaffAssignmentRequest;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Admin\Management\AdminAssignmentManagementService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class StaffAssignmentController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = StaffAssignment::query()
|
||||
->with(['application', 'user', 'position', 'pusatMengundi', 'saluranMengundi', 'reportsTo.user'])
|
||||
->when($request->filled('pusat_mengundi_id'), fn ($q) => $q->where('pusat_mengundi_id', $request->integer('pusat_mengundi_id')))
|
||||
->when($request->filled('position_id'), fn ($q) => $q->where('position_id', $request->integer('position_id')))
|
||||
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')));
|
||||
|
||||
return view('admin.management.assignments.index', [
|
||||
'assignments' => $this->sortedQuery($query, ['status', 'assigned_at'], 'id', 'desc')
|
||||
->paginate(15)
|
||||
->withQueryString(),
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'pusats' => PusatMengundi::query()->orderBy('name')->get(),
|
||||
'filters' => $request->only(['pusat_mengundi_id', 'position_id', 'status']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(StaffAssignment $assignment): View
|
||||
{
|
||||
$ktmPosition = Position::query()->where('code', 'KTM')->first();
|
||||
|
||||
return view('admin.management.assignments.edit', [
|
||||
'assignment' => $assignment->load(['application', 'user', 'election.settings', 'position', 'pusatMengundi', 'saluranMengundi', 'reportsTo.user', 'histories']),
|
||||
'positions' => Position::query()->where('is_assignable', true)->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'pusats' => PusatMengundi::query()->with('election')->orderBy('name')->get(),
|
||||
'salurans' => SaluranMengundi::query()->with('pusatMengundi')->orderBy('number')->get(),
|
||||
'ktmAssignments' => $ktmPosition
|
||||
? StaffAssignment::query()
|
||||
->with(['user', 'pusatMengundi', 'saluranMengundi'])
|
||||
->where('position_id', $ktmPosition->id)
|
||||
->where('status', 'active')
|
||||
->where('id', '!=', $assignment->id)
|
||||
->whereNotNull('saluran_mengundi_id')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
: collect(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateStaffAssignmentRequest $request, StaffAssignment $assignment, AdminAssignmentManagementService $service): RedirectResponse
|
||||
{
|
||||
$service->update($assignment, $request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('admin.management.assignments.edit', $assignment)
|
||||
->with('status', 'Tugasan staf telah dikemas kini.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Management;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Management\StoreReturnTransactionRequest;
|
||||
use App\Http\Requests\Admin\Management\StoreTakenTransactionRequest;
|
||||
use App\Http\Requests\Admin\Management\UpsertWheelchairAllocationRequest;
|
||||
use App\Models\Election;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\WheelchairAllocation;
|
||||
use App\Services\Admin\Management\WheelchairManagementService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class WheelchairController extends Controller
|
||||
{
|
||||
public function index(WheelchairManagementService $service): View
|
||||
{
|
||||
$allocations = WheelchairAllocation::query()
|
||||
->with(['election', 'pusatMengundi', 'transactions'])
|
||||
->latest()
|
||||
->paginate(15);
|
||||
|
||||
return view('admin.management.wheelchairs.index', [
|
||||
'allocations' => $allocations,
|
||||
'elections' => Election::query()->orderByDesc('id')->get(),
|
||||
'pusats' => PusatMengundi::query()->orderBy('name')->get(),
|
||||
'service' => $service,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertWheelchairAllocationRequest $request, WheelchairManagementService $service): RedirectResponse
|
||||
{
|
||||
$allocation = $service->upsertAllocation($request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('admin.management.wheelchairs.show', $allocation)
|
||||
->with('status', 'Peruntukan kerusi roda telah disimpan.');
|
||||
}
|
||||
|
||||
public function show(WheelchairAllocation $allocation, WheelchairManagementService $service): View
|
||||
{
|
||||
return view('admin.management.wheelchairs.show', [
|
||||
'allocation' => $allocation->load(['election', 'pusatMengundi', 'transactions.recordedBy']),
|
||||
'elections' => Election::query()->orderByDesc('id')->get(),
|
||||
'pusats' => PusatMengundi::query()->orderBy('name')->get(),
|
||||
'service' => $service,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertWheelchairAllocationRequest $request, WheelchairAllocation $allocation, WheelchairManagementService $service): RedirectResponse
|
||||
{
|
||||
$data = [
|
||||
...$request->validated(),
|
||||
'election_id' => $allocation->election_id,
|
||||
'pusat_mengundi_id' => $allocation->pusat_mengundi_id,
|
||||
];
|
||||
|
||||
$service->upsertAllocation($request->user(), $data);
|
||||
|
||||
return redirect()->route('admin.management.wheelchairs.show', $allocation)
|
||||
->with('status', 'Peruntukan kerusi roda telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function recordTaken(StoreTakenTransactionRequest $request, WheelchairAllocation $allocation, WheelchairManagementService $service): RedirectResponse
|
||||
{
|
||||
$service->recordTransaction($allocation, $request->user(), [
|
||||
'transaction_type' => 'taken',
|
||||
...$request->validated(),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.management.wheelchairs.show', $allocation)
|
||||
->with('status', 'Rekod ambil kerusi roda telah disimpan.');
|
||||
}
|
||||
|
||||
public function recordReturn(StoreReturnTransactionRequest $request, WheelchairAllocation $allocation, WheelchairManagementService $service): RedirectResponse
|
||||
{
|
||||
$service->recordTransaction($allocation, $request->user(), [
|
||||
'transaction_type' => 'returned',
|
||||
...$request->validated(),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.management.wheelchairs.show', $allocation)
|
||||
->with('status', 'Rekod pulang kerusi roda telah disimpan.');
|
||||
}
|
||||
}
|
||||
34
app/Http/Controllers/Admin/Setup/AdminSetupController.php
Normal file
34
app/Http/Controllers/Admin/Setup/AdminSetupController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Dun;
|
||||
use App\Models\Election;
|
||||
use App\Models\Position;
|
||||
use App\Models\PositionQuota;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AdminSetupController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.setup.index', [
|
||||
'counts' => [
|
||||
'elections' => Election::query()->count(),
|
||||
'duns' => Dun::query()->count(),
|
||||
'pusat' => PusatMengundi::query()->count(),
|
||||
'saluran' => SaluranMengundi::query()->count(),
|
||||
'positions' => Position::query()->count(),
|
||||
'quotas' => PositionQuota::query()->count(),
|
||||
'ppmAssignments' => StaffAssignment::query()
|
||||
->whereHas('position', fn ($query) => $query->where('code', 'PPM'))
|
||||
->where('status', 'active')
|
||||
->count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertBahagianPilihanrayaRequest;
|
||||
use App\Models\BahagianPilihanraya;
|
||||
use App\Models\Election;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BahagianPilihanrayaController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.setup.bahagian.index', [
|
||||
'bahagians' => $this->sortedQuery(
|
||||
BahagianPilihanraya::query()->with('election'),
|
||||
['code', 'name'],
|
||||
'code'
|
||||
)->paginate(15)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.setup.bahagian.create', [
|
||||
'bahagian' => new BahagianPilihanraya,
|
||||
'elections' => Election::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertBahagianPilihanrayaRequest $request): RedirectResponse
|
||||
{
|
||||
BahagianPilihanraya::query()->create($request->validated());
|
||||
|
||||
return redirect()->route('admin.setup.bahagian.index')
|
||||
->with('status', 'Bahagian Pilihanraya telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(BahagianPilihanraya $bahagianPilihanraya): View
|
||||
{
|
||||
return view('admin.setup.bahagian.edit', [
|
||||
'bahagian' => $bahagianPilihanraya,
|
||||
'elections' => Election::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertBahagianPilihanrayaRequest $request, BahagianPilihanraya $bahagianPilihanraya): RedirectResponse
|
||||
{
|
||||
$bahagianPilihanraya->update($request->validated());
|
||||
|
||||
return redirect()->route('admin.setup.bahagian.index')
|
||||
->with('status', 'Bahagian Pilihanraya telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(BahagianPilihanraya $bahagianPilihanraya): RedirectResponse
|
||||
{
|
||||
$bahagianPilihanraya->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.bahagian.index')
|
||||
->with('status', 'Bahagian Pilihanraya telah dipadam.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertDaerahMengundiRequest;
|
||||
use App\Models\BahagianPilihanraya;
|
||||
use App\Models\DaerahMengundi;
|
||||
use App\Models\Election;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DaerahMengundiController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.setup.daerah.index', [
|
||||
'daerahs' => $this->sortedQuery(
|
||||
DaerahMengundi::query()->with(['election', 'bahagianPilihanraya']),
|
||||
['code', 'name'],
|
||||
'code'
|
||||
)->paginate(15)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.setup.daerah.create', [
|
||||
'daerah' => new DaerahMengundi,
|
||||
'elections' => Election::query()->orderBy('name')->get(),
|
||||
'bahagians' => BahagianPilihanraya::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertDaerahMengundiRequest $request): RedirectResponse
|
||||
{
|
||||
DaerahMengundi::query()->create($request->validated());
|
||||
|
||||
return redirect()->route('admin.setup.daerah.index')
|
||||
->with('status', 'Daerah Mengundi telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(DaerahMengundi $daerahMengundi): View
|
||||
{
|
||||
return view('admin.setup.daerah.edit', [
|
||||
'daerah' => $daerahMengundi,
|
||||
'elections' => Election::query()->orderBy('name')->get(),
|
||||
'bahagians' => BahagianPilihanraya::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertDaerahMengundiRequest $request, DaerahMengundi $daerahMengundi): RedirectResponse
|
||||
{
|
||||
$daerahMengundi->update($request->validated());
|
||||
|
||||
return redirect()->route('admin.setup.daerah.index')
|
||||
->with('status', 'Daerah Mengundi telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(DaerahMengundi $daerahMengundi): RedirectResponse
|
||||
{
|
||||
$daerahMengundi->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.daerah.index')
|
||||
->with('status', 'Daerah Mengundi telah dipadam.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertElectionSettingsRequest;
|
||||
use App\Models\Election;
|
||||
use App\Services\Admin\ElectionSettingsService;
|
||||
use App\Services\RegistrationPeriodService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ElectionSettingsController extends Controller
|
||||
{
|
||||
public function edit(Election $election, RegistrationPeriodService $registrationPeriodService): View
|
||||
{
|
||||
return view('admin.setup.settings.edit', [
|
||||
'election' => $election->load('settings'),
|
||||
'isRegistrationOpen' => $election->settings
|
||||
? $registrationPeriodService->isOpen($election)
|
||||
: false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(
|
||||
UpsertElectionSettingsRequest $request,
|
||||
Election $election,
|
||||
ElectionSettingsService $service,
|
||||
): RedirectResponse {
|
||||
abort_if($election->settings !== null, 409);
|
||||
|
||||
$service->create($election, $request->validated(), $request->user());
|
||||
|
||||
return redirect()->route('admin.setup.settings.edit', $election)
|
||||
->with('status', 'Tetapan pilihanraya telah dicipta.');
|
||||
}
|
||||
|
||||
public function update(
|
||||
UpsertElectionSettingsRequest $request,
|
||||
Election $election,
|
||||
ElectionSettingsService $service,
|
||||
): RedirectResponse {
|
||||
abort_unless($election->settings !== null, 404);
|
||||
|
||||
$service->update($election, $request->validated(), $request->user());
|
||||
|
||||
return redirect()->route('admin.setup.settings.edit', $election)
|
||||
->with('status', 'Tetapan pilihanraya telah dikemas kini.');
|
||||
}
|
||||
}
|
||||
81
app/Http/Controllers/Admin/Setup/PositionController.php
Normal file
81
app/Http/Controllers/Admin/Setup/PositionController.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertPositionRequest;
|
||||
use App\Models\Position;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PositionController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.setup.positions.index', [
|
||||
'positions' => $this->sortedQuery(
|
||||
Position::query(),
|
||||
['code', 'name', 'scope', 'sort_order'],
|
||||
'sort_order'
|
||||
)->paginate(20)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.setup.positions.create', [
|
||||
'position' => new Position,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertPositionRequest $request): RedirectResponse
|
||||
{
|
||||
Position::query()->create($this->payload($request));
|
||||
|
||||
return redirect()->route('admin.setup.positions.index')
|
||||
->with('status', 'Jawatan telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(Position $position): View
|
||||
{
|
||||
return view('admin.setup.positions.edit', [
|
||||
'position' => $position,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertPositionRequest $request, Position $position): RedirectResponse
|
||||
{
|
||||
$position->update($this->payload($request));
|
||||
|
||||
return redirect()->route('admin.setup.positions.index')
|
||||
->with('status', 'Jawatan telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(Position $position): RedirectResponse
|
||||
{
|
||||
$position->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.positions.index')
|
||||
->with('status', 'Jawatan telah dipadam.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function payload(UpsertPositionRequest $request): array
|
||||
{
|
||||
return [
|
||||
...$request->safe()->except([
|
||||
'is_public_applyable',
|
||||
'is_assignable',
|
||||
'allows_dual_role',
|
||||
]),
|
||||
'is_public_applyable' => $request->boolean('is_public_applyable'),
|
||||
'is_assignable' => $request->boolean('is_assignable'),
|
||||
'allows_dual_role' => $request->boolean('allows_dual_role'),
|
||||
];
|
||||
}
|
||||
}
|
||||
100
app/Http/Controllers/Admin/Setup/PositionQuotaController.php
Normal file
100
app/Http/Controllers/Admin/Setup/PositionQuotaController.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertPositionQuotaRequest;
|
||||
use App\Models\Position;
|
||||
use App\Models\PositionQuota;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PositionQuotaController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi): View
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
return view('admin.setup.quotas.index', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'saluran' => $saluranMengundi,
|
||||
'quotas' => $this->sortedQuery(
|
||||
PositionQuota::query()
|
||||
->with(['position'])
|
||||
->where('saluran_mengundi_id', $saluranMengundi->id),
|
||||
['quota'],
|
||||
'id'
|
||||
)->paginate(25)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi): View
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
return view('admin.setup.quotas.create', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'saluran' => $saluranMengundi,
|
||||
'quota' => new PositionQuota,
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('code')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertPositionQuotaRequest $request, PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi): RedirectResponse
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
PositionQuota::query()->updateOrCreate(
|
||||
[
|
||||
'election_id' => $saluranMengundi->election_id,
|
||||
'pusat_mengundi_id' => $pusatMengundi->id,
|
||||
'saluran_mengundi_id' => $saluranMengundi->id,
|
||||
'position_id' => $request->validated('position_id'),
|
||||
],
|
||||
['quota' => $request->validated('quota')],
|
||||
);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.saluran.kuota.index', [$pusatMengundi, $saluranMengundi])
|
||||
->with('status', 'Kekosongan jawatan telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi, PositionQuota $quota): View
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
abort_unless($quota->saluran_mengundi_id === $saluranMengundi->id, 404);
|
||||
|
||||
return view('admin.setup.quotas.edit', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'saluran' => $saluranMengundi,
|
||||
'quota' => $quota->load('position'),
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('code')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertPositionQuotaRequest $request, PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi, PositionQuota $quota): RedirectResponse
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
abort_unless($quota->saluran_mengundi_id === $saluranMengundi->id, 404);
|
||||
|
||||
$quota->update(['quota' => $request->validated('quota')]);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.saluran.kuota.index', [$pusatMengundi, $saluranMengundi])
|
||||
->with('status', 'Kekosongan jawatan telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi, PositionQuota $quota): RedirectResponse
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
abort_unless($quota->saluran_mengundi_id === $saluranMengundi->id, 404);
|
||||
|
||||
$quota->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.pusat.saluran.kuota.index', [$pusatMengundi, $saluranMengundi])
|
||||
->with('status', 'Kekosongan jawatan telah dipadam.');
|
||||
}
|
||||
}
|
||||
50
app/Http/Controllers/Admin/Setup/PusatImportController.php
Normal file
50
app/Http/Controllers/Admin/Setup/PusatImportController.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\ImportPusatRequest;
|
||||
use App\Models\Dun;
|
||||
use App\Models\Election;
|
||||
use App\Services\Admin\PusatImportService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PusatImportController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.setup.import.create', [
|
||||
'elections' => Election::query()->orderByDesc('id')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(ImportPusatRequest $request, PusatImportService $importService): RedirectResponse
|
||||
{
|
||||
/** @var Election $election */
|
||||
$election = Election::query()->findOrFail($request->validated('election_id'));
|
||||
|
||||
$dunCode = strtoupper(trim((string) $request->validated('dun_code')));
|
||||
$dunName = trim((string) $request->validated('dun_name'));
|
||||
|
||||
$dun = Dun::query()->updateOrCreate(
|
||||
['election_id' => $election->id, 'code' => $dunCode],
|
||||
['name' => $dunName],
|
||||
);
|
||||
|
||||
$result = $importService->import($election, $dun, $request->file('file'));
|
||||
|
||||
$message = "Import selesai: {$result['imported']} Pusat Mengundi, {$result['salurans']} Saluran dicipta untuk DUN {$dunCode} - {$dunName}.";
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
$message .= ' '.count($result['errors']).' baris gagal.';
|
||||
|
||||
return redirect()->route('admin.setup.import.create')
|
||||
->with('status', $message)
|
||||
->with('import_errors', $result['errors']);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.setup.pusat.index')
|
||||
->with('status', $message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertPositionQuotaRequest;
|
||||
use App\Models\Position;
|
||||
use App\Models\PositionQuota;
|
||||
use App\Models\PusatMengundi;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PusatLevelQuotaController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
return view('admin.setup.quotas.pusat-index', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'quotas' => $this->sortedQuery(
|
||||
PositionQuota::query()
|
||||
->with(['position'])
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->whereNull('saluran_mengundi_id'),
|
||||
['quota'],
|
||||
'id'
|
||||
)->paginate(25)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
return view('admin.setup.quotas.pusat-create', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'quota' => new PositionQuota,
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('code')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertPositionQuotaRequest $request, PusatMengundi $pusatMengundi): RedirectResponse
|
||||
{
|
||||
PositionQuota::query()->updateOrCreate(
|
||||
[
|
||||
'election_id' => $pusatMengundi->election_id,
|
||||
'pusat_mengundi_id' => $pusatMengundi->id,
|
||||
'saluran_mengundi_id' => null,
|
||||
'position_id' => $request->validated('position_id'),
|
||||
],
|
||||
['quota' => $request->validated('quota')],
|
||||
);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.kuota.index', $pusatMengundi)
|
||||
->with('status', 'Kuota peringkat pusat telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(PusatMengundi $pusatMengundi, PositionQuota $quota): View
|
||||
{
|
||||
abort_unless($quota->pusat_mengundi_id === $pusatMengundi->id && $quota->saluran_mengundi_id === null, 404);
|
||||
|
||||
return view('admin.setup.quotas.pusat-edit', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'quota' => $quota->load('position'),
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('code')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertPositionQuotaRequest $request, PusatMengundi $pusatMengundi, PositionQuota $quota): RedirectResponse
|
||||
{
|
||||
abort_unless($quota->pusat_mengundi_id === $pusatMengundi->id && $quota->saluran_mengundi_id === null, 404);
|
||||
|
||||
$quota->update(['quota' => $request->validated('quota')]);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.kuota.index', $pusatMengundi)
|
||||
->with('status', 'Kuota peringkat pusat telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(PusatMengundi $pusatMengundi, PositionQuota $quota): RedirectResponse
|
||||
{
|
||||
abort_unless($quota->pusat_mengundi_id === $pusatMengundi->id && $quota->saluran_mengundi_id === null, 404);
|
||||
|
||||
$quota->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.pusat.kuota.index', $pusatMengundi)
|
||||
->with('status', 'Kuota peringkat pusat telah dipadam.');
|
||||
}
|
||||
}
|
||||
156
app/Http/Controllers/Admin/Setup/PusatMengundiController.php
Normal file
156
app/Http/Controllers/Admin/Setup/PusatMengundiController.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\AssignPpmRequest;
|
||||
use App\Http\Requests\Admin\Setup\UpsertPusatMengundiRequest;
|
||||
use App\Models\Dun;
|
||||
use App\Models\Election;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\User;
|
||||
use App\Services\Admin\PpmAssignmentService;
|
||||
use App\Services\Admin\PusatOperationalSetupService;
|
||||
use App\Services\Admin\PusatQrCodeService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PusatMengundiController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.setup.pusat.index', [
|
||||
'pusats' => $this->sortedQuery(
|
||||
PusatMengundi::query()->with(['election', 'dun', 'saluranMengundis']),
|
||||
['code', 'name'],
|
||||
'code'
|
||||
)->paginate(15)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.setup.pusat.create', [
|
||||
'pusat' => new PusatMengundi,
|
||||
'elections' => Election::query()->orderBy('name')->get(),
|
||||
'duns' => Dun::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(
|
||||
UpsertPusatMengundiRequest $request,
|
||||
PusatQrCodeService $qrCodeService,
|
||||
PusatOperationalSetupService $operationalSetupService,
|
||||
): RedirectResponse {
|
||||
$pusat = DB::transaction(function () use ($request, $operationalSetupService): PusatMengundi {
|
||||
$pusat = PusatMengundi::query()->create([
|
||||
...$request->safe()->only([
|
||||
'election_id',
|
||||
'dun_id',
|
||||
'code',
|
||||
'name',
|
||||
'address',
|
||||
]),
|
||||
'public_uuid' => (string) Str::uuid(),
|
||||
]);
|
||||
|
||||
$operationalSetupService->configure($pusat, $request->validated());
|
||||
|
||||
return $pusat;
|
||||
});
|
||||
|
||||
$qrCodeService->generate($pusat);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.index')
|
||||
->with('status', 'Pusat Mengundi telah disimpan dan QR telah dijana.');
|
||||
}
|
||||
|
||||
public function show(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
$ppmPosition = Position::query()->where('code', 'PPM')->first();
|
||||
|
||||
return view('admin.setup.pusat.show', [
|
||||
'pusat' => $pusatMengundi->load([
|
||||
'election',
|
||||
'dun',
|
||||
'saluranMengundis',
|
||||
'positionQuotas.position',
|
||||
]),
|
||||
'ppmAssignment' => $ppmPosition
|
||||
? $pusatMengundi->staffAssignments()
|
||||
->with('user')
|
||||
->where('position_id', $ppmPosition->id)
|
||||
->where('status', 'active')
|
||||
->whereNull('saluran_mengundi_id')
|
||||
->first()
|
||||
: null,
|
||||
'users' => User::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
return view('admin.setup.pusat.edit', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'elections' => Election::query()->orderBy('name')->get(),
|
||||
'duns' => Dun::query()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertPusatMengundiRequest $request, PusatMengundi $pusatMengundi): RedirectResponse
|
||||
{
|
||||
$pusatMengundi->update($request->safe()->only([
|
||||
'election_id',
|
||||
'dun_id',
|
||||
'code',
|
||||
'name',
|
||||
'address',
|
||||
]));
|
||||
|
||||
return redirect()->route('admin.setup.pusat.show', $pusatMengundi)
|
||||
->with('status', 'Pusat Mengundi telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(PusatMengundi $pusatMengundi): RedirectResponse
|
||||
{
|
||||
$pusatMengundi->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.pusat.index')
|
||||
->with('status', 'Pusat Mengundi telah dipadam.');
|
||||
}
|
||||
|
||||
public function generateQr(PusatMengundi $pusatMengundi, PusatQrCodeService $qrCodeService): RedirectResponse
|
||||
{
|
||||
$qrCodeService->generate($pusatMengundi);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.show', $pusatMengundi)
|
||||
->with('status', 'QR pendaftaran telah dijana semula.');
|
||||
}
|
||||
|
||||
public function qrSvg(PusatMengundi $pusatMengundi): Response
|
||||
{
|
||||
abort_unless($pusatMengundi->qr_code_path && Storage::disk('local')->exists($pusatMengundi->qr_code_path), 404);
|
||||
|
||||
return response(Storage::disk('local')->get($pusatMengundi->qr_code_path), 200, [
|
||||
'Content-Type' => 'image/svg+xml',
|
||||
]);
|
||||
}
|
||||
|
||||
public function assignPpm(AssignPpmRequest $request, PusatMengundi $pusatMengundi, PpmAssignmentService $assignmentService): RedirectResponse
|
||||
{
|
||||
$user = User::query()->findOrFail($request->validated('user_id'));
|
||||
|
||||
$assignmentService->assign($pusatMengundi, $user);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.show', $pusatMengundi)
|
||||
->with('status', 'PPM telah ditetapkan.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Setup;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Setup\UpsertSaluranMengundiRequest;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SaluranMengundiController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
return view('admin.setup.saluran.index', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'salurans' => $this->sortedQuery(
|
||||
SaluranMengundi::query()->where('pusat_mengundi_id', $pusatMengundi->id),
|
||||
['number', 'voter_count'],
|
||||
'number'
|
||||
)->paginate(20)->withQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
return view('admin.setup.saluran.create', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'saluran' => new SaluranMengundi,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UpsertSaluranMengundiRequest $request, PusatMengundi $pusatMengundi): RedirectResponse
|
||||
{
|
||||
$pusatMengundi->saluranMengundis()->create([
|
||||
'election_id' => $pusatMengundi->election_id,
|
||||
'number' => $request->validated('number'),
|
||||
'voter_count' => $request->validated('voter_count'),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.saluran.index', $pusatMengundi)
|
||||
->with('status', 'Saluran Mengundi telah disimpan.');
|
||||
}
|
||||
|
||||
public function edit(PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi): View
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
return view('admin.setup.saluran.edit', [
|
||||
'pusat' => $pusatMengundi,
|
||||
'saluran' => $saluranMengundi,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpsertSaluranMengundiRequest $request, PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi): RedirectResponse
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
$saluranMengundi->update([
|
||||
'number' => $request->validated('number'),
|
||||
'voter_count' => $request->validated('voter_count'),
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.setup.pusat.saluran.index', $pusatMengundi)
|
||||
->with('status', 'Saluran Mengundi telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function destroy(PusatMengundi $pusatMengundi, SaluranMengundi $saluranMengundi): RedirectResponse
|
||||
{
|
||||
abort_unless($saluranMengundi->pusat_mengundi_id === $pusatMengundi->id, 404);
|
||||
|
||||
$saluranMengundi->forceDelete();
|
||||
|
||||
return redirect()->route('admin.setup.pusat.saluran.index', $pusatMengundi)
|
||||
->with('status', 'Saluran Mengundi telah dipadam.');
|
||||
}
|
||||
}
|
||||
47
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
47
app/Http/Controllers/Auth/AuthenticatedSessionController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
40
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
40
app/Http/Controllers/Auth/ConfirmablePasswordController.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConfirmablePasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the confirm password view.
|
||||
*/
|
||||
public function show(): View
|
||||
{
|
||||
return view('auth.confirm-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user's password.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if (! Auth::guard('web')->validate([
|
||||
'email' => $request->user()->email,
|
||||
'password' => $request->password,
|
||||
])) {
|
||||
throw ValidationException::withMessages([
|
||||
'password' => __('auth.password'),
|
||||
]);
|
||||
}
|
||||
|
||||
$request->session()->put('auth.password_confirmed_at', time());
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationNotificationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Send a new email verification notification.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return back()->with('status', 'verification-link-sent');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EmailVerificationPromptController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the email verification prompt.
|
||||
*/
|
||||
public function __invoke(Request $request): RedirectResponse|View
|
||||
{
|
||||
return $request->user()->hasVerifiedEmail()
|
||||
? redirect()->intended(route('dashboard', absolute: false))
|
||||
: view('auth.verify-email');
|
||||
}
|
||||
}
|
||||
63
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
63
app/Http/Controllers/Auth/NewPasswordController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
// Here we will attempt to reset the user's password. If it is successful we
|
||||
// will update the password on an actual user model and persist it to the
|
||||
// database. Otherwise we will parse the error and return the response.
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
// If the password was successfully reset, we will redirect the user back to
|
||||
// the application's home authenticated view. If there is an error we can
|
||||
// redirect them back to where they came from with their error message.
|
||||
return $status == Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
29
app/Http/Controllers/Auth/PasswordController.php
Normal file
29
app/Http/Controllers/Auth/PasswordController.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class PasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validateWithBag('updatePassword', [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||
]);
|
||||
|
||||
$request->user()->update([
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
return back()->with('status', 'password-updated');
|
||||
}
|
||||
}
|
||||
45
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
45
app/Http/Controllers/Auth/PasswordResetLinkController.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset link request view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming password reset link request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
// We will send the password reset link to this user. Once we have attempted
|
||||
// to send the link, we will examine the response then see the message we
|
||||
// need to show to the user. Finally, we'll send out a proper response.
|
||||
$status = Password::sendResetLink(
|
||||
$request->only('email')
|
||||
);
|
||||
|
||||
return $status == Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
51
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
51
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
10
app/Http/Controllers/Controller.php
Normal file
10
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
}
|
||||
163
app/Http/Controllers/DashboardController.php
Normal file
163
app/Http/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\BankVerification;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Admin\Management\PennempatanService;
|
||||
use App\Services\Ktm\KtmScopeService;
|
||||
use App\Services\Ppm\PpmScopeService;
|
||||
use App\Services\Public\KtmVacancyService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(Request $request): RedirectResponse|View
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user->hasRole('Admin')) {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
|
||||
if ($user->hasRole('Admin Kewangan')) {
|
||||
return redirect()->route('kewangan.dashboard');
|
||||
}
|
||||
|
||||
if ($user->hasRole('PPM')) {
|
||||
return redirect()->route('ppm.dashboard');
|
||||
}
|
||||
|
||||
if ($user->hasRole('KTM')) {
|
||||
return redirect()->route('ktm.dashboard');
|
||||
}
|
||||
|
||||
if ($user->hasRole('Pemohon')) {
|
||||
return redirect()->route('pemohon.dashboard');
|
||||
}
|
||||
|
||||
return view('dashboards.index', $this->payload(
|
||||
'Dashboard',
|
||||
'Akaun ini belum mempunyai peranan sistem.',
|
||||
'Hubungi Admin untuk penetapan peranan.',
|
||||
[
|
||||
['label' => 'Peranan aktif', 'value' => '0', 'icon' => 'bi-shield-lock', 'tone' => 'secondary'],
|
||||
['label' => 'Akses modul', 'value' => 'Belum aktif', 'icon' => 'bi-door-closed', 'tone' => 'secondary'],
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
public function admin(PennempatanService $pennempatanService): View
|
||||
{
|
||||
$pusatsWithVacancy = $pennempatanService->pusatsWithVacancySummary();
|
||||
|
||||
return view('dashboards.admin', [
|
||||
'cards' => [
|
||||
['label' => 'Pusat Mengundi', 'value' => (string) PusatMengundi::query()->count(), 'icon' => 'bi-building', 'tone' => 'primary'],
|
||||
['label' => 'Saluran', 'value' => (string) SaluranMengundi::query()->count(), 'icon' => 'bi-diagram-3', 'tone' => 'info'],
|
||||
['label' => 'Pemohon', 'value' => (string) Application::query()->count(), 'icon' => 'bi-people', 'tone' => 'success'],
|
||||
['label' => 'Staf Aktif', 'value' => (string) StaffAssignment::query()->where('status', 'active')->count(), 'icon' => 'bi-person-check', 'tone' => 'success'],
|
||||
],
|
||||
'pusatsWithVacancy' => $pusatsWithVacancy,
|
||||
]);
|
||||
}
|
||||
|
||||
public function kewangan(): View
|
||||
{
|
||||
$missingBankStatement = BankVerification::query()
|
||||
->whereHas('application', fn ($query) => $query->whereIn('status', ['approved', 'assigned']))
|
||||
->whereHas('application', fn ($query) => $query->whereDoesntHave('documents', fn ($documentQuery) => $documentQuery->where('document_type', 'bank_statement')))
|
||||
->count();
|
||||
|
||||
return view('dashboards.index', $this->payload(
|
||||
'Dashboard Admin Kewangan',
|
||||
'Semakan bank dan dokumen kewangan',
|
||||
'Paparan asas untuk status verifikasi bank tanpa akses perubahan penempatan.',
|
||||
[
|
||||
['label' => 'Pending bank', 'value' => (string) BankVerification::query()->where('status', 'pending')->whereHas('application', fn ($query) => $query->whereIn('status', ['approved', 'assigned']))->count(), 'icon' => 'bi-bank', 'tone' => 'warning'],
|
||||
['label' => 'Dokumen hilang', 'value' => (string) $missingBankStatement, 'icon' => 'bi-file-earmark-x', 'tone' => 'danger'],
|
||||
['label' => 'Ditolak', 'value' => (string) BankVerification::query()->where('status', 'rejected')->count(), 'icon' => 'bi-x-circle', 'tone' => 'danger'],
|
||||
['label' => 'Perlu pembetulan', 'value' => (string) BankVerification::query()->where('status', 'requires_correction')->count(), 'icon' => 'bi-pencil-square', 'tone' => 'info'],
|
||||
],
|
||||
[
|
||||
['label' => 'Senarai staf diluluskan', 'route' => 'kewangan.bank.index'],
|
||||
['label' => 'Semakan akaun bank', 'route' => 'kewangan.bank.index'],
|
||||
['label' => 'Export finance list', 'route' => 'kewangan.bank.export'],
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
public function ppm(Request $request, PpmScopeService $scopeService): View
|
||||
{
|
||||
$pusatIds = $scopeService->assignedPusatIds($request->user());
|
||||
|
||||
return view('dashboards.index', $this->payload(
|
||||
'Dashboard PPM',
|
||||
'Pengurusan Pusat Mengundi sendiri',
|
||||
'Paparan asas untuk semakan permohonan, tugasan saluran, wakil agensi, dan kehadiran.',
|
||||
[
|
||||
['label' => 'Pusat jagaan', 'value' => (string) $pusatIds->count(), 'icon' => 'bi-geo-alt', 'tone' => 'primary'],
|
||||
['label' => 'Permohonan pending', 'value' => (string) Application::query()->whereIn('pusat_mengundi_id', $pusatIds)->whereIn('status', ['submitted', 'under_ppm_review'])->count(), 'icon' => 'bi-inbox', 'tone' => 'warning'],
|
||||
['label' => 'Staf aktif', 'value' => (string) StaffAssignment::query()->whereIn('pusat_mengundi_id', $pusatIds)->where('status', 'active')->count(), 'icon' => 'bi-person-check', 'tone' => 'success'],
|
||||
['label' => 'Kehadiran hadir', 'value' => (string) Attendance::query()->whereIn('pusat_mengundi_id', $pusatIds)->where('status', 'present')->count(), 'icon' => 'bi-check2-circle', 'tone' => 'info'],
|
||||
],
|
||||
[
|
||||
['label' => 'Semak permohonan', 'route' => 'ppm.applications.index'],
|
||||
['label' => 'Rekod kehadiran', 'route' => 'ppm.attendance.index'],
|
||||
'Assign KTM/KP/KPDP',
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
public function ktm(Request $request, KtmScopeService $scopeService, KtmVacancyService $vacancyService): View
|
||||
{
|
||||
$assignments = $scopeService->assignmentsFor($request->user());
|
||||
$assignmentIds = $assignments->pluck('id');
|
||||
$kpVacancy = $assignments->sum(fn (StaffAssignment $assignment): int => $vacancyService->remainingForAssignment($assignment, 'KP'));
|
||||
|
||||
return view('dashboards.index', $this->payload(
|
||||
'Dashboard KTM',
|
||||
'Pengurusan pasukan KP sendiri',
|
||||
'Paparan asas untuk saluran ditugaskan, kekosongan KP, dan pendaftaran KP.',
|
||||
[
|
||||
['label' => 'Saluran ditugaskan', 'value' => (string) $assignments->count(), 'icon' => 'bi-signpost', 'tone' => 'primary'],
|
||||
['label' => 'KP aktif', 'value' => (string) StaffAssignment::query()->whereIn('reports_to_assignment_id', $assignmentIds)->where('status', 'active')->count(), 'icon' => 'bi-people', 'tone' => 'success'],
|
||||
['label' => 'Kekosongan KP', 'value' => (string) $kpVacancy, 'icon' => 'bi-person-plus', 'tone' => 'warning'],
|
||||
],
|
||||
[
|
||||
['label' => 'Senarai KP', 'route' => 'ktm.applications.index'],
|
||||
['label' => 'Daftar KP', 'route' => 'ktm.applications.create'],
|
||||
'Semak permohonan KP',
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
public function pemohon(): View
|
||||
{
|
||||
return view('dashboards.index', $this->payload(
|
||||
'Dashboard Pemohon',
|
||||
'Status permohonan',
|
||||
'Akaun Pemohon akan digunakan jika portal status kendiri diaktifkan dalam fasa akan datang.',
|
||||
[
|
||||
['label' => 'Permohonan', 'value' => '0', 'icon' => 'bi-file-text', 'tone' => 'secondary'],
|
||||
['label' => 'Status', 'value' => 'Belum aktif', 'icon' => 'bi-info-circle', 'tone' => 'info'],
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{label: string, value: string, icon: string, tone: string}> $cards
|
||||
* @param array<int, string|array{label: string, route: string}> $actions
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function payload(string $title, string $eyebrow, string $description, array $cards, array $actions = []): array
|
||||
{
|
||||
return compact('title', 'eyebrow', 'description', 'cards', 'actions');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/DocumentDownloadController.php
Normal file
44
app/Http/Controllers/DocumentDownloadController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ApplicationDocument;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class DocumentDownloadController extends Controller
|
||||
{
|
||||
public function admin(ApplicationDocument $document): StreamedResponse
|
||||
{
|
||||
abort_unless(
|
||||
in_array($document->document_type, ApplicationDocument::ALLOWED_FOR_ADMIN, true),
|
||||
403
|
||||
);
|
||||
abort_unless(Storage::disk($document->disk)->exists($document->path), 404);
|
||||
|
||||
activity('documents')
|
||||
->performedOn($document)
|
||||
->causedBy(request()->user())
|
||||
->withProperties(['document_type' => $document->document_type])
|
||||
->log('document_downloaded_by_admin');
|
||||
|
||||
return Storage::disk($document->disk)->download($document->path, $document->original_name);
|
||||
}
|
||||
|
||||
public function finance(ApplicationDocument $document): StreamedResponse
|
||||
{
|
||||
abort_unless(
|
||||
in_array($document->document_type, ApplicationDocument::ALLOWED_FOR_FINANCE, true),
|
||||
403
|
||||
);
|
||||
abort_unless(Storage::disk($document->disk)->exists($document->path), 404);
|
||||
|
||||
activity('documents')
|
||||
->performedOn($document)
|
||||
->causedBy(request()->user())
|
||||
->withProperties(['document_type' => $document->document_type])
|
||||
->log('bank_statement_downloaded_by_finance');
|
||||
|
||||
return Storage::disk($document->disk)->download($document->path, $document->original_name);
|
||||
}
|
||||
}
|
||||
87
app/Http/Controllers/Kewangan/BankVerificationController.php
Normal file
87
app/Http/Controllers/Kewangan/BankVerificationController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Kewangan;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Kewangan\UpdateBankVerificationRequest;
|
||||
use App\Models\BankVerification;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Services\Finance\BankVerificationService;
|
||||
use App\Services\Finance\FinanceExportService;
|
||||
use App\Services\Finance\FinanceVerificationQuery;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class BankVerificationController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(Request $request, FinanceVerificationQuery $verificationQuery): View
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
|
||||
return view('kewangan.bank.index', [
|
||||
'verifications' => $this->sortedQuery(
|
||||
$verificationQuery->build($filters),
|
||||
['status'],
|
||||
'id',
|
||||
'desc'
|
||||
)->paginate(15)->withQueryString(),
|
||||
'positions' => Position::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'pusats' => PusatMengundi::query()->orderBy('name')->get(),
|
||||
'filters' => $filters,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(BankVerification $bankVerification): View
|
||||
{
|
||||
return view('kewangan.bank.show', [
|
||||
'verification' => $bankVerification->load([
|
||||
'application.election',
|
||||
'application.pusatMengundi',
|
||||
'application.approvedPosition',
|
||||
'application.requestedPosition',
|
||||
'application.documents',
|
||||
'application.staffAssignments.position',
|
||||
'application.staffAssignments.saluranMengundi',
|
||||
'verifiedBy',
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateBankVerificationRequest $request, BankVerification $bankVerification, BankVerificationService $service): RedirectResponse
|
||||
{
|
||||
$service->update($bankVerification, $request->user(), $request->validated());
|
||||
|
||||
return redirect()->route('kewangan.bank.show', $bankVerification)
|
||||
->with('status', 'Status verifikasi bank telah dikemas kini.');
|
||||
}
|
||||
|
||||
public function export(Request $request, FinanceExportService $exportService): StreamedResponse
|
||||
{
|
||||
$exportLog = $exportService->export($request->user(), $this->filters($request));
|
||||
|
||||
return Storage::disk($exportLog->disk ?? 'local')
|
||||
->download($exportLog->path, $exportLog->file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function filters(Request $request): array
|
||||
{
|
||||
return [
|
||||
'status' => $request->filled('status') ? $request->string('status')->toString() : null,
|
||||
'pusat_mengundi_id' => $request->filled('pusat_mengundi_id') ? $request->integer('pusat_mengundi_id') : null,
|
||||
'role_id' => $request->filled('role_id') ? $request->integer('role_id') : null,
|
||||
'missing_bank_statement' => $request->boolean('missing_bank_statement'),
|
||||
'missing_account_number' => $request->boolean('missing_account_number'),
|
||||
'q' => $request->filled('q') ? $request->string('q')->toString() : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
107
app/Http/Controllers/Ktm/TeamController.php
Normal file
107
app/Http/Controllers/Ktm/TeamController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Ktm;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Ktm\RegisterKpApplicantRequest;
|
||||
use App\Models\Application;
|
||||
use App\Models\Election;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Ktm\KtmApplicationService;
|
||||
use App\Services\Ktm\KtmScopeService;
|
||||
use App\Services\Public\KtmVacancyService;
|
||||
use App\Services\RegistrationPeriodService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TeamController extends Controller
|
||||
{
|
||||
public function index(Request $request, KtmScopeService $scopeService, KtmVacancyService $vacancyService): View
|
||||
{
|
||||
$assignments = $scopeService->assignmentsFor($request->user());
|
||||
$assignmentIds = $assignments->pluck('id');
|
||||
|
||||
return view('ktm.applications.index', [
|
||||
'assignments' => $assignments,
|
||||
'applications' => $this->sortedQuery(
|
||||
Application::query()
|
||||
->with(['pusatMengundi', 'requestedPosition', 'approvedPosition', 'selectedKtmAssignment.saluranMengundi'])
|
||||
->whereIn('selected_ktm_assignment_id', $assignmentIds),
|
||||
['name', 'status', 'created_at'],
|
||||
'created_at',
|
||||
'desc'
|
||||
)->paginate(15)->withQueryString(),
|
||||
'vacancyService' => $vacancyService,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request, KtmScopeService $scopeService, RegistrationPeriodService $registrationPeriodService): View
|
||||
{
|
||||
$assignments = $scopeService->assignmentsFor($request->user());
|
||||
|
||||
return view('ktm.applications.create', [
|
||||
'assignments' => $assignments,
|
||||
'registrationOpenByAssignment' => $assignments
|
||||
->mapWithKeys(fn (StaffAssignment $assignment): array => [
|
||||
$assignment->id => $registrationPeriodService->isOpen($this->assignmentElection($assignment)),
|
||||
])
|
||||
->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(RegisterKpApplicantRequest $request, KtmApplicationService $service): RedirectResponse
|
||||
{
|
||||
$assignment = StaffAssignment::query()->findOrFail($request->validated('ktm_assignment_id'));
|
||||
|
||||
$application = $service->registerKp($assignment, $request->user(), $request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('ktm.applications.show', $application->public_uuid)
|
||||
->with('status', 'Rekod KP telah didaftarkan dan emel makluman telah dihantar.');
|
||||
}
|
||||
|
||||
public function show(Application $application, Request $request, KtmScopeService $scopeService): View
|
||||
{
|
||||
abort_unless($scopeService->canAccessApplication($request->user(), $application), 403);
|
||||
|
||||
return view('ktm.applications.show', [
|
||||
'application' => $application->load([
|
||||
'pusatMengundi',
|
||||
'requestedPosition',
|
||||
'approvedPosition',
|
||||
'selectedKtmAssignment.user',
|
||||
'selectedKtmAssignment.saluranMengundi',
|
||||
'staffAssignments.position',
|
||||
'statusHistories.changedBy',
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function approve(Application $application, Request $request, KtmApplicationService $service): RedirectResponse
|
||||
{
|
||||
$service->approveKp($application, $request->user());
|
||||
|
||||
return redirect()
|
||||
->route('ktm.applications.show', $application->public_uuid)
|
||||
->with('status', 'Permohonan KP telah diluluskan dan diassign kepada team anda.');
|
||||
}
|
||||
|
||||
public function destroy(Application $application, Request $request, KtmApplicationService $service): RedirectResponse
|
||||
{
|
||||
$service->deleteCreatedApplication($application, $request->user());
|
||||
|
||||
return redirect()
|
||||
->route('ktm.applications.index')
|
||||
->with('status', 'Rekod KTM-created telah dipadam. Pemohon kini boleh mendaftar sendiri.');
|
||||
}
|
||||
|
||||
private function assignmentElection(StaffAssignment $assignment): Election
|
||||
{
|
||||
/** @var Election $election */
|
||||
$election = $assignment->election()->with('settings')->firstOrFail();
|
||||
|
||||
return $election;
|
||||
}
|
||||
}
|
||||
131
app/Http/Controllers/Ppm/ApplicationReviewController.php
Normal file
131
app/Http/Controllers/Ppm/ApplicationReviewController.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Ppm;
|
||||
|
||||
use App\Http\Concerns\SortsQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Ppm\ApproveApplicationRequest;
|
||||
use App\Http\Requests\Ppm\RejectApplicationRequest;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDocument;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Services\Ppm\AssignmentVacancyService;
|
||||
use App\Services\Ppm\PpmApplicationReviewService;
|
||||
use App\Services\Ppm\PpmScopeService;
|
||||
use App\Services\Public\KtmVacancyService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ApplicationReviewController extends Controller
|
||||
{
|
||||
use SortsQuery;
|
||||
|
||||
public function index(Request $request, PpmScopeService $scopeService): View
|
||||
{
|
||||
$pusatIds = $scopeService->assignedPusatIds($request->user());
|
||||
$status = $request->string('status')->toString();
|
||||
|
||||
$query = Application::query()
|
||||
->with(['pusatMengundi', 'requestedPosition', 'approvedPosition'])
|
||||
->whereIn('pusat_mengundi_id', $pusatIds)
|
||||
->when($status !== '', fn ($q) => $q->where('status', $status));
|
||||
|
||||
return view('ppm.applications.index', [
|
||||
'applications' => $this->sortedQuery($query, ['name', 'status', 'created_at'], 'created_at', 'desc')
|
||||
->paginate(15)
|
||||
->withQueryString(),
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(
|
||||
Application $application,
|
||||
AssignmentVacancyService $assignmentVacancyService,
|
||||
KtmVacancyService $ktmVacancyService,
|
||||
): View {
|
||||
$this->authorize('view', $application);
|
||||
|
||||
$application->load([
|
||||
'pusatMengundi.daerahMengundi',
|
||||
'pusatMengundi.saluranMengundis',
|
||||
'pusatMengundi.policeEscorts.saluranMengundi',
|
||||
'pusatMengundi.kkmRepresentatives',
|
||||
'pusatMengundi.jkmRepresentatives',
|
||||
'requestedPosition',
|
||||
'approvedPosition',
|
||||
'documents',
|
||||
'selectedKtmAssignment.user',
|
||||
'selectedKtmAssignment.saluranMengundi',
|
||||
'staffAssignments.position',
|
||||
'staffAssignments.saluranMengundi',
|
||||
'staffAssignments.reportsTo.user',
|
||||
'statusHistories.changedBy',
|
||||
]);
|
||||
|
||||
/** @var PusatMengundi $pusatMengundi */
|
||||
$pusatMengundi = $application->pusatMengundi;
|
||||
|
||||
return view('ppm.applications.show', [
|
||||
'application' => $application,
|
||||
'roles' => Position::query()
|
||||
->whereIn('code', ['KTM', 'KP', 'KPDP', 'PAPM'])
|
||||
->where('is_public_applyable', true)
|
||||
->orderBy('sort_order')
|
||||
->get(),
|
||||
'availableKtmSalurans' => $assignmentVacancyService->availableSaluransForRole($pusatMengundi, 'KTM'),
|
||||
'ktmOptionsByRole' => $ktmVacancyService->optionsForRoles($pusatMengundi, ['KP', 'KPDP'], $application->id),
|
||||
]);
|
||||
}
|
||||
|
||||
public function approve(
|
||||
ApproveApplicationRequest $request,
|
||||
Application $application,
|
||||
PpmApplicationReviewService $reviewService,
|
||||
): RedirectResponse {
|
||||
$this->authorize('review', $application);
|
||||
|
||||
$reviewService->approve($application, $request->user(), $request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('ppm.applications.show', $application->public_uuid)
|
||||
->with('status', 'Permohonan telah diluluskan dan assignment telah disimpan.');
|
||||
}
|
||||
|
||||
public function reject(
|
||||
RejectApplicationRequest $request,
|
||||
Application $application,
|
||||
PpmApplicationReviewService $reviewService,
|
||||
): RedirectResponse {
|
||||
$this->authorize('review', $application);
|
||||
|
||||
$reviewService->reject($application, $request->user(), $request->validated('reason'));
|
||||
|
||||
return redirect()
|
||||
->route('ppm.applications.show', $application->public_uuid)
|
||||
->with('status', 'Permohonan telah ditolak.');
|
||||
}
|
||||
|
||||
public function downloadDocument(Application $application, ApplicationDocument $document): StreamedResponse
|
||||
{
|
||||
$this->authorize('downloadDocument', $application);
|
||||
|
||||
abort_unless($document->application_id === $application->id, 403);
|
||||
abort_unless(
|
||||
in_array($document->document_type, ApplicationDocument::ALLOWED_FOR_PPM, true),
|
||||
403
|
||||
);
|
||||
abort_unless(Storage::disk($document->disk)->exists($document->path), 404);
|
||||
|
||||
activity('documents')
|
||||
->performedOn($document)
|
||||
->causedBy(request()->user())
|
||||
->withProperties(['document_type' => $document->document_type])
|
||||
->log('document_downloaded_by_ppm');
|
||||
|
||||
return Storage::disk($document->disk)->download($document->path, $document->original_name);
|
||||
}
|
||||
}
|
||||
66
app/Http/Controllers/Ppm/AttendanceController.php
Normal file
66
app/Http/Controllers/Ppm/AttendanceController.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Ppm;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Ppm\RecordAttendanceRequest;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Services\Attendance\AttendanceRecordingService;
|
||||
use App\Services\Attendance\AttendanceSummaryService;
|
||||
use App\Services\Ppm\PpmScopeService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AttendanceController extends Controller
|
||||
{
|
||||
public function index(Request $request, PpmScopeService $scopeService, AttendanceSummaryService $summaryService): View
|
||||
{
|
||||
$pusatIds = $scopeService->assignedPusatIds($request->user());
|
||||
$pusats = PusatMengundi::query()
|
||||
->with(['election.settings'])
|
||||
->whereIn('id', $pusatIds)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('ppm.attendance.index', [
|
||||
'pusats' => $pusats,
|
||||
'summaryService' => $summaryService,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(PusatMengundi $pusatMengundi, Request $request, PpmScopeService $scopeService, AttendanceSummaryService $summaryService): View
|
||||
{
|
||||
if (! $scopeService->assignedPusatIds($request->user())->contains((int) $pusatMengundi->id)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$assignments = StaffAssignment::query()
|
||||
->with(['application', 'user', 'position', 'saluranMengundi', 'attendance'])
|
||||
->where('pusat_mengundi_id', $pusatMengundi->id)
|
||||
->where('status', 'active')
|
||||
->orderBy('position_id')
|
||||
->orderBy('saluran_mengundi_id')
|
||||
->get();
|
||||
|
||||
return view('ppm.attendance.show', [
|
||||
'pusat' => $pusatMengundi->load('election.settings'),
|
||||
'assignments' => $assignments,
|
||||
'summary' => $summaryService->totalsForPusat($pusatMengundi),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(PusatMengundi $pusatMengundi, RecordAttendanceRequest $request, AttendanceRecordingService $service): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$service->recordBulk($pusatMengundi, $request->user(), $request->validated('attendance'));
|
||||
} catch (ValidationException $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return redirect()->route('ppm.attendance.show', $pusatMengundi)
|
||||
->with('status', 'Kehadiran telah direkodkan.');
|
||||
}
|
||||
}
|
||||
60
app/Http/Controllers/ProfileController.php
Normal file
60
app/Http/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validateWithBag('userDeletion', [
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return Redirect::to('/');
|
||||
}
|
||||
}
|
||||
96
app/Http/Controllers/PublicApplicationController.php
Normal file
96
app/Http/Controllers/PublicApplicationController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Public\StorePublicApplicationRequest;
|
||||
use App\Models\Application;
|
||||
use App\Models\Election;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Services\Public\KtmVacancyService;
|
||||
use App\Services\Public\PublicApplicationSubmissionService;
|
||||
use App\Services\RegistrationPeriodService;
|
||||
use App\Support\SensitiveData;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PublicApplicationController extends Controller
|
||||
{
|
||||
public function create(
|
||||
PusatMengundi $pusatMengundi,
|
||||
RegistrationPeriodService $registrationPeriodService,
|
||||
KtmVacancyService $ktmVacancyService,
|
||||
): View {
|
||||
$pusatMengundi->loadMissing('daerahMengundi');
|
||||
/** @var Election $election */
|
||||
$election = $pusatMengundi->election()->with('settings')->firstOrFail();
|
||||
|
||||
if (! $registrationPeriodService->isOpen($election)) {
|
||||
return view('public.applications.closed', [
|
||||
'pusatMengundi' => $pusatMengundi,
|
||||
'message' => $registrationPeriodService->closedMessage($election),
|
||||
]);
|
||||
}
|
||||
|
||||
return view('public.applications.create', [
|
||||
'pusatMengundi' => $pusatMengundi,
|
||||
'roles' => Position::query()
|
||||
->whereIn('code', ['KTM', 'KP', 'KPDP', 'PAPM'])
|
||||
->where('is_public_applyable', true)
|
||||
->orderBy('sort_order')
|
||||
->get(),
|
||||
'ktmOptionsByRole' => $ktmVacancyService->optionsForRoles($pusatMengundi),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(
|
||||
StorePublicApplicationRequest $request,
|
||||
PusatMengundi $pusatMengundi,
|
||||
PublicApplicationSubmissionService $submissionService,
|
||||
RegistrationPeriodService $registrationPeriodService,
|
||||
): RedirectResponse {
|
||||
/** @var Election $election */
|
||||
$election = $pusatMengundi->election()->with('settings')->firstOrFail();
|
||||
|
||||
if (! $registrationPeriodService->isOpen($election)) {
|
||||
return redirect()
|
||||
->route('public.applications.create', $pusatMengundi->public_uuid)
|
||||
->withErrors(['registration' => $registrationPeriodService->closedMessage($election)]);
|
||||
}
|
||||
|
||||
$application = $submissionService->submit($pusatMengundi, $request->validated());
|
||||
|
||||
return redirect()->route('public.applications.success', $application->public_uuid);
|
||||
}
|
||||
|
||||
public function success(Application $application): View
|
||||
{
|
||||
return view('public.applications.success', [
|
||||
'application' => $application->load(['pusatMengundi', 'requestedPosition']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function status(string $publicUuid): View
|
||||
{
|
||||
// Query with withTrashed so soft-deleted records don't 404, allowing a uniform
|
||||
// response for all unknown/invalid UUIDs to prevent enumeration.
|
||||
$application = Application::withTrashed()
|
||||
->where('public_uuid', $publicUuid)
|
||||
->with(['pusatMengundi', 'requestedPosition', 'approvedPosition'])
|
||||
->first();
|
||||
|
||||
// Treat soft-deleted and KTM-internal records as not found to avoid leaking
|
||||
// information about whether a UUID ever existed.
|
||||
if (! $application instanceof Application
|
||||
|| $application->trashed()
|
||||
|| $application->status === 'deleted_by_ktm'
|
||||
) {
|
||||
return view('public.applications.status', ['application' => null]);
|
||||
}
|
||||
|
||||
return view('public.applications.status', [
|
||||
'application' => $application,
|
||||
'maskedName' => SensitiveData::maskName($application->name),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PusatMengundi;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PublicApplicationPlaceholderController extends Controller
|
||||
{
|
||||
public function create(PusatMengundi $pusatMengundi): View
|
||||
{
|
||||
return view('public.application-placeholder', [
|
||||
'pusatMengundi' => $pusatMengundi->load('daerahMengundi'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
57
app/Http/Middleware/RequireAdminPostCloseNote.php
Normal file
57
app/Http/Middleware/RequireAdminPostCloseNote.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Election;
|
||||
use App\Services\RegistrationPeriodService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RequireAdminPostCloseNote
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RegistrationPeriodService $registrationPeriodService,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Only intercept write operations.
|
||||
if (! in_array($request->method(), ['POST', 'PATCH', 'PUT', 'DELETE'], true)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$election = $this->resolveElection($request);
|
||||
|
||||
if ($election instanceof Election && ! $this->registrationPeriodService->isOpen($election)) {
|
||||
$note = $request->input('note') ?? $request->input('catatan');
|
||||
|
||||
if (blank($note)) {
|
||||
return back()
|
||||
->withInput()
|
||||
->withErrors([
|
||||
'note' => 'Catatan wajib diisi untuk perubahan selepas tempoh pendaftaran ditutup.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// For create routes the election_id is in the request body.
|
||||
// For edit/delete routes the election is resolved from the active election,
|
||||
// as those route models carry their own election context inside the service.
|
||||
private function resolveElection(Request $request): ?Election
|
||||
{
|
||||
$electionId = $request->input('election_id');
|
||||
|
||||
if ($electionId) {
|
||||
return Election::query()->with('settings')->find($electionId);
|
||||
}
|
||||
|
||||
return Election::query()
|
||||
->with('settings')
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AssignApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'saluran_mengundi_id' => blank($this->input('saluran_mengundi_id')) ? null : $this->input('saluran_mengundi_id'),
|
||||
'reports_to_assignment_id' => blank($this->input('reports_to_assignment_id')) ? null : $this->input('reports_to_assignment_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'position_id' => ['required', 'integer', 'exists:positions,id'],
|
||||
'pusat_mengundi_id' => ['required', 'integer', 'exists:pusat_mengundis,id'],
|
||||
'saluran_mengundi_id' => ['nullable', 'integer', 'exists:saluran_mengundis,id'],
|
||||
'reports_to_assignment_id' => ['nullable', 'integer', 'exists:staff_assignments,id'],
|
||||
'status' => ['nullable', Rule::in(['active', 'inactive', 'replaced', 'cancelled'])],
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DeleteRepresentativeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreManualApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ic_number' => preg_replace('/\D+/', '', (string) $this->input('ic_number')),
|
||||
'phone_number' => trim((string) $this->input('phone_number')),
|
||||
'email' => blank($this->input('email')) ? null : strtolower(trim((string) $this->input('email'))),
|
||||
'selected_ktm_assignment_id' => blank($this->input('selected_ktm_assignment_id')) ? null : $this->input('selected_ktm_assignment_id'),
|
||||
'bank_name' => blank($this->input('bank_name')) ? null : trim((string) $this->input('bank_name')),
|
||||
'bank_account_number' => blank($this->input('bank_account_number')) ? null : preg_replace('/\D+/', '', (string) $this->input('bank_account_number')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'election_id' => ['required', 'integer', 'exists:elections,id'],
|
||||
'pusat_mengundi_id' => ['required', 'integer', 'exists:pusat_mengundis,id'],
|
||||
'selected_ktm_assignment_id' => ['nullable', 'integer', 'exists:staff_assignments,id'],
|
||||
'requested_position_id' => ['required', 'integer', 'exists:positions,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'ic_number' => ['required', 'digits:12'],
|
||||
'phone_number' => ['required', 'string', 'max:30'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:2000'],
|
||||
'bank_name' => ['nullable', 'string', 'max:255'],
|
||||
'bank_account_number' => ['nullable', 'string', 'max:50'],
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreRepresentativeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ic_number' => blank($this->input('ic_number')) ? null : preg_replace('/\D+/', '', (string) $this->input('ic_number')),
|
||||
'saluran_mengundi_id' => blank($this->input('saluran_mengundi_id')) ? null : $this->input('saluran_mengundi_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', Rule::in(['police', 'kkm', 'jkm'])],
|
||||
'election_id' => ['required', 'integer', 'exists:elections,id'],
|
||||
'pusat_mengundi_id' => ['required', 'integer', 'exists:pusat_mengundis,id'],
|
||||
'saluran_mengundi_id' => ['nullable', 'integer', 'exists:saluran_mengundis,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'ic_number' => ['nullable', 'digits:12'],
|
||||
'phone_number' => ['nullable', 'string', 'max:30'],
|
||||
'rank' => ['nullable', 'string', 'max:255'],
|
||||
'station' => ['nullable', 'string', 'max:255'],
|
||||
'agency' => ['nullable', 'string', 'max:255'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreReturnTransactionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => ['required', 'integer', 'min:1', 'max:9999'],
|
||||
'returned_at' => ['nullable', 'date'],
|
||||
'returned_by_name' => ['required', 'string', 'max:255'],
|
||||
'return_condition' => ['required', Rule::in(['baik', 'rosak'])],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreTakenTransactionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => ['required', 'integer', 'min:1', 'max:9999'],
|
||||
'taken_at' => ['nullable', 'date'],
|
||||
'taken_by_name' => ['required', 'string', 'max:255'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreWheelchairTransactionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'transaction_type' => ['required', Rule::in(['taken', 'returned'])],
|
||||
'quantity' => ['required', 'integer', 'min:1', 'max:9999'],
|
||||
'taken_at' => ['nullable', 'date'],
|
||||
'taken_by_name' => ['nullable', 'string', 'max:255', 'required_if:transaction_type,taken'],
|
||||
'returned_at' => ['nullable', 'date'],
|
||||
'return_condition' => ['nullable', Rule::in(['baik', 'rosak']), 'required_if:transaction_type,returned'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateManagedApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ic_number' => preg_replace('/\D+/', '', (string) $this->input('ic_number')),
|
||||
'phone_number' => trim((string) $this->input('phone_number')),
|
||||
'email' => blank($this->input('email')) ? null : strtolower(trim((string) $this->input('email'))),
|
||||
'selected_ktm_assignment_id' => blank($this->input('selected_ktm_assignment_id')) ? null : $this->input('selected_ktm_assignment_id'),
|
||||
'approved_position_id' => blank($this->input('approved_position_id')) ? null : $this->input('approved_position_id'),
|
||||
'bank_name' => blank($this->input('bank_name')) ? null : trim((string) $this->input('bank_name')),
|
||||
'bank_account_number' => blank($this->input('bank_account_number')) ? null : preg_replace('/\D+/', '', (string) $this->input('bank_account_number')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'pusat_mengundi_id' => ['required', 'integer', 'exists:pusat_mengundis,id'],
|
||||
'selected_ktm_assignment_id' => ['nullable', 'integer', 'exists:staff_assignments,id'],
|
||||
'requested_position_id' => ['required', 'integer', 'exists:positions,id'],
|
||||
'approved_position_id' => ['nullable', 'integer', 'exists:positions,id'],
|
||||
'status' => ['required', Rule::in(['draft', 'submitted', 'under_ppm_review', 'approved', 'rejected', 'assigned', 'cancelled'])],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'ic_number' => ['required', 'digits:12'],
|
||||
'phone_number' => ['required', 'string', 'max:30'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:2000'],
|
||||
'bank_name' => ['nullable', 'string', 'max:255'],
|
||||
'bank_account_number' => ['nullable', 'string', 'max:50'],
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateStaffAssignmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'saluran_mengundi_id' => blank($this->input('saluran_mengundi_id')) ? null : $this->input('saluran_mengundi_id'),
|
||||
'reports_to_assignment_id' => blank($this->input('reports_to_assignment_id')) ? null : $this->input('reports_to_assignment_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'position_id' => ['required', 'integer', 'exists:positions,id'],
|
||||
'pusat_mengundi_id' => ['required', 'integer', 'exists:pusat_mengundis,id'],
|
||||
'saluran_mengundi_id' => ['nullable', 'integer', 'exists:saluran_mengundis,id'],
|
||||
'reports_to_assignment_id' => ['nullable', 'integer', 'exists:staff_assignments,id'],
|
||||
'status' => ['required', Rule::in(['active', 'inactive', 'replaced', 'cancelled'])],
|
||||
'note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Management;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpsertWheelchairAllocationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'election_id' => ['required', 'integer', 'exists:elections,id'],
|
||||
'pusat_mengundi_id' => ['required', 'integer', 'exists:pusat_mengundis,id'],
|
||||
'allocated_quantity' => ['required', 'integer', 'min:0', 'max:9999'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/Admin/Setup/AssignPpmRequest.php
Normal file
23
app/Http/Requests/Admin/Setup/AssignPpmRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AssignPpmRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => ['required', 'exists:users,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/Admin/Setup/ImportPusatRequest.php
Normal file
39
app/Http/Requests/Admin/Setup/ImportPusatRequest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ImportPusatRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'election_id' => ['required', 'integer', 'exists:elections,id'],
|
||||
'dun_code' => ['required', 'string', 'max:20'],
|
||||
'dun_name' => ['required', 'string', 'max:255'],
|
||||
'file' => ['required', 'file', 'mimes:xlsx', 'max:10240'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'election_id' => 'pilihan raya',
|
||||
'dun_code' => 'kod DUN',
|
||||
'dun_name' => 'nama DUN',
|
||||
'file' => 'fail XLSX',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use App\Models\BahagianPilihanraya;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpsertBahagianPilihanrayaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$bahagian = $this->route('bahagianPilihanraya');
|
||||
|
||||
return [
|
||||
'election_id' => ['required', 'exists:elections,id'],
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('bahagian_pilihanrayas', 'code')
|
||||
->where('election_id', $this->input('election_id'))
|
||||
->ignore($bahagian instanceof BahagianPilihanraya ? $bahagian->id : null),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use App\Models\DaerahMengundi;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpsertDaerahMengundiRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$daerah = $this->route('daerahMengundi');
|
||||
|
||||
return [
|
||||
'election_id' => ['required', 'exists:elections,id'],
|
||||
'bahagian_pilihanraya_id' => ['required', 'exists:bahagian_pilihanrayas,id'],
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('daerah_mengundis', 'code')
|
||||
->where('election_id', $this->input('election_id'))
|
||||
->ignore($daerah instanceof DaerahMengundi ? $daerah->id : null),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpsertElectionSettingsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'registration_start_date' => ['required', 'date'],
|
||||
'registration_end_date' => ['required', 'date', 'after_or_equal:registration_start_date'],
|
||||
'polling_date' => ['required', 'date', 'after_or_equal:registration_end_date'],
|
||||
'is_attendance_active' => ['required', 'boolean'],
|
||||
'is_registration_open_override' => ['nullable', 'in:,0,1'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_attendance_active' => $this->boolean('is_attendance_active'),
|
||||
'is_registration_open_override' => $this->resolveOverride(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveOverride(): ?bool
|
||||
{
|
||||
$value = $this->input('is_registration_open_override');
|
||||
|
||||
if ($value === '' || $value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
}
|
||||
24
app/Http/Requests/Admin/Setup/UpsertPositionQuotaRequest.php
Normal file
24
app/Http/Requests/Admin/Setup/UpsertPositionQuotaRequest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpsertPositionQuotaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'position_id' => ['required', 'exists:positions,id'],
|
||||
'quota' => ['required', 'integer', 'min:0', 'max:9999'],
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Http/Requests/Admin/Setup/UpsertPositionRequest.php
Normal file
38
app/Http/Requests/Admin/Setup/UpsertPositionRequest.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use App\Models\Position;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpsertPositionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$position = $this->route('position');
|
||||
|
||||
return [
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('positions', 'code')->ignore($position instanceof Position ? $position->id : null),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'scope' => ['required', Rule::in(['pusat', 'saluran', 'external'])],
|
||||
'is_public_applyable' => ['nullable', 'boolean'],
|
||||
'is_assignable' => ['nullable', 'boolean'],
|
||||
'allows_dual_role' => ['nullable', 'boolean'],
|
||||
'sort_order' => ['nullable', 'integer', 'min:0', 'max:999'],
|
||||
];
|
||||
}
|
||||
}
|
||||
50
app/Http/Requests/Admin/Setup/UpsertPusatMengundiRequest.php
Normal file
50
app/Http/Requests/Admin/Setup/UpsertPusatMengundiRequest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use App\Models\PusatMengundi;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpsertPusatMengundiRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$pusatMengundi = $this->route('pusatMengundi');
|
||||
$pusatId = $pusatMengundi instanceof PusatMengundi ? $pusatMengundi->id : null;
|
||||
$operationalQuantity = $this->isMethod('post') ? ['required', 'integer'] : ['sometimes', 'integer'];
|
||||
|
||||
return [
|
||||
'election_id' => ['required', 'integer', 'exists:elections,id'],
|
||||
'dun_id' => ['nullable', 'integer', 'exists:duns,id'],
|
||||
'code' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('pusat_mengundis', 'code')
|
||||
->where(fn ($query) => $query->where('election_id', $this->input('election_id')))
|
||||
->ignore($pusatId),
|
||||
],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:2000'],
|
||||
'saluran_count' => [...$operationalQuantity, 'min:1', 'max:100'],
|
||||
'ktm_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'polis_iring_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'kp_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'ppm_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'kpdp_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'papm_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'jkm_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'kkm_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
'calon_tambahan_count' => [...$operationalQuantity, 'min:0', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Setup;
|
||||
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Models\SaluranMengundi;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpsertSaluranMengundiRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('Admin') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$pusatMengundi = $this->route('pusatMengundi');
|
||||
$pusatId = $pusatMengundi instanceof PusatMengundi ? $pusatMengundi->id : null;
|
||||
|
||||
$saluranMengundi = $this->route('saluranMengundi');
|
||||
$saluranId = $saluranMengundi instanceof SaluranMengundi ? $saluranMengundi->id : null;
|
||||
|
||||
return [
|
||||
'number' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
Rule::unique('saluran_mengundis', 'number')
|
||||
->where(fn ($query) => $query->where('pusat_mengundi_id', $pusatId))
|
||||
->ignore($saluranId),
|
||||
],
|
||||
'voter_count' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
86
app/Http/Requests/Auth/LoginRequest.php
Normal file
86
app/Http/Requests/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the login request is not rate limited.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout($this));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rate limiting throttle key for the request.
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||
}
|
||||
}
|
||||
25
app/Http/Requests/Kewangan/UpdateBankVerificationRequest.php
Normal file
25
app/Http/Requests/Kewangan/UpdateBankVerificationRequest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Kewangan;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateBankVerificationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasAnyRole(['Admin Kewangan', 'Admin']) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => ['required', Rule::in(['pending', 'verified', 'rejected', 'requires_correction'])],
|
||||
'finance_note' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/Ktm/RegisterKpApplicantRequest.php
Normal file
36
app/Http/Requests/Ktm/RegisterKpApplicantRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ktm;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RegisterKpApplicantRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('KTM') === true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ic_number' => preg_replace('/\D+/', '', (string) $this->input('ic_number')),
|
||||
'phone_number' => trim((string) $this->input('phone_number')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ktm_assignment_id' => ['required', 'integer', 'exists:staff_assignments,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'ic_number' => ['required', 'digits:12'],
|
||||
'phone_number' => ['required', 'string', 'max:30'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Http/Requests/Ppm/ApproveApplicationRequest.php
Normal file
35
app/Http/Requests/Ppm/ApproveApplicationRequest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ppm;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ApproveApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('PPM') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'approved_position_code' => ['required', Rule::in(['KTM', 'KP', 'KPDP', 'PAPM'])],
|
||||
'saluran_mengundi_id' => [
|
||||
Rule::requiredIf(fn (): bool => $this->input('approved_position_code') === 'KTM'),
|
||||
'nullable',
|
||||
'integer',
|
||||
],
|
||||
'ktm_assignment_id' => [
|
||||
Rule::requiredIf(fn (): bool => in_array($this->input('approved_position_code'), ['KP', 'KPDP'], true)),
|
||||
'nullable',
|
||||
'integer',
|
||||
],
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Http/Requests/Ppm/RecordAttendanceRequest.php
Normal file
27
app/Http/Requests/Ppm/RecordAttendanceRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ppm;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RecordAttendanceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('PPM') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'attendance' => ['required', 'array'],
|
||||
'attendance.*.status' => ['required', Rule::in(['present', 'absent', 'not_recorded'])],
|
||||
'attendance.*.check_in_time' => ['nullable', 'date'],
|
||||
'attendance.*.note' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/Ppm/RejectApplicationRequest.php
Normal file
23
app/Http/Requests/Ppm/RejectApplicationRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Ppm;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RejectApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->hasRole('PPM') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reason' => ['required', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/ProfileUpdateRequest.php
Normal file
31
app/Http/Requests/ProfileUpdateRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'lowercase',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class)->ignore($this->user()->id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
158
app/Http/Requests/Public/StorePublicApplicationRequest.php
Normal file
158
app/Http/Requests/Public/StorePublicApplicationRequest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Public;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Election;
|
||||
use App\Models\Position;
|
||||
use App\Models\PusatMengundi;
|
||||
use App\Services\Public\KtmVacancyService;
|
||||
use App\Services\RegistrationPeriodService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class StorePublicApplicationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'ic_number' => preg_replace('/\D+/', '', (string) $this->input('ic_number')),
|
||||
'phone_number' => trim((string) $this->input('phone_number')),
|
||||
'bank_account_number' => preg_replace('/[\s-]+/', '', (string) $this->input('bank_account_number')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'ic_number' => ['required', 'digits:12'],
|
||||
'phone_number' => ['required', 'string', 'max:30'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'address' => ['required', 'string', 'max:1000'],
|
||||
'requested_position_code' => ['required', Rule::in(['KTM', 'KP', 'KPDP', 'PAPM'])],
|
||||
'selected_ktm_assignment_id' => [
|
||||
Rule::requiredIf(fn (): bool => in_array($this->input('requested_position_code'), ['KP', 'KPDP'], true)),
|
||||
'nullable',
|
||||
'integer',
|
||||
],
|
||||
'bank_name' => ['required', 'string', 'max:255'],
|
||||
'bank_account_number' => ['required', 'string', 'max:50'],
|
||||
'ic_document' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:5120'],
|
||||
'bank_statement' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:5120'],
|
||||
];
|
||||
}
|
||||
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (Validator $validator): void {
|
||||
$pusatMengundi = $this->route('pusatMengundi');
|
||||
|
||||
if (! $pusatMengundi instanceof PusatMengundi) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Election $election */
|
||||
$election = $pusatMengundi->election()->with('settings')->firstOrFail();
|
||||
|
||||
if (! app(RegistrationPeriodService::class)->isOpen($election)) {
|
||||
$validator->errors()->add('registration', app(RegistrationPeriodService::class)->closedMessage($election));
|
||||
}
|
||||
|
||||
$roleCode = (string) $this->input('requested_position_code');
|
||||
$position = Position::query()
|
||||
->where('code', $roleCode)
|
||||
->where('is_public_applyable', true)
|
||||
->first();
|
||||
|
||||
if (! $position instanceof Position) {
|
||||
$validator->errors()->add('requested_position_code', 'Jawatan yang dipilih tidak dibuka untuk permohonan awam.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validateDuplicateIc($validator, $pusatMengundi);
|
||||
|
||||
if (in_array($roleCode, ['KP', 'KPDP'], true)) {
|
||||
$this->validateKtmVacancy($validator, $pusatMengundi, $roleCode);
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'nama',
|
||||
'ic_number' => 'no. kad pengenalan',
|
||||
'phone_number' => 'no. telefon',
|
||||
'email' => 'emel',
|
||||
'address' => 'alamat',
|
||||
'requested_position_code' => 'jawatan dipohon',
|
||||
'selected_ktm_assignment_id' => 'KTM',
|
||||
'bank_name' => 'nama bank',
|
||||
'bank_account_number' => 'no. akaun bank',
|
||||
'ic_document' => 'dokumen kad pengenalan',
|
||||
'bank_statement' => 'penyata bank',
|
||||
];
|
||||
}
|
||||
|
||||
private function validateDuplicateIc(Validator $validator, PusatMengundi $pusatMengundi): void
|
||||
{
|
||||
// SoftDeletes scope automatically excludes deleted_at IS NOT NULL records (e.g. soft-deleted
|
||||
// KTM-created applicants). The whereNotIn further excludes 'cancelled' records (which are
|
||||
// NOT soft-deleted) and 'deleted_by_ktm' records as an explicit safety layer.
|
||||
$existing = Application::query()
|
||||
->where('election_id', $pusatMengundi->election_id)
|
||||
->where('ic_number', $this->input('ic_number'))
|
||||
->whereNotIn('status', ['cancelled', 'deleted_by_ktm'])
|
||||
->first();
|
||||
|
||||
if (! $existing instanceof Application) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($existing->source === 'created_by_ktm') {
|
||||
$validator->errors()->add(
|
||||
'ic_number',
|
||||
'No. kad pengenalan ini telah didaftarkan oleh KTM. KTM perlu memadam rekod tersebut sebelum pemohon boleh mendaftar sendiri.',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$validator->errors()->add(
|
||||
'ic_number',
|
||||
'No. kad pengenalan ini telah mempunyai permohonan untuk pilihanraya semasa.',
|
||||
);
|
||||
}
|
||||
|
||||
private function validateKtmVacancy(Validator $validator, PusatMengundi $pusatMengundi, string $roleCode): void
|
||||
{
|
||||
$assignmentId = (int) $this->input('selected_ktm_assignment_id');
|
||||
|
||||
if ($assignmentId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! app(KtmVacancyService::class)->hasVacancyForAssignment($assignmentId, $pusatMengundi, $roleCode)) {
|
||||
$validator->errors()->add(
|
||||
'selected_ktm_assignment_id',
|
||||
'KTM yang dipilih tidak sah atau kekosongan untuk jawatan ini telah penuh.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/Mail/KtmCreatedApplicantMail.php
Normal file
36
app/Mail/KtmCreatedApplicantMail.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\StaffAssignment;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class KtmCreatedApplicantMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public Application $application,
|
||||
public StaffAssignment $ktmAssignment,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Makluman Pendaftaran KP oleh KTM',
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.ktm.created-applicant',
|
||||
);
|
||||
}
|
||||
}
|
||||
36
app/Mail/OperationalAccountCreatedMail.php
Normal file
36
app/Mail/OperationalAccountCreatedMail.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class OperationalAccountCreatedMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public User $user,
|
||||
public string $roleCode,
|
||||
public string $tempPassword,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Akaun Login Sistem Pengurusan Pilihanraya — '.$this->roleCode,
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.staff.operational-account-created',
|
||||
);
|
||||
}
|
||||
}
|
||||
77
app/Models/Application.php
Normal file
77
app/Models/Application.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\ApplicationFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Application extends Model
|
||||
{
|
||||
/** @use HasFactory<ApplicationFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'approved_at' => 'datetime',
|
||||
'rejected_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function selectedKtmAssignment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StaffAssignment::class, 'selected_ktm_assignment_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function requestedPosition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class, 'requested_position_id');
|
||||
}
|
||||
|
||||
public function approvedPosition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class, 'approved_position_id');
|
||||
}
|
||||
|
||||
public function documents(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApplicationDocument::class);
|
||||
}
|
||||
|
||||
public function statusHistories(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApplicationStatusHistory::class);
|
||||
}
|
||||
|
||||
public function staffAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignment::class);
|
||||
}
|
||||
|
||||
public function bankVerification(): HasOne
|
||||
{
|
||||
return $this->hasOne(BankVerification::class);
|
||||
}
|
||||
}
|
||||
37
app/Models/ApplicationDocument.php
Normal file
37
app/Models/ApplicationDocument.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ApplicationDocument extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
public const TYPE_IC_DOCUMENT = 'ic_document';
|
||||
|
||||
public const TYPE_BANK_STATEMENT = 'bank_statement';
|
||||
|
||||
/** @var list<string> */
|
||||
public const ALLOWED_FOR_ADMIN = [self::TYPE_IC_DOCUMENT, self::TYPE_BANK_STATEMENT];
|
||||
|
||||
/** @var list<string> */
|
||||
public const ALLOWED_FOR_PPM = [self::TYPE_IC_DOCUMENT, self::TYPE_BANK_STATEMENT];
|
||||
|
||||
/** @var list<string> */
|
||||
public const ALLOWED_FOR_FINANCE = [self::TYPE_BANK_STATEMENT];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function application(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Application::class);
|
||||
}
|
||||
|
||||
public function uploadedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by_user_id');
|
||||
}
|
||||
}
|
||||
28
app/Models/ApplicationStatusHistory.php
Normal file
28
app/Models/ApplicationStatusHistory.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ApplicationStatusHistory extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'metadata' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function application(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Application::class);
|
||||
}
|
||||
|
||||
public function changedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'changed_by_user_id');
|
||||
}
|
||||
}
|
||||
48
app/Models/Attendance.php
Normal file
48
app/Models/Attendance.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Attendance extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'check_in_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function staffAssignment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StaffAssignment::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function saluranMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SaluranMengundi::class);
|
||||
}
|
||||
|
||||
public function position(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class);
|
||||
}
|
||||
|
||||
public function recordedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'recorded_by_user_id');
|
||||
}
|
||||
}
|
||||
28
app/Models/BahagianPilihanraya.php
Normal file
28
app/Models/BahagianPilihanraya.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\BahagianPilihanrayaFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BahagianPilihanraya extends Model
|
||||
{
|
||||
/** @use HasFactory<BahagianPilihanrayaFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function daerahMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(DaerahMengundi::class);
|
||||
}
|
||||
}
|
||||
28
app/Models/BankVerification.php
Normal file
28
app/Models/BankVerification.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BankVerification extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'verified_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function application(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Application::class);
|
||||
}
|
||||
|
||||
public function verifiedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'verified_by_user_id');
|
||||
}
|
||||
}
|
||||
33
app/Models/DaerahMengundi.php
Normal file
33
app/Models/DaerahMengundi.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\DaerahMengundiFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class DaerahMengundi extends Model
|
||||
{
|
||||
/** @use HasFactory<DaerahMengundiFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function bahagianPilihanraya(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BahagianPilihanraya::class);
|
||||
}
|
||||
|
||||
public function pusatMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(PusatMengundi::class);
|
||||
}
|
||||
}
|
||||
28
app/Models/Dun.php
Normal file
28
app/Models/Dun.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\DunFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Dun extends Model
|
||||
{
|
||||
/** @use HasFactory<DunFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(PusatMengundi::class);
|
||||
}
|
||||
}
|
||||
66
app/Models/Election.php
Normal file
66
app/Models/Election.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\ElectionFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Election extends Model
|
||||
{
|
||||
/** @use HasFactory<ElectionFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'starts_at' => 'date',
|
||||
'polling_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function settings(): HasOne
|
||||
{
|
||||
return $this->hasOne(ElectionSetting::class);
|
||||
}
|
||||
|
||||
public function duns(): HasMany
|
||||
{
|
||||
return $this->hasMany(Dun::class);
|
||||
}
|
||||
|
||||
public function bahagianPilihanrayas(): HasMany
|
||||
{
|
||||
return $this->hasMany(BahagianPilihanraya::class);
|
||||
}
|
||||
|
||||
public function daerahMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(DaerahMengundi::class);
|
||||
}
|
||||
|
||||
public function pusatMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function saluranMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(SaluranMengundi::class);
|
||||
}
|
||||
|
||||
public function applications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Application::class);
|
||||
}
|
||||
|
||||
public function staffAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignment::class);
|
||||
}
|
||||
}
|
||||
28
app/Models/ElectionSetting.php
Normal file
28
app/Models/ElectionSetting.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ElectionSetting extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'registration_start_date' => 'date',
|
||||
'registration_end_date' => 'date',
|
||||
'polling_date' => 'date',
|
||||
'is_registration_open' => 'boolean',
|
||||
'is_registration_open_override' => 'boolean',
|
||||
'is_attendance_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
}
|
||||
35
app/Models/ExportLog.php
Normal file
35
app/Models/ExportLog.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ExportLog extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'generated_at' => 'datetime',
|
||||
'filter_parameters' => 'array',
|
||||
'purged_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function generatedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'generated_by_user_id');
|
||||
}
|
||||
|
||||
public function purgedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'purged_by_user_id');
|
||||
}
|
||||
}
|
||||
24
app/Models/JkmRepresentative.php
Normal file
24
app/Models/JkmRepresentative.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class JkmRepresentative extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
}
|
||||
24
app/Models/KkmRepresentative.php
Normal file
24
app/Models/KkmRepresentative.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class KkmRepresentative extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
}
|
||||
29
app/Models/PoliceEscort.php
Normal file
29
app/Models/PoliceEscort.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PoliceEscort extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function saluranMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SaluranMengundi::class);
|
||||
}
|
||||
}
|
||||
41
app/Models/Position.php
Normal file
41
app/Models/Position.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\PositionFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Position extends Model
|
||||
{
|
||||
/** @use HasFactory<PositionFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_public_applyable' => 'boolean',
|
||||
'is_assignable' => 'boolean',
|
||||
'allows_dual_role' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function quotas(): HasMany
|
||||
{
|
||||
return $this->hasMany(PositionQuota::class);
|
||||
}
|
||||
|
||||
public function applications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Application::class, 'requested_position_id');
|
||||
}
|
||||
|
||||
public function staffAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignment::class);
|
||||
}
|
||||
}
|
||||
34
app/Models/PositionQuota.php
Normal file
34
app/Models/PositionQuota.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PositionQuota extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function saluranMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SaluranMengundi::class);
|
||||
}
|
||||
|
||||
public function position(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class);
|
||||
}
|
||||
}
|
||||
74
app/Models/PusatMengundi.php
Normal file
74
app/Models/PusatMengundi.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\PusatMengundiFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PusatMengundi extends Model
|
||||
{
|
||||
/** @use HasFactory<PusatMengundiFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function dun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Dun::class);
|
||||
}
|
||||
|
||||
public function daerahMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DaerahMengundi::class);
|
||||
}
|
||||
|
||||
public function saluranMengundis(): HasMany
|
||||
{
|
||||
return $this->hasMany(SaluranMengundi::class);
|
||||
}
|
||||
|
||||
public function positionQuotas(): HasMany
|
||||
{
|
||||
return $this->hasMany(PositionQuota::class);
|
||||
}
|
||||
|
||||
public function applications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Application::class);
|
||||
}
|
||||
|
||||
public function policeEscorts(): HasMany
|
||||
{
|
||||
return $this->hasMany(PoliceEscort::class);
|
||||
}
|
||||
|
||||
public function kkmRepresentatives(): HasMany
|
||||
{
|
||||
return $this->hasMany(KkmRepresentative::class);
|
||||
}
|
||||
|
||||
public function jkmRepresentatives(): HasMany
|
||||
{
|
||||
return $this->hasMany(JkmRepresentative::class);
|
||||
}
|
||||
|
||||
public function staffAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignment::class);
|
||||
}
|
||||
|
||||
public function wheelchairAllocation(): HasOne
|
||||
{
|
||||
return $this->hasOne(WheelchairAllocation::class);
|
||||
}
|
||||
}
|
||||
38
app/Models/SaluranMengundi.php
Normal file
38
app/Models/SaluranMengundi.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\SaluranMengundiFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class SaluranMengundi extends Model
|
||||
{
|
||||
/** @use HasFactory<SaluranMengundiFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function positionQuotas(): HasMany
|
||||
{
|
||||
return $this->hasMany(PositionQuota::class);
|
||||
}
|
||||
|
||||
public function staffAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignment::class);
|
||||
}
|
||||
}
|
||||
76
app/Models/StaffAssignment.php
Normal file
76
app/Models/StaffAssignment.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\StaffAssignmentFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StaffAssignment extends Model
|
||||
{
|
||||
/** @use HasFactory<StaffAssignmentFactory> */
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'assigned_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function application(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Application::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function position(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Position::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function saluranMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SaluranMengundi::class);
|
||||
}
|
||||
|
||||
public function reportsTo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'reports_to_assignment_id');
|
||||
}
|
||||
|
||||
public function teamMembers(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'reports_to_assignment_id');
|
||||
}
|
||||
|
||||
public function histories(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignmentHistory::class);
|
||||
}
|
||||
|
||||
public function attendance(): HasOne
|
||||
{
|
||||
return $this->hasOne(Attendance::class);
|
||||
}
|
||||
}
|
||||
21
app/Models/StaffAssignmentHistory.php
Normal file
21
app/Models/StaffAssignmentHistory.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class StaffAssignmentHistory extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function staffAssignment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StaffAssignment::class);
|
||||
}
|
||||
|
||||
public function changedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'changed_by_user_id');
|
||||
}
|
||||
}
|
||||
27
app/Models/SystemNote.php
Normal file
27
app/Models/SystemNote.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class SystemNote extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function noteable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function createdBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
50
app/Models/User.php
Normal file
50
app/Models/User.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, HasRoles, Notifiable;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(UserProfile::class);
|
||||
}
|
||||
|
||||
public function applications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Application::class);
|
||||
}
|
||||
|
||||
public function staffAssignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(StaffAssignment::class);
|
||||
}
|
||||
}
|
||||
16
app/Models/UserProfile.php
Normal file
16
app/Models/UserProfile.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserProfile extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
27
app/Models/WheelchairAllocation.php
Normal file
27
app/Models/WheelchairAllocation.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class WheelchairAllocation extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function election(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Election::class);
|
||||
}
|
||||
|
||||
public function pusatMengundi(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PusatMengundi::class);
|
||||
}
|
||||
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(WheelchairTransaction::class);
|
||||
}
|
||||
}
|
||||
29
app/Models/WheelchairTransaction.php
Normal file
29
app/Models/WheelchairTransaction.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WheelchairTransaction extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'taken_at' => 'datetime',
|
||||
'returned_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function allocation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WheelchairAllocation::class, 'wheelchair_allocation_id');
|
||||
}
|
||||
|
||||
public function recordedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'recorded_by_user_id');
|
||||
}
|
||||
}
|
||||
26
app/Policies/ApplicationPolicy.php
Normal file
26
app/Policies/ApplicationPolicy.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\User;
|
||||
use App\Services\Ppm\PpmScopeService;
|
||||
|
||||
class ApplicationPolicy
|
||||
{
|
||||
public function view(User $user, Application $application): bool
|
||||
{
|
||||
return app(PpmScopeService::class)->canAccessApplication($user, $application);
|
||||
}
|
||||
|
||||
public function review(User $user, Application $application): bool
|
||||
{
|
||||
return $this->view($user, $application)
|
||||
&& in_array($application->status, ['submitted', 'under_ppm_review', 'approved'], true);
|
||||
}
|
||||
|
||||
public function downloadDocument(User $user, Application $application): bool
|
||||
{
|
||||
return $this->view($user, $application);
|
||||
}
|
||||
}
|
||||
16
app/Providers/AppServiceProvider.php
Normal file
16
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void {}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
Paginator::useBootstrapFive();
|
||||
}
|
||||
}
|
||||
67
app/Services/Admin/ElectionSettingsService.php
Normal file
67
app/Services/Admin/ElectionSettingsService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\Election;
|
||||
use App\Models\ElectionSetting;
|
||||
use App\Models\User;
|
||||
|
||||
class ElectionSettingsService
|
||||
{
|
||||
public function create(Election $election, array $data, User $actingUser): ElectionSetting
|
||||
{
|
||||
$settings = $election->settings()->create([
|
||||
'registration_start_date' => $data['registration_start_date'],
|
||||
'registration_end_date' => $data['registration_end_date'],
|
||||
'polling_date' => $data['polling_date'],
|
||||
'is_attendance_active' => $data['is_attendance_active'],
|
||||
'is_registration_open_override' => $data['is_registration_open_override'],
|
||||
]);
|
||||
|
||||
activity('admin_settings')
|
||||
->performedOn($settings)
|
||||
->causedBy($actingUser)
|
||||
->withProperties(['election_id' => $election->id, 'after' => $data])
|
||||
->log('election_settings_created');
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function update(Election $election, array $data, User $actingUser): void
|
||||
{
|
||||
/** @var ElectionSetting $settings */
|
||||
$settings = $election->settings;
|
||||
|
||||
$before = $settings->only([
|
||||
'registration_start_date',
|
||||
'registration_end_date',
|
||||
'polling_date',
|
||||
'is_attendance_active',
|
||||
'is_registration_open_override',
|
||||
]);
|
||||
|
||||
$settings->update([
|
||||
'registration_start_date' => $data['registration_start_date'],
|
||||
'registration_end_date' => $data['registration_end_date'],
|
||||
'polling_date' => $data['polling_date'],
|
||||
'is_attendance_active' => $data['is_attendance_active'],
|
||||
'is_registration_open_override' => $data['is_registration_open_override'],
|
||||
]);
|
||||
|
||||
activity('admin_settings')
|
||||
->performedOn($settings)
|
||||
->causedBy($actingUser)
|
||||
->withProperties([
|
||||
'election_id' => $election->id,
|
||||
'before' => $before,
|
||||
'after' => $settings->only([
|
||||
'registration_start_date',
|
||||
'registration_end_date',
|
||||
'polling_date',
|
||||
'is_attendance_active',
|
||||
'is_registration_open_override',
|
||||
]),
|
||||
])
|
||||
->log('election_settings_updated');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user