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