This commit is contained in:
Saufi
2026-06-03 08:51:22 +08:00
commit a14d43fe34
347 changed files with 38197 additions and 0 deletions

View 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);
}
}

View File

@@ -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)),
],
]);
}
}

View File

@@ -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(),
];
}
}

View 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.'.');
}
}

View File

@@ -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),
};
}
}

View File

@@ -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.');
}
}

View File

@@ -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.');
}
}

View 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(),
],
]);
}
}

View File

@@ -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.');
}
}

View File

@@ -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.');
}
}

View File

@@ -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.');
}
}

View 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'),
];
}
}

View 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.');
}
}

View 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);
}
}

View File

@@ -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.');
}
}

View 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.');
}
}

View 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\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.');
}
}

View 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('/');
}
}

View 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));
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View 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)]);
}
}

View 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');
}
}

View 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)]);
}
}

View 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));
}
}

View 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');
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
abstract class Controller
{
use AuthorizesRequests;
}

View 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');
}
}

View 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);
}
}

View 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,
];
}
}

View 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;
}
}

View 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);
}
}

View 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.');
}
}

View 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('/');
}
}

View 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),
]);
}
}

View File

@@ -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'),
]);
}
}