Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ LONGITUDE_MAP = 115.046600
API_DATABASE_GABUNGAN_HOST=http://127.0.0.1:8001
SANCTUM_STATEFUL_DOMAINS=localhost

# DataSpasial - API Satu Data (App\Services\DataSpasialApiService)
# Base URL & token mengikuti pengaturan database gabungan (API_DATABASE_GABUNGAN_HOST
# dan Setting `database_gabungan_api_key`); TIDAK ada env khusus untuk keduanya.
# Batas waktu permintaan (detik) & masa cache baca (detik, 0 = nonaktif).
API_DATASPASIAL_TIMEOUT=10
API_DATASPASIAL_CACHE_TTL=300

# OTP Configuration
OTP_TOKEN_EXPIRES_MINUTES=5
OTP_MAX_VERIFICATION_ATTEMPTS=3
Expand Down
78 changes: 78 additions & 0 deletions app/Enums/DataSpasialReason.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use BenSampo\Enum\Enum;

/**
* Penyebab data DataSpasial tidak dapat diperoleh.
*
* CATATAN PENTING: enum ini hanya untuk keperluan diagnosa (log backend).
* `ModuleNotInstalled` dan `ServiceUnavailable` sengaja menghasilkan
* `DataSpasialStatus::Unavailable` yang sama sehingga UI tidak perlu
* membedakan keduanya maupun memetakan teks pesan dari API (FR-014).
*
* @method static static NotConfigured()
* @method static static Timeout()
* @method static static ConnectionFailed()
* @method static static ServiceUnavailable()
* @method static static ModuleNotInstalled()
* @method static static Unauthorized()
* @method static static SchemaVersionUnsupported()
* @method static static ClientError()
*/
final class DataSpasialReason extends Enum
{
/**
* URL dasar atau token API belum dikonfigurasi (FR-003).
*/
public const NotConfigured = 'not_configured';

/**
* API tidak menjawab dalam batas waktu (FR-004).
*/
public const Timeout = 'timeout';

/**
* Kegagalan koneksi: DNS, TCP, atau TLS.
*/
public const ConnectionFailed = 'connection_failed';

/**
* Status 5xx selain 503.
*/
public const ServiceUnavailable = 'http_5xx';

/**
* Status 503 pada endpoint DataSpasial: modul belum terpasang atau
* versinya tidak cocok (API-02 / #467).
*/
public const ModuleNotInstalled = 'module_not_installed';

/**
* Status 401 (token ditolak) atau 403 (ability tidak cukup).
*/
public const Unauthorized = 'unauthorized';

/**
* Status 400 dengan error_code SchemaVersionUnsupported.
*/
public const SchemaVersionUnsupported = 'schema_version_unsupported';

/**
* Status 4xx lainnya: 404, 409, 422.
*/
public const ClientError = 'client_error';

/**
* Pesan yang ditampilkan ke pengguna akhir untuk semua penyebab di atas.
* WAJIB generik — jangan pernah menyertakan detail teknis (FR-014/FR-016).
*/
public static function userMessage(): string
{
return config('dataspasial.unavailable_message')
?? 'Data sementara tidak tersedia. Silakan coba lagi nanti.';
}
}
36 changes: 36 additions & 0 deletions app/Enums/DataSpasialStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use BenSampo\Enum\Enum;

/**
* Status hasil pemanggilan API Satu Data.
*
* Tiga nilai ini adalah pembeda utama antara "data memang kosong" dan
* "data tidak dapat diperoleh" (FR-005). View WAJIB bercabang berdasarkan
* nilai ini, bukan berdasarkan apakah array kosong.
*
* @method static static Data()
* @method static static Empty()
* @method static static Unavailable()
*/
final class DataSpasialStatus extends Enum
{
/**
* Respons sukses berisi minimal satu item.
*/
public const Data = 'data';

/**
* Respons sukses tanpa data (FR-015).
*/
public const Empty = 'empty';

/**
* Data tidak dapat diperoleh sementara (FR-014).
*/
public const Unavailable = 'unavailable';
}
37 changes: 37 additions & 0 deletions app/Enums/Modul.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,43 @@ final class Modul extends Enum
],
],
],
[
'text' => 'DataSpasial',
'icon' => 'fas fa-map',
'permission' => 'dataspasial',
'submenu' => [
[
'icon' => 'far fa-fw fa-circle',
'text' => 'Skema DataSpasial',
'url' => 'dataspasial/skema',
'permission' => 'dataspasial-skema',
],
[
'icon' => 'far fa-fw fa-circle',
'text' => 'Peta Gabungan',
'url' => 'dataspasial/peta',
'permission' => 'dataspasial-peta',
],
[
'icon' => 'far fa-fw fa-circle',
'text' => 'Dasbor DataSpasial',
'url' => 'dataspasial/dasbor',
'permission' => 'dataspasial-dasbor',
],
[
'icon' => 'far fa-fw fa-circle',
'text' => 'Impor Data',
'url' => 'dataspasial/impor',
'permission' => 'dataspasial-impor',
],
[
'icon' => 'far fa-fw fa-circle',
'text' => 'Monitoring',
'url' => 'dataspasial/monitoring',
'permission' => 'dataspasial-monitoring',
],
],
],
[
'text' => 'SOTK',
'icon' => 'fa fa-tags',
Expand Down
138 changes: 138 additions & 0 deletions app/Enums/StatusAdopsiDesa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use BenSampo\Enum\Enum;

/**
* Status adopsi skema per desa (KAB-03 / FR-061).
*
* ⚠️ **INI BUKAN state yang disimpan di API.** Enum ini memodelkan **label
* bahasa pengguna** untuk nilai `status` yang dikembalikan API Satu Data,
* sehingga seluruh tempat yang menampilkan status memakai satu sumber
* kebenaran (research.md R13).
*
* ⚠️ **VERIFIKASI LANGSUNG (2026-10-16)** — API hanya punya **dua** nilai:
* `memakai_skema` dan `belum_ada_data`. Nilai "tertinggal" yang dipakai pada
* tangkapan layar acuan **tidak ada di kontrak**, sehingga status **Tertinggal**
* diturunkan dari perbandingan nomor versi terpakai dengan nomor versi terbit
* terbaru sektor tersebut (lihat `AdopsiDesaService::barisSektor()`).
*
* Kontrak `openapi-fondasi.yaml` menyebut `terbaru`/`tertinggal`/`belum` pada
* field `status_adopsi` — itu **draft**, bukan implementasi. Lihat
* `specs/005-penerbitan-adopsi-skema/contracts/penerbitan-adopsi-api.md` §5.2.
*
* @method static static Terpusat()
* @method static static Tertinggal()
* @method static static MenungguAdopsi()
* @method static static TidakDiketahui()
*/
final class StatusAdopsiDesa extends Enum
{
/**
* Desa memakai versi terbit terbaru sektor tersebut.
*/
public const Terpusat = 'terpusat';

/**
* Desa memakai versi lama → **diturunkan** dari nomor versi (bukan enum API).
*/
public const Tertinggal = 'tertinggal';

/**
* Desa belum pernah menarik skema (`belum_ada_data`).
*/
public const MenungguAdopsi = 'menunggu_adopsi';

/**
* Nilai status yang tidak dikenali atau kosong → tampil "—" (FR-071).
*/
public const TidakDiketahui = 'tidak_diketahui';

/**
* Label yang ditampilkan ke admin.
*/
public function label(): string
{
return match ($this->value) {
self::Terpusat => 'Terpusat',
self::Tertinggal => 'Tertinggal',
self::MenungguAdopsi => 'Menunggu adopsi',
default => '—',
};
}

/**
* Warna lencana Bootstrap (kelas CSS).
*/
public function warnaLencana(): string
{
return match ($this->value) {
self::Terpusat => 'badge-success',
self::Tertinggal => 'badge-warning',
self::MenungguAdopsi => 'badge-info',
default => 'badge-light',
};
}

/**
* Status ini punya sumber data sehingga **boleh** dipakai sebagai saringan.
*
* Status `tidak_diketahui` tidak boleh disaring: hasil saringan atas nilai
* yang tidak ada mapanya akan menyesatkan (FR-061/FR-063).
*/
public function bolehDisaring(): bool
{
return $this->value !== self::TidakDiketahui;
}

/**
* Status ini menjadi penanda desa tertinggal (FR-062).
*/
public function ditandai(): bool
{
return $this->value === self::Tertinggal;
}

/**
* Peta nilai `status` dari API → status UI.
*
* @param string $statusApi nilai `attributes.status` dari API
* @param int|null $nomorVersiTerpakai `nomor_versi_skema` (null = belum adopsi)
* @param int|null $nomorVersiTerbaru nomor versi terbit terbaru sektor (null = belum diketahui)
*/
public static function dariApi(?string $statusApi, ?int $nomorVersiTerpakai, ?int $nomorVersiTerbaru): self
{
if ($statusApi === 'belum_ada_data' || $nomorVersiTerpakai === null) {
return self::fromValue(self::MenungguAdopsi);
}

if ($statusApi !== 'memakai_skema') {
return self::fromValue(self::TidakDiketahui);
}

// ⚠️ FR-061 — versi yang LEBIH BARU dari versi terbit terbaru diperlakukan
// sebagai data anomali: status Terpusat, **tidak boleh** diklaim tertinggal.
if ($nomorVersiTerbaru === null || $nomorVersiTerpakai >= $nomorVersiTerbaru) {
return self::fromValue(self::Terpusat);
}

return self::fromValue(self::Tertinggal);
}

/**
* Daftar status yang boleh dipakai sebagai pilihan saringan.
*
* @return array<int, self>
*/
public static function pilihanSaringan(): array
{
return [
self::fromValue(self::Terpusat),
self::fromValue(self::Tertinggal),
self::fromValue(self::MenungguAdopsi),
];
}
}
75 changes: 75 additions & 0 deletions app/Enums/StatusSkemaEditor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use BenSampo\Enum\Enum;

/**
* Keadaan editor skema pada satu sektor di sisi UI.
*
* INI BUKAN state yang disimpan di API — enum ini hanya memodelkan "apa yang
* boleh diinteraksi" pada satu render halaman, sehingga aturan tombol simpan
* dan tombol tambah isian tidak tersebar di setiap Blade.
*
* Status sebenarnya (`draf` / `terbit`) datang dari API; `Kosong` dan
* `BelumTersedia` diturunkan dari hasil `DataSpasialResult` (FR-005/FR-006).
*
* @method static static Kosong()
* @method static static Draf()
* @method static static Terbit()
* @method static static BelumTersedia()
*/
final class StatusSkemaEditor extends Enum
{
/**
* Sektor belum punya skema sama sekali (FR-008b).
*/
public const Kosong = 'kosong';

/**
* Sedang ada draf berjalan — satu-satunya keadaan yang dapat diubah (FR-008b).
*/
public const Draf = 'draf';

/**
* Hanya ada versi terbit — ditampilkan baca-saja (FR-008a).
*/
public const Terbit = 'terbit';

/**
* Sumber data tidak dapat dihubungi; seluruh tombol nonaktif (FR-005).
*/
public const BelumTersedia = 'belum_tersedia';

/**
* Tombol "Simpan draf" hanya aktif pada keadaan draf (FR-008b).
*/
public function bolehSimpan(): bool
{
return $this->value === self::Draf;
}

/**
* Tombol tambah/hapus/urut isian hanya aktif pada keadaan draf (FR-008a).
*/
public function bolehUbah(): bool
{
return $this->value === self::Draf;
}

/**
* Menyusun keadaan editor dari skema yang dipilih.
*
* @param array{status: string, jumlah_skema: int} $skema
*/
public static function dari(array $skema): self
{
return match ($skema['status'] ?? '') {
self::Terbit => self::fromValue(self::Terbit),
self::Draf => self::fromValue(self::Draf),
default => self::fromValue(($skema['jumlah_skema'] ?? 0) > 0 ? self::Terbit : self::Kosong),
};
}
}
Loading
Loading