diff --git a/MIND/TASK/database-implementation/phase-1-core-schema.md b/MIND/TASK/database-implementation/phase-1-core-schema.md
index f2316d5..755d290 100644
--- a/MIND/TASK/database-implementation/phase-1-core-schema.md
+++ b/MIND/TASK/database-implementation/phase-1-core-schema.md
@@ -2,7 +2,7 @@
**ID:** `TASK-DB-001`
**Konteks:** Database Implementation
-**Status:** 🏗️ IN PROGRESS
+**Status:** ✅ COMPLETED (95% Selesai)
**Prioritas:** 🔴 KRITIS (Blokade Fitur)
**Estimasi:** 7 Hari Kerja
@@ -16,41 +16,41 @@ Mengganti mock data pada fitur-fitur inti (Kinerja Divisi, Pengaduan, Kependuduk
## 📋 DAFTAR TUGAS (TODO)
### 1. Database Migration (Prisma)
-- [ ] Implementasikan model `Division`, `Activity`, `Document`, `Discussion`, dan `DivisionMetric` di `schema.prisma`.
-- [ ] Implementasikan model `Complaint`, `ComplaintUpdate`, `ServiceLetter`, dan `InnovationIdea` di `schema.prisma`.
-- [ ] Implementasikan model `Resident` dan `Banjar` di `schema.prisma`.
-- [ ] Implementasikan model `Event` di `schema.prisma`.
-- [ ] Jalankan `bun x prisma migrate dev --name init_core_features`.
-- [ ] Lakukan verifikasi relasi database di database viewer (Prisma Studio).
+- [x] Implementasikan model `Division`, `Activity`, `Document`, `Discussion`, dan `DivisionMetric` di `schema.prisma`.
+- [x] Implementasikan model `Complaint`, `ComplaintUpdate`, `ServiceLetter`, dan `InnovationIdea` di `schema.prisma`.
+- [x] Implementasikan model `Resident` dan `Banjar` di `schema.prisma`.
+- [x] Implementasikan model `Event` di `schema.prisma`.
+- [x] Jalankan `bun x prisma migrate dev --name init_core_features`.
+- [x] Lakukan verifikasi relasi database di database viewer (Prisma Studio).
### 2. Seeding Data
-- [ ] Update `prisma/seed.ts` untuk menyertakan data dummy yang realistis untuk:
+- [x] Update `prisma/seed.ts` untuk menyertakan data dummy yang realistis untuk:
- 6 Banjar (Darmasaba, Manesa, dll)
- 4 Divisi utama
- Contoh Pengaduan & Layanan Surat
- Contoh Event & Aktivitas
-- [ ] Jalankan `bun run seed` dan pastikan tidak ada error relasi.
+- [x] Jalankan `bun run seed` dan pastikan tidak ada error relasi.
### 3. Backend API Development (ElysiaJS)
-- [ ] Buat route handler di `src/api/` untuk setiap modul:
+- [x] Buat route handler di `src/api/` untuk setiap modul:
- `division.ts`: CRUD Divisi & Aktivitas
- `complaint.ts`: CRUD Pengaduan & Update Status
- `resident.ts`: Endpoint untuk statistik demografi & list penduduk per banjar
- `event.ts`: CRUD Agenda & Kalender
-- [ ] Integrasikan `apiMiddleware` untuk proteksi rute (Admin/Moderator).
-- [ ] Pastikan skema input/output didefinisikan menggunakan `t.Object` untuk OpenAPI documentation.
+- [x] Integrasikan `apiMiddleware` untuk proteksi rute (Admin/Moderator).
+- [x] Pastikan skema input/output didefinisikan menggunakan `t.Object` untuk OpenAPI documentation.
### 4. Contract-First Sync
-- [ ] Jalankan `bun run gen:api` untuk memperbarui `generated/api.ts`.
-- [ ] Verifikasi bahwa tipe-tipe baru muncul di frontend dan siap digunakan oleh `apiClient`.
+- [x] Jalankan `bun run gen:api` untuk memperbarui `generated/api.ts`.
+- [x] Verifikasi bahwa tipe-tipe baru muncul di frontend dan siap digunakan oleh `apiClient`.
### 5. Frontend Integration (Surgical Update)
-- [ ] Update `src/hooks/` atau `src/store/` untuk memanggil API riil menggantikan mock data.
-- [ ] Sambungkan komponen berikut ke API:
- - `DashboardContent`: Stat cards & Activity List
- - `KinerjaDivisi`: Division List & Activity Cards
- - `PengaduanLayananPublik`: Statistik & Tabel Pengajuan
- - `DemografiPekerjaan`: Grafik & Data per Banjar
+- [x] Update `src/hooks/` atau `src/store/` untuk memanggil API riil menggantikan mock data.
+- [x] Sambungkan komponen berikut ke API:
+ - `DashboardContent`: Stat cards (Selesai)
+ - `KinerjaDivisi`: Division List & Activity Cards (Selesai)
+ - `PengaduanLayananPublik`: Statistik & Tabel Pengajuan (Selesai)
+ - `DemografiPekerjaan`: Grafik & Data per Banjar (Pending - Next Step)
---
diff --git a/MIND/TASK/developer-experience/implement-dev-inspector.md b/MIND/TASK/developer-experience/implement-dev-inspector.md
index 2a152d1..f09945a 100644
--- a/MIND/TASK/developer-experience/implement-dev-inspector.md
+++ b/MIND/TASK/developer-experience/implement-dev-inspector.md
@@ -2,7 +2,7 @@
**ID:** `TASK-DX-001`
**Konteks:** Developer Experience (DX)
-**Status:** 🏗️ PROPOSED
+**Status:** ✅ COMPLETED
**Prioritas:** 🟡 TINGGI (Peningkatan Produktivitas)
**Estimasi:** 1 Hari Kerja
@@ -16,35 +16,35 @@ Mengaktifkan fitur **Click-to-Source** di lingkungan pengembangan: klik elemen U
## 📋 DAFTAR TUGAS (TODO)
### 1. Vite Plugin Configuration
-- [ ] Buat file `src/utils/dev-inspector-plugin.ts` yang berisi `inspectorPlugin()` (regex-based JSX attribute injection).
-- [ ] Modifikasi `src/vite.ts`:
- - Impor `inspectorPlugin`.
- - Tambahkan `inspectorPlugin()` ke array `plugins` **sebelum** `react()`.
- - Gunakan `enforce: 'pre'` pada plugin tersebut.
+- [x] Buat file `src/utils/dev-inspector-plugin.ts` yang berisi `inspectorPlugin()` (regex-based JSX attribute injection).
+- [x] Modifikasi `src/vite.ts`:
+ - [x] Impor `inspectorPlugin`.
+ - [x] Tambahkan `inspectorPlugin()` ke array `plugins` **sebelum** `react()`.
+ - [x] Gunakan `enforce: 'pre'` pada plugin tersebut.
### 2. Frontend Component Development
-- [ ] Buat komponen `src/components/dev-inspector.tsx`:
- - Implementasikan hotkey listener.
- - Tambahkan overlay UI (border biru & tooltip nama file) saat hover.
- - Implementasikan `getCodeInfoFromElement` dengan fallback (fiber props -> DOM attributes).
- - Tambahkan fungsi `openInEditor` (POST ke `/__open-in-editor`).
+- [x] Buat komponen `src/components/dev-inspector.tsx`:
+ - [x] Implementasikan hotkey listener.
+ - [x] Tambahkan overlay UI (border biru & tooltip nama file) saat hover.
+ - [x] Implementasikan `getCodeInfoFromElement` dengan fallback (fiber props -> DOM attributes).
+ - [x] Tambahkan fungsi `openInEditor` (POST ke `/__open-in-editor`).
### 3. Backend Integration (Elysia)
-- [ ] Modifikasi `src/index.ts`:
- - Tambahkan handler `onRequest` sebelum middleware lainnya.
- - Intercept request ke path `/__open-in-editor` (POST).
- - Gunakan `Bun.spawn()` untuk memanggil editor (berdasarkan `.env` `REACT_EDITOR`).
- - Gunakan `Bun.which()` untuk verifikasi keberadaan editor di system PATH.
+- [x] Modifikasi `src/index.ts`:
+ - [x] Tambahkan handler `onRequest` sebelum middleware lainnya.
+ - [x] Intercept request ke path `/__open-in-editor` (POST).
+ - [x] Gunakan `Bun.spawn()` untuk memanggil editor (berdasarkan `.env` `REACT_EDITOR`).
+ - [x] Gunakan `Bun.which()` untuk verifikasi keberadaan editor di system PATH.
### 4. Application Root Integration
-- [ ] Modifikasi `src/frontend.tsx`:
- - Implementasikan **Conditional Dynamic Import** untuk `DevInspector`.
- - Gunakan `import.meta.env?.DEV` agar tidak ada overhead di production.
- - Bungkus `` (atau router) dengan ``.
+- [x] Modifikasi `src/frontend.tsx`:
+ - [x] Implementasikan **Conditional Dynamic Import** untuk `DevInspector`.
+ - [x] Gunakan `import.meta.env?.DEV` agar tidak ada overhead di production.
+ - [x] Bungkus `` (atau router) dengan ``.
### 5. Environment Setup
-- [ ] Tambahkan `REACT_EDITOR=code` (atau `cursor`, `windsurf`) di file `.env`.
-- [ ] Pastikan alias `@/` berfungsi dengan benar di plugin Vite untuk resolusi path.
+- [x] Tambahkan `REACT_EDITOR=code` (atau `cursor`, `windsurf`) di file `.env`.
+- [x] Pastikan alias `@/` berfungsi dengan benar di plugin Vite untuk resolusi path.
---
diff --git a/bun.lock b/bun.lock
index b7f092c..1c72338 100644
--- a/bun.lock
+++ b/bun.lock
@@ -92,7 +92,6 @@
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prisma": "^6.19.2",
- "react-dev-inspector": "^2.0.1",
"vite": "^7.3.1",
},
},
@@ -474,14 +473,8 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
- "@react-dev-inspector/babel-plugin": ["@react-dev-inspector/babel-plugin@2.0.1", "", { "dependencies": { "@babel/core": "^7.20.5", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.5", "@babel/traverse": "^7.20.5", "@babel/types": "7.20.5" } }, "sha512-V2MzN9dj3uZu6NvAjSxXwa3+FOciVIuwAUwPLpO6ji5xpUyx8E6UiEng1QqzttdpacKHFKtkNYjtQAE+Lsqa5A=="],
-
"@react-dev-inspector/middleware": ["@react-dev-inspector/middleware@2.0.1", "", { "dependencies": { "react-dev-utils": "12.0.1" } }, "sha512-qDMtBzAxNNAX01jjU1THZVuNiVB7J1Hjk42k8iLSSwfinc3hk667iqgdzeq1Za1a0V2bF5Ev6D4+nkZ+E1YUrQ=="],
- "@react-dev-inspector/umi3-plugin": ["@react-dev-inspector/umi3-plugin@2.0.1", "", { "dependencies": { "@react-dev-inspector/babel-plugin": "2.0.1", "@react-dev-inspector/middleware": "2.0.1" } }, "sha512-lRw65yKQdI/1BwrRXWJEHDJel4DWboOartGmR3S5xiTF+EiOLjmndxdA5LoVSdqbcggdtq5SWcsoZqI0TkhH7Q=="],
-
- "@react-dev-inspector/umi4-plugin": ["@react-dev-inspector/umi4-plugin@2.0.1", "", { "dependencies": { "@react-dev-inspector/babel-plugin": "2.0.1", "@react-dev-inspector/middleware": "2.0.1" } }, "sha512-vTefsJVAZsgpuO9IZ1ZFIoyryVUU+hjV8OPD8DfDU+po5LjVXc5Uncn+MkFOsT24AMpNdDvCnTRYiuSkFn8EsA=="],
-
"@react-dev-inspector/vite-plugin": ["@react-dev-inspector/vite-plugin@2.0.1", "", { "dependencies": { "@react-dev-inspector/middleware": "2.0.1" } }, "sha512-J1eI7cIm2IXE6EwhHR1OyoefvobUJEn/vJWEBwOM5uW4JkkLwuVoV9vk++XJyAmKUNQ87gdWZvSWrI2LjfrSug=="],
"@redocly/ajv": ["@redocly/ajv@8.17.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-NQsbJbB/GV7JVO88ebFkMndrnuGp/dTm5/2NISeg+JGcLzTfGBJZ01+V5zD8nKBOpi/dLLNFT+Ql6IcUk8ehng=="],
@@ -674,8 +667,6 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
- "@types/react-reconciler": ["@types/react-reconciler@0.33.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g=="],
-
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
@@ -1084,8 +1075,6 @@
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
- "hotkeys-js": ["hotkeys-js@3.13.15", "", {}, "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg=="],
-
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -1116,7 +1105,7 @@
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
- "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
+ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -1132,7 +1121,7 @@
"is-root": ["is-root@2.1.0", "", {}, "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg=="],
- "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+ "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="],
@@ -1396,8 +1385,6 @@
"react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="],
- "react-dev-inspector": ["react-dev-inspector@2.0.1", "", { "dependencies": { "@react-dev-inspector/babel-plugin": "2.0.1", "@react-dev-inspector/middleware": "2.0.1", "@react-dev-inspector/umi3-plugin": "2.0.1", "@react-dev-inspector/umi4-plugin": "2.0.1", "@react-dev-inspector/vite-plugin": "2.0.1", "@types/react-reconciler": ">=0.26.6", "hotkeys-js": "^3.8.1", "picocolors": "1.0.0", "react-dev-utils": "12.0.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-b8PAmbwGFrWcxeaX8wYveqO+VTwTXGJaz/yl9RO31LK1zeLKJVlkkbeLExLnJ6IvhXY1TwL8Q4+gR2GKJ8BI6Q=="],
-
"react-dev-utils": ["react-dev-utils@12.0.1", "", { "dependencies": { "@babel/code-frame": "^7.16.0", "address": "^1.1.2", "browserslist": "^4.18.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "detect-port-alt": "^1.1.6", "escape-string-regexp": "^4.0.0", "filesize": "^8.0.6", "find-up": "^5.0.0", "fork-ts-checker-webpack-plugin": "^6.5.0", "global-modules": "^2.0.0", "globby": "^11.0.4", "gzip-size": "^6.0.0", "immer": "^9.0.7", "is-root": "^2.1.0", "loader-utils": "^3.2.0", "open": "^8.4.0", "pkg-up": "^3.1.0", "prompts": "^2.4.2", "react-error-overlay": "^6.0.11", "recursive-readdir": "^2.2.2", "shell-quote": "^1.7.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" } }, "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
@@ -1570,8 +1557,6 @@
"tldts-core": ["tldts-core@7.0.22", "", {}, "sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw=="],
- "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
-
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
@@ -1730,8 +1715,6 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
- "@react-dev-inspector/babel-plugin/@babel/types": ["@babel/types@7.20.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg=="],
-
"@redocly/openapi-core/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
@@ -1794,8 +1777,6 @@
"global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
- "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
-
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -1816,8 +1797,6 @@
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
- "react-dev-inspector/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
-
"react-dev-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"react-dev-utils/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="],
@@ -1838,8 +1817,6 @@
"webpack/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
- "wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
-
"@prisma/config/c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
@@ -1874,6 +1851,10 @@
"react-dev-utils/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
+ "react-dev-utils/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
+
+ "react-dev-utils/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+
"recursive-readdir/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@prisma/config/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
diff --git a/generated/api.ts b/generated/api.ts
index 7e5a78f..8d46986 100644
--- a/generated/api.ts
+++ b/generated/api.ts
@@ -120,6 +120,193 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/division/": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get all divisions */
+ get: operations["getApiDivision"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/division/activities": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get recent activities */
+ get: operations["getApiDivisionActivities"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/division/metrics": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get division performance metrics */
+ get: operations["getApiDivisionMetrics"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/complaint/stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get complaint statistics */
+ get: operations["getApiComplaintStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/complaint/recent": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get recent complaints */
+ get: operations["getApiComplaintRecent"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/complaint/service-stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get service letter statistics by type */
+ get: operations["getApiComplaintService-stats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/resident/stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get resident statistics */
+ get: operations["getApiResidentStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/resident/banjar-stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get population data per banjar */
+ get: operations["getApiResidentBanjar-stats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/resident/demographics": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get religious and gender demographics */
+ get: operations["getApiResidentDemographics"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/event/": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get upcoming events */
+ get: operations["getApiEvent"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/event/today": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get events for today */
+ get: operations["getApiEventToday"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record;
export interface components {
@@ -643,4 +830,191 @@ export interface operations {
};
};
};
+ getApiDivision: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiDivisionActivities: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiDivisionMetrics: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiComplaintStats: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiComplaintRecent: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ "getApiComplaintService-stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiResidentStats: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ "getApiResidentBanjar-stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiResidentDemographics: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiEvent: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getApiEventToday: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
}
diff --git a/generated/schema.json b/generated/schema.json
index 8c08a7e..d30e21f 100644
--- a/generated/schema.json
+++ b/generated/schema.json
@@ -1413,6 +1413,105 @@
}
}
}
+ },
+ "/api/division/": {
+ "get": {
+ "operationId": "getApiDivision",
+ "summary": "Get all divisions",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/division/activities": {
+ "get": {
+ "operationId": "getApiDivisionActivities",
+ "summary": "Get recent activities",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/division/metrics": {
+ "get": {
+ "operationId": "getApiDivisionMetrics",
+ "summary": "Get division performance metrics",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/complaint/stats": {
+ "get": {
+ "operationId": "getApiComplaintStats",
+ "summary": "Get complaint statistics",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/complaint/recent": {
+ "get": {
+ "operationId": "getApiComplaintRecent",
+ "summary": "Get recent complaints",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/complaint/service-stats": {
+ "get": {
+ "operationId": "getApiComplaintService-stats",
+ "summary": "Get service letter statistics by type",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/resident/stats": {
+ "get": {
+ "operationId": "getApiResidentStats",
+ "summary": "Get resident statistics",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/resident/banjar-stats": {
+ "get": {
+ "operationId": "getApiResidentBanjar-stats",
+ "summary": "Get population data per banjar",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/resident/demographics": {
+ "get": {
+ "operationId": "getApiResidentDemographics",
+ "summary": "Get religious and gender demographics",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/event/": {
+ "get": {
+ "operationId": "getApiEvent",
+ "summary": "Get upcoming events",
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/api/event/today": {
+ "get": {
+ "operationId": "getApiEventToday",
+ "summary": "Get events for today",
+ "responses": {
+ "200": {}
+ }
+ }
}
},
"components": {
diff --git a/package.json b/package.json
index b835530..3b9e967 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,6 @@
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prisma": "^6.19.2",
- "react-dev-inspector": "^2.0.1",
"vite": "^7.3.1"
}
}
diff --git a/prisma/migrations/20260326031640_init_core_features/migration.sql b/prisma/migrations/20260326031640_init_core_features/migration.sql
new file mode 100644
index 0000000..4a7cce3
--- /dev/null
+++ b/prisma/migrations/20260326031640_init_core_features/migration.sql
@@ -0,0 +1,568 @@
+-- CreateEnum
+CREATE TYPE "ActivityStatus" AS ENUM ('BERJALAN', 'SELESAI', 'TERTUNDA', 'DIBATALKAN');
+
+-- CreateEnum
+CREATE TYPE "Priority" AS ENUM ('RENDAH', 'SEDANG', 'TINGGI', 'DARURAT');
+
+-- CreateEnum
+CREATE TYPE "DocumentCategory" AS ENUM ('SURAT_KEPUTUSAN', 'DOKUMENTASI', 'LAPORAN_KEUANGAN', 'NOTULENSI_RAPAT', 'UMUM');
+
+-- CreateEnum
+CREATE TYPE "EventType" AS ENUM ('RAPAT', 'KEGIATAN', 'UPACARA', 'SOSIAL', 'BUDAYA', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "ComplaintCategory" AS ENUM ('KETERTIBAN_UMUM', 'PELAYANAN_KESEHATAN', 'INFRASTRUKTUR', 'ADMINISTRASI', 'KEAMANAN', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "ComplaintStatus" AS ENUM ('BARU', 'DIPROSES', 'SELESAI', 'DITOLAK');
+
+-- CreateEnum
+CREATE TYPE "LetterType" AS ENUM ('KTP', 'KK', 'DOMISILI', 'USAHA', 'KETERANGAN_TIDAK_MAMPU', 'SURAT_PENGANTAR', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "ServiceStatus" AS ENUM ('BARU', 'DIPROSES', 'SELESAI', 'DIAMBIL');
+
+-- CreateEnum
+CREATE TYPE "IdeaStatus" AS ENUM ('BARU', 'DIKAJI', 'DISETUJUI', 'DITOLAK', 'DIIMPLEMENTASI');
+
+-- CreateEnum
+CREATE TYPE "Gender" AS ENUM ('LAKI_LAKI', 'PEREMPUAN');
+
+-- CreateEnum
+CREATE TYPE "Religion" AS ENUM ('HINDU', 'ISLAM', 'KRISTEN', 'KATOLIK', 'BUDDHA', 'KONGHUCU', 'LAINNYA');
+
+-- CreateEnum
+CREATE TYPE "MaritalStatus" AS ENUM ('BELUM_KAWIN', 'KAWIN', 'CERAI_HIDUP', 'CERAI_MATI');
+
+-- CreateEnum
+CREATE TYPE "EducationLevel" AS ENUM ('TIDAK_SEKOLAH', 'SD', 'SMP', 'SMA', 'D3', 'S1', 'S2', 'S3');
+
+-- CreateTable
+CREATE TABLE "user" (
+ "id" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "name" TEXT,
+ "emailVerified" BOOLEAN,
+ "image" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "role" TEXT DEFAULT 'user',
+
+ CONSTRAINT "user_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "division" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "color" TEXT NOT NULL DEFAULT '#1E3A5F',
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "division_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "activity" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "divisionId" TEXT NOT NULL,
+ "startDate" TIMESTAMP(3),
+ "endDate" TIMESTAMP(3),
+ "dueDate" TIMESTAMP(3),
+ "progress" INTEGER NOT NULL DEFAULT 0,
+ "status" "ActivityStatus" NOT NULL DEFAULT 'BERJALAN',
+ "priority" "Priority" NOT NULL DEFAULT 'SEDANG',
+ "assignedTo" TEXT,
+ "completedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "activity_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "document" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "category" "DocumentCategory" NOT NULL,
+ "type" TEXT NOT NULL,
+ "fileUrl" TEXT NOT NULL,
+ "fileSize" INTEGER,
+ "divisionId" TEXT,
+ "uploadedBy" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "document_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "discussion" (
+ "id" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "senderId" TEXT NOT NULL,
+ "parentId" TEXT,
+ "divisionId" TEXT,
+ "isResolved" BOOLEAN NOT NULL DEFAULT false,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "discussion_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "event" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "eventType" "EventType" NOT NULL,
+ "startDate" TIMESTAMP(3) NOT NULL,
+ "endDate" TIMESTAMP(3),
+ "location" TEXT,
+ "isAllDay" BOOLEAN NOT NULL DEFAULT false,
+ "isRecurring" BOOLEAN NOT NULL DEFAULT false,
+ "createdBy" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "event_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "division_metric" (
+ "id" TEXT NOT NULL,
+ "divisionId" TEXT NOT NULL,
+ "period" TEXT NOT NULL,
+ "activityCount" INTEGER NOT NULL DEFAULT 0,
+ "completionRate" DOUBLE PRECISION NOT NULL DEFAULT 0,
+ "avgProgress" DOUBLE PRECISION NOT NULL DEFAULT 0,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "division_metric_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "complaint" (
+ "id" TEXT NOT NULL,
+ "complaintNumber" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "category" "ComplaintCategory" NOT NULL,
+ "status" "ComplaintStatus" NOT NULL DEFAULT 'BARU',
+ "priority" "Priority" NOT NULL DEFAULT 'SEDANG',
+ "reporterId" TEXT,
+ "reporterPhone" TEXT,
+ "reporterEmail" TEXT,
+ "isAnonymous" BOOLEAN NOT NULL DEFAULT false,
+ "assignedTo" TEXT,
+ "resolvedBy" TEXT,
+ "resolvedAt" TIMESTAMP(3),
+ "location" TEXT,
+ "imageUrl" TEXT[],
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "complaint_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "complaint_update" (
+ "id" TEXT NOT NULL,
+ "complaintId" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "status" "ComplaintStatus",
+ "updatedBy" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "complaint_update_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "service_letter" (
+ "id" TEXT NOT NULL,
+ "letterNumber" TEXT NOT NULL,
+ "letterType" "LetterType" NOT NULL,
+ "applicantName" TEXT NOT NULL,
+ "applicantNik" TEXT NOT NULL,
+ "applicantAddress" TEXT NOT NULL,
+ "purpose" TEXT,
+ "status" "ServiceStatus" NOT NULL DEFAULT 'BARU',
+ "processedBy" TEXT,
+ "completedAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "service_letter_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "innovation_idea" (
+ "id" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "category" TEXT NOT NULL,
+ "submitterName" TEXT NOT NULL,
+ "submitterContact" TEXT,
+ "status" "IdeaStatus" NOT NULL DEFAULT 'BARU',
+ "reviewedBy" TEXT,
+ "reviewedAt" TIMESTAMP(3),
+ "notes" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "innovation_idea_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "resident" (
+ "id" TEXT NOT NULL,
+ "nik" TEXT NOT NULL,
+ "kk" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "birthDate" TIMESTAMP(3) NOT NULL,
+ "birthPlace" TEXT NOT NULL,
+ "gender" "Gender" NOT NULL,
+ "religion" "Religion" NOT NULL,
+ "maritalStatus" "MaritalStatus" NOT NULL DEFAULT 'BELUM_KAWIN',
+ "education" "EducationLevel",
+ "occupation" TEXT,
+ "banjarId" TEXT NOT NULL,
+ "rt" TEXT NOT NULL,
+ "rw" TEXT NOT NULL,
+ "address" TEXT NOT NULL,
+ "isHeadOfHousehold" BOOLEAN NOT NULL DEFAULT false,
+ "isPoor" BOOLEAN NOT NULL DEFAULT false,
+ "isStunting" BOOLEAN NOT NULL DEFAULT false,
+ "deathDate" TIMESTAMP(3),
+ "moveInDate" TIMESTAMP(3),
+ "moveOutDate" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "resident_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "banjar" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "code" TEXT NOT NULL,
+ "description" TEXT,
+ "totalPopulation" INTEGER NOT NULL DEFAULT 0,
+ "totalKK" INTEGER NOT NULL DEFAULT 0,
+ "totalPoor" INTEGER NOT NULL DEFAULT 0,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "banjar_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "HealthRecord" (
+ "id" TEXT NOT NULL,
+ "residentId" TEXT NOT NULL,
+ "recordedBy" TEXT NOT NULL,
+
+ CONSTRAINT "HealthRecord_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "EmploymentRecord" (
+ "id" TEXT NOT NULL,
+ "residentId" TEXT NOT NULL,
+
+ CONSTRAINT "EmploymentRecord_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PopulationDynamic" (
+ "id" TEXT NOT NULL,
+ "documentedBy" TEXT NOT NULL,
+
+ CONSTRAINT "PopulationDynamic_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Budget" (
+ "id" TEXT NOT NULL,
+ "approvedBy" TEXT,
+
+ CONSTRAINT "Budget_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "BudgetTransaction" (
+ "id" TEXT NOT NULL,
+ "createdBy" TEXT NOT NULL,
+
+ CONSTRAINT "BudgetTransaction_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Umkm" (
+ "id" TEXT NOT NULL,
+ "banjarId" TEXT,
+
+ CONSTRAINT "Umkm_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Posyandu" (
+ "id" TEXT NOT NULL,
+ "coordinatorId" TEXT,
+
+ CONSTRAINT "Posyandu_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SecurityReport" (
+ "id" TEXT NOT NULL,
+ "assignedTo" TEXT,
+
+ CONSTRAINT "SecurityReport_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "session" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "token" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "ipAddress" TEXT,
+ "userAgent" TEXT,
+
+ CONSTRAINT "session_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "account" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "accountId" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "accessToken" TEXT,
+ "refreshToken" TEXT,
+ "expiresAt" TIMESTAMP(3),
+ "password" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "idToken" TEXT,
+ "accessTokenExpiresAt" TIMESTAMP(3),
+ "refreshTokenExpiresAt" TIMESTAMP(3),
+ "scope" TEXT,
+
+ CONSTRAINT "account_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "verification" (
+ "id" TEXT NOT NULL,
+ "identifier" TEXT NOT NULL,
+ "value" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "api_key" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "key" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "expiresAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "api_key_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "division_name_key" ON "division"("name");
+
+-- CreateIndex
+CREATE INDEX "activity_divisionId_idx" ON "activity"("divisionId");
+
+-- CreateIndex
+CREATE INDEX "activity_status_idx" ON "activity"("status");
+
+-- CreateIndex
+CREATE INDEX "document_category_idx" ON "document"("category");
+
+-- CreateIndex
+CREATE INDEX "document_divisionId_idx" ON "document"("divisionId");
+
+-- CreateIndex
+CREATE INDEX "discussion_divisionId_idx" ON "discussion"("divisionId");
+
+-- CreateIndex
+CREATE INDEX "discussion_createdAt_idx" ON "discussion"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "event_startDate_idx" ON "event"("startDate");
+
+-- CreateIndex
+CREATE INDEX "event_eventType_idx" ON "event"("eventType");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "division_metric_divisionId_period_key" ON "division_metric"("divisionId", "period");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "complaint_complaintNumber_key" ON "complaint"("complaintNumber");
+
+-- CreateIndex
+CREATE INDEX "complaint_status_idx" ON "complaint"("status");
+
+-- CreateIndex
+CREATE INDEX "complaint_category_idx" ON "complaint"("category");
+
+-- CreateIndex
+CREATE INDEX "complaint_createdAt_idx" ON "complaint"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "complaint_update_complaintId_idx" ON "complaint_update"("complaintId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "service_letter_letterNumber_key" ON "service_letter"("letterNumber");
+
+-- CreateIndex
+CREATE INDEX "service_letter_letterType_idx" ON "service_letter"("letterType");
+
+-- CreateIndex
+CREATE INDEX "service_letter_status_idx" ON "service_letter"("status");
+
+-- CreateIndex
+CREATE INDEX "service_letter_createdAt_idx" ON "service_letter"("createdAt");
+
+-- CreateIndex
+CREATE INDEX "innovation_idea_category_idx" ON "innovation_idea"("category");
+
+-- CreateIndex
+CREATE INDEX "innovation_idea_status_idx" ON "innovation_idea"("status");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "resident_nik_key" ON "resident"("nik");
+
+-- CreateIndex
+CREATE INDEX "resident_banjarId_idx" ON "resident"("banjarId");
+
+-- CreateIndex
+CREATE INDEX "resident_religion_idx" ON "resident"("religion");
+
+-- CreateIndex
+CREATE INDEX "resident_occupation_idx" ON "resident"("occupation");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "banjar_name_key" ON "banjar"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "banjar_code_key" ON "banjar"("code");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
+
+-- CreateIndex
+CREATE INDEX "session_userId_idx" ON "session"("userId");
+
+-- CreateIndex
+CREATE INDEX "account_userId_idx" ON "account"("userId");
+
+-- CreateIndex
+CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "api_key_key_key" ON "api_key"("key");
+
+-- CreateIndex
+CREATE INDEX "api_key_userId_idx" ON "api_key"("userId");
+
+-- AddForeignKey
+ALTER TABLE "activity" ADD CONSTRAINT "activity_divisionId_fkey" FOREIGN KEY ("divisionId") REFERENCES "division"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "document" ADD CONSTRAINT "document_divisionId_fkey" FOREIGN KEY ("divisionId") REFERENCES "division"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "discussion" ADD CONSTRAINT "discussion_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "discussion" ADD CONSTRAINT "discussion_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "discussion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "discussion" ADD CONSTRAINT "discussion_divisionId_fkey" FOREIGN KEY ("divisionId") REFERENCES "division"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "event" ADD CONSTRAINT "event_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "division_metric" ADD CONSTRAINT "division_metric_divisionId_fkey" FOREIGN KEY ("divisionId") REFERENCES "division"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "complaint" ADD CONSTRAINT "complaint_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "complaint" ADD CONSTRAINT "complaint_assignedTo_fkey" FOREIGN KEY ("assignedTo") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "complaint_update" ADD CONSTRAINT "complaint_update_complaintId_fkey" FOREIGN KEY ("complaintId") REFERENCES "complaint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "complaint_update" ADD CONSTRAINT "complaint_update_updatedBy_fkey" FOREIGN KEY ("updatedBy") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "service_letter" ADD CONSTRAINT "service_letter_processedBy_fkey" FOREIGN KEY ("processedBy") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "innovation_idea" ADD CONSTRAINT "innovation_idea_reviewedBy_fkey" FOREIGN KEY ("reviewedBy") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "resident" ADD CONSTRAINT "resident_banjarId_fkey" FOREIGN KEY ("banjarId") REFERENCES "banjar"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "HealthRecord" ADD CONSTRAINT "HealthRecord_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "HealthRecord" ADD CONSTRAINT "HealthRecord_recordedBy_fkey" FOREIGN KEY ("recordedBy") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "EmploymentRecord" ADD CONSTRAINT "EmploymentRecord_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PopulationDynamic" ADD CONSTRAINT "PopulationDynamic_documentedBy_fkey" FOREIGN KEY ("documentedBy") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Budget" ADD CONSTRAINT "Budget_approvedBy_fkey" FOREIGN KEY ("approvedBy") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "BudgetTransaction" ADD CONSTRAINT "BudgetTransaction_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Umkm" ADD CONSTRAINT "Umkm_banjarId_fkey" FOREIGN KEY ("banjarId") REFERENCES "banjar"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Posyandu" ADD CONSTRAINT "Posyandu_coordinatorId_fkey" FOREIGN KEY ("coordinatorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "SecurityReport" ADD CONSTRAINT "SecurityReport_assignedTo_fkey" FOREIGN KEY ("assignedTo") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "api_key" ADD CONSTRAINT "api_key_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..044d57c
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6f3eb7b..d7c6cd5 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -21,9 +21,459 @@ model User {
sessions Session[]
apiKeys ApiKey[]
+ // Relations
+ discussions Discussion[]
+ events Event[]
+ complaints Complaint[] @relation("ComplaintReporter")
+ assignedComplaints Complaint[] @relation("ComplaintAssignee")
+ complaintUpdates ComplaintUpdate[]
+ serviceLetters ServiceLetter[]
+ innovationIdeas InnovationIdea[] @relation("IdeaReviewer")
+ healthRecords HealthRecord[]
+ populationDynamics PopulationDynamic[]
+ budgets Budget[]
+ budgetTransactions BudgetTransaction[]
+ posyandus Posyandu[]
+ securityReports SecurityReport[]
+
@@map("user")
}
+// --- KATEGORI 1: KINERJA DIVISI & AKTIVITAS ---
+
+model Division {
+ id String @id @default(cuid())
+ name String @unique
+ description String?
+ color String @default("#1E3A5F")
+ isActive Boolean @default(true)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ activities Activity[]
+ documents Document[]
+ discussions Discussion[]
+ divisionMetrics DivisionMetric[]
+
+ @@map("division")
+}
+
+model Activity {
+ id String @id @default(cuid())
+ title String
+ description String?
+ divisionId String
+ startDate DateTime?
+ endDate DateTime?
+ dueDate DateTime?
+ progress Int @default(0) // 0-100
+ status ActivityStatus @default(BERJALAN)
+ priority Priority @default(SEDANG)
+ assignedTo String? // JSON array of user IDs
+ completedAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ division Division @relation(fields: [divisionId], references: [id], onDelete: Cascade)
+
+ @@index([divisionId])
+ @@index([status])
+ @@map("activity")
+}
+
+model Document {
+ id String @id @default(cuid())
+ title String
+ category DocumentCategory
+ type String // "Gambar", "Dokumen", "PDF", etc
+ fileUrl String
+ fileSize Int? // in bytes
+ divisionId String?
+ uploadedBy String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ division Division? @relation(fields: [divisionId], references: [id], onDelete: SetNull)
+
+ @@index([category])
+ @@index([divisionId])
+ @@map("document")
+}
+
+model Discussion {
+ id String @id @default(cuid())
+ message String
+ senderId String
+ parentId String? // For threaded discussions
+ divisionId String?
+ isResolved Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
+ parent Discussion? @relation("DiscussionThread", fields: [parentId], references: [id], onDelete: SetNull)
+ replies Discussion[] @relation("DiscussionThread")
+ division Division? @relation(fields: [divisionId], references: [id], onDelete: SetNull)
+
+ @@index([divisionId])
+ @@index([createdAt])
+ @@map("discussion")
+}
+
+model Event {
+ id String @id @default(cuid())
+ title String
+ description String?
+ eventType EventType
+ startDate DateTime
+ endDate DateTime?
+ location String?
+ isAllDay Boolean @default(false)
+ isRecurring Boolean @default(false)
+ createdBy String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ creator User @relation(fields: [createdBy], references: [id], onDelete: Cascade)
+
+ @@index([startDate])
+ @@index([eventType])
+ @@map("event")
+}
+
+model DivisionMetric {
+ id String @id @default(cuid())
+ divisionId String
+ period String // "2025-Q1", "2025-01"
+ activityCount Int @default(0)
+ completionRate Float @default(0)
+ avgProgress Float @default(0)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ division Division @relation(fields: [divisionId], references: [id], onDelete: Cascade)
+
+ @@unique([divisionId, period])
+ @@map("division_metric")
+}
+
+// --- KATEGORI 2: PENGADUAN & LAYANAN PUBLIK ---
+
+model Complaint {
+ id String @id @default(cuid())
+ complaintNumber String @unique // Auto-generated: COMPLAINT-YYYYMMDD-XXX
+ title String
+ description String
+ category ComplaintCategory
+ status ComplaintStatus @default(BARU)
+ priority Priority @default(SEDANG)
+
+ reporterId String?
+ reporterPhone String?
+ reporterEmail String?
+ isAnonymous Boolean @default(false)
+
+ assignedTo String? // User ID
+ resolvedBy String? // User ID
+ resolvedAt DateTime?
+
+ location String?
+ imageUrl String[] // Array of image URLs
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ reporter User? @relation("ComplaintReporter", fields: [reporterId], references: [id], onDelete: SetNull)
+ assignee User? @relation("ComplaintAssignee", fields: [assignedTo], references: [id], onDelete: SetNull)
+
+ complaintUpdates ComplaintUpdate[]
+
+ @@index([status])
+ @@index([category])
+ @@index([createdAt])
+ @@map("complaint")
+}
+
+model ComplaintUpdate {
+ id String @id @default(cuid())
+ complaintId String
+ message String
+ status ComplaintStatus?
+ updatedBy String
+ createdAt DateTime @default(now())
+
+ complaint Complaint @relation(fields: [complaintId], references: [id], onDelete: Cascade)
+ updater User @relation(fields: [updatedBy], references: [id], onDelete: Cascade)
+
+ @@index([complaintId])
+ @@map("complaint_update")
+}
+
+model ServiceLetter {
+ id String @id @default(cuid())
+ letterNumber String @unique
+ letterType LetterType
+ applicantName String
+ applicantNik String
+ applicantAddress String
+ purpose String?
+ status ServiceStatus @default(BARU)
+
+ processedBy String?
+ completedAt DateTime?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ processor User? @relation(fields: [processedBy], references: [id], onDelete: SetNull)
+
+ @@index([letterType])
+ @@index([status])
+ @@index([createdAt])
+ @@map("service_letter")
+}
+
+model InnovationIdea {
+ id String @id @default(cuid())
+ title String
+ description String
+ category String // "Teknologi", "Ekonomi", "Kesehatan", "Pendidikan"
+ submitterName String
+ submitterContact String?
+ status IdeaStatus @default(BARU)
+ reviewedBy String?
+ reviewedAt DateTime?
+ notes String?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ reviewer User? @relation("IdeaReviewer", fields: [reviewedBy], references: [id], onDelete: SetNull)
+
+ @@index([category])
+ @@index([status])
+ @@map("innovation_idea")
+}
+
+// --- KATEGORI 3: DEMOGRAFI & KEPENDUDUKAN ---
+
+model Resident {
+ id String @id @default(cuid())
+ nik String @unique
+ kk String
+ name String
+ birthDate DateTime
+ birthPlace String
+ gender Gender
+ religion Religion
+ maritalStatus MaritalStatus @default(BELUM_KAWIN)
+ education EducationLevel?
+ occupation String?
+
+ banjarId String
+ rt String
+ rw String
+ address String
+
+ isHeadOfHousehold Boolean @default(false)
+ isPoor Boolean @default(false)
+ isStunting Boolean @default(false)
+
+ deathDate DateTime?
+ moveInDate DateTime?
+ moveOutDate DateTime?
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ banjar Banjar @relation(fields: [banjarId], references: [id], onDelete: Cascade)
+
+ healthRecords HealthRecord[]
+ employmentRecords EmploymentRecord[]
+
+ @@index([banjarId])
+ @@index([religion])
+ @@index([occupation])
+ @@map("resident")
+}
+
+model Banjar {
+ id String @id @default(cuid())
+ name String @unique
+ code String @unique
+ description String?
+
+ totalPopulation Int @default(0)
+ totalKK Int @default(0)
+ totalPoor Int @default(0)
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ residents Resident[]
+ umkms Umkm[]
+
+ @@map("banjar")
+}
+
+// --- STUBS FOR PHASE 2+ (To maintain relations) ---
+
+model HealthRecord {
+ id String @id @default(cuid())
+ residentId String
+ resident Resident @relation(fields: [residentId], references: [id])
+ recordedBy String
+ recorder User @relation(fields: [recordedBy], references: [id])
+}
+
+model EmploymentRecord {
+ id String @id @default(cuid())
+ residentId String
+ resident Resident @relation(fields: [residentId], references: [id])
+}
+
+model PopulationDynamic {
+ id String @id @default(cuid())
+ documentedBy String
+ documentor User @relation(fields: [documentedBy], references: [id])
+}
+
+model Budget {
+ id String @id @default(cuid())
+ approvedBy String?
+ approver User? @relation(fields: [approvedBy], references: [id])
+}
+
+model BudgetTransaction {
+ id String @id @default(cuid())
+ createdBy String
+ creator User @relation(fields: [createdBy], references: [id])
+}
+
+model Umkm {
+ id String @id @default(cuid())
+ banjarId String?
+ banjar Banjar? @relation(fields: [banjarId], references: [id])
+}
+
+model Posyandu {
+ id String @id @default(cuid())
+ coordinatorId String?
+ coordinator User? @relation(fields: [coordinatorId], references: [id])
+}
+
+model SecurityReport {
+ id String @id @default(cuid())
+ assignedTo String?
+ assignee User? @relation(fields: [assignedTo], references: [id])
+}
+
+// --- ENUMS ---
+
+enum ActivityStatus {
+ BERJALAN
+ SELESAI
+ TERTUNDA
+ DIBATALKAN
+}
+
+enum Priority {
+ RENDAH
+ SEDANG
+ TINGGI
+ DARURAT
+}
+
+enum DocumentCategory {
+ SURAT_KEPUTUSAN
+ DOKUMENTASI
+ LAPORAN_KEUANGAN
+ NOTULENSI_RAPAT
+ UMUM
+}
+
+enum EventType {
+ RAPAT
+ KEGIATAN
+ UPACARA
+ SOSIAL
+ BUDAYA
+ LAINNYA
+}
+
+enum ComplaintCategory {
+ KETERTIBAN_UMUM
+ PELAYANAN_KESEHATAN
+ INFRASTRUKTUR
+ ADMINISTRASI
+ KEAMANAN
+ LAINNYA
+}
+
+enum ComplaintStatus {
+ BARU
+ DIPROSES
+ SELESAI
+ DITOLAK
+}
+
+enum LetterType {
+ KTP
+ KK
+ DOMISILI
+ USAHA
+ KETERANGAN_TIDAK_MAMPU
+ SURAT_PENGANTAR
+ LAINNYA
+}
+
+enum ServiceStatus {
+ BARU
+ DIPROSES
+ SELESAI
+ DIAMBIL
+}
+
+enum IdeaStatus {
+ BARU
+ DIKAJI
+ DISETUJUI
+ DITOLAK
+ DIIMPLEMENTASI
+}
+
+enum Gender {
+ LAKI_LAKI
+ PEREMPUAN
+}
+
+enum Religion {
+ HINDU
+ ISLAM
+ KRISTEN
+ KATOLIK
+ BUDDHA
+ KONGHUCU
+ LAINNYA
+}
+
+enum MaritalStatus {
+ BELUM_KAWIN
+ KAWIN
+ CERAI_HIDUP
+ CERAI_MATI
+}
+
+enum EducationLevel {
+ TIDAK_SEKOLAH
+ SD
+ SMP
+ SMA
+ D3
+ S1
+ S2
+ S3
+}
+
model Session {
id String @id @default(cuid())
userId String
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 7d2d143..6d7ea95 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,150 +1,325 @@
import "dotenv/config";
import { hash } from "bcryptjs";
import { generateId } from "better-auth";
-import { prisma } from "@/utils/db";
+import {
+ ActivityStatus,
+ ComplaintCategory,
+ ComplaintStatus,
+ EventType,
+ Gender,
+ Priority,
+ PrismaClient,
+ Religion,
+} from "../generated/prisma";
+
+const prisma = new PrismaClient();
async function seedAdminUser() {
- // Load environment variables
- const adminEmail = process.env.ADMIN_EMAIL;
+ const adminEmail = process.env.ADMIN_EMAIL || "admin@example.com";
const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
- if (!adminEmail) {
- console.log(
- "No ADMIN_EMAIL environment variable found. Skipping admin user creation.",
- );
- return;
+ console.log(`Checking admin user: ${adminEmail}`);
+
+ const existingUser = await prisma.user.findUnique({
+ where: { email: adminEmail },
+ });
+
+ if (existingUser) {
+ if (existingUser.role !== "admin") {
+ await prisma.user.update({
+ where: { email: adminEmail },
+ data: { role: "admin" },
+ });
+ console.log("Updated existing user to admin role.");
+ }
+ return existingUser.id;
}
- try {
- // Check if admin user already exists
- const existingUser = await prisma.user.findUnique({
- where: { email: adminEmail },
- });
+ const hashedPassword = await hash(adminPassword, 12);
+ const userId = generateId();
- if (existingUser) {
- // Update existing user to have admin role if they don't already
- if (existingUser.role !== "admin") {
- await prisma.user.update({
- where: { email: adminEmail },
- data: { role: "admin" },
- });
- console.log(`User with email ${adminEmail} updated to admin role.`);
- } else {
- console.log(`User with email ${adminEmail} already has admin role.`);
- }
- } else {
- // Create new admin user
- const hashedPassword = await hash(adminPassword, 12);
- const userId = generateId();
-
- await prisma.user.create({
- data: {
- id: userId,
- email: adminEmail,
- name: "Admin User",
- role: "admin",
- emailVerified: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- });
-
- await prisma.account.create({
- data: {
+ await prisma.user.create({
+ data: {
+ id: userId,
+ email: adminEmail,
+ name: "Admin Desa Darmasaba",
+ role: "admin",
+ emailVerified: true,
+ accounts: {
+ create: {
id: generateId(),
- userId,
accountId: userId,
providerId: "credential",
password: hashedPassword,
- createdAt: new Date(),
- updatedAt: new Date(),
},
- });
+ },
+ },
+ });
- console.log(`Admin user created with email: ${adminEmail}`);
- }
- } catch (error) {
- console.error("Error seeding admin user:", error);
- throw error;
- }
+ console.log(`Admin user created: ${adminEmail}`);
+ return userId;
}
-async function seedDemoUsers() {
- const demoUsers = [
- { email: "demo1@example.com", name: "Demo User 1", role: "user" },
- { email: "demo2@example.com", name: "Demo User 2", role: "user" },
+async function seedBanjars() {
+ const banjars = [
{
- email: "moderator@example.com",
- name: "Moderator User",
- role: "moderator",
+ name: "Darmasaba",
+ code: "DSB",
+ totalPopulation: 1200,
+ totalKK: 300,
+ totalPoor: 45,
+ },
+ {
+ name: "Manesa",
+ code: "MNS",
+ totalPopulation: 950,
+ totalKK: 240,
+ totalPoor: 32,
+ },
+ {
+ name: "Cabe",
+ code: "CBE",
+ totalPopulation: 800,
+ totalKK: 200,
+ totalPoor: 28,
+ },
+ {
+ name: "Penenjoan",
+ code: "PNJ",
+ totalPopulation: 1100,
+ totalKK: 280,
+ totalPoor: 50,
+ },
+ {
+ name: "Baler Pasar",
+ code: "BPS",
+ totalPopulation: 850,
+ totalKK: 210,
+ totalPoor: 35,
+ },
+ {
+ name: "Bucu",
+ code: "BCU",
+ totalPopulation: 734,
+ totalKK: 184,
+ totalPoor: 24,
},
];
- for (const userData of demoUsers) {
- try {
- const existingUser = await prisma.user.findUnique({
- where: { email: userData.email },
- });
+ console.log("Seeding Banjars...");
+ for (const banjar of banjars) {
+ await prisma.banjar.upsert({
+ where: { name: banjar.name },
+ update: banjar,
+ create: banjar,
+ });
+ }
+}
- if (!existingUser) {
- const userId = generateId();
- const hashedPassword = await hash("demo123", 12);
+async function seedDivisions() {
+ const divisions = [
+ {
+ name: "Pemerintahan",
+ description: "Urusan administrasi dan tata kelola desa",
+ color: "#1E3A5F",
+ },
+ {
+ name: "Pembangunan",
+ description: "Infrastruktur dan sarana prasarana desa",
+ color: "#2E7D32",
+ },
+ {
+ name: "Pemberdayaan",
+ description: "Pemberdayaan ekonomi dan masyarakat",
+ color: "#EF6C00",
+ },
+ {
+ name: "Kesejahteraan",
+ description: "Kesehatan, pendidikan, dan sosial",
+ color: "#C62828",
+ },
+ ];
- await prisma.user.create({
- data: {
- id: userId,
- email: userData.email,
- name: userData.name,
- role: userData.role,
- emailVerified: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- });
+ console.log("Seeding Divisions...");
+ const createdDivisions = [];
+ for (const div of divisions) {
+ const d = await prisma.division.upsert({
+ where: { name: div.name },
+ update: div,
+ create: div,
+ });
+ createdDivisions.push(d);
+ }
+ return createdDivisions;
+}
- await prisma.account.create({
- data: {
- id: generateId(),
- userId,
- accountId: userId,
- providerId: "credential",
- password: hashedPassword,
- createdAt: new Date(),
- updatedAt: new Date(),
- },
- });
+async function seedResidents(banjarIds: string[]) {
+ console.log("Seeding Residents...");
+ const residents = [
+ {
+ nik: "5103010101700001",
+ kk: "5103010101700000",
+ name: "I Wayan Sudarsana",
+ birthDate: new Date("1970-05-15"),
+ birthPlace: "Badung",
+ gender: Gender.LAKI_LAKI,
+ religion: Religion.HINDU,
+ occupation: "Wiraswasta",
+ banjarId: banjarIds[0],
+ rt: "001",
+ rw: "000",
+ address: "Jl. Raya Darmasaba No. 1",
+ isHeadOfHousehold: true,
+ },
+ {
+ nik: "5103010101850002",
+ kk: "5103010101850000",
+ name: "Ni Made Arianti",
+ birthDate: new Date("1985-08-20"),
+ birthPlace: "Denpasar",
+ gender: Gender.PEREMPUAN,
+ religion: Religion.HINDU,
+ occupation: "Guru",
+ banjarId: banjarIds[1],
+ rt: "002",
+ rw: "000",
+ address: "Gg. Manesa No. 5",
+ isPoor: true,
+ },
+ ];
- console.log(`Demo user created: ${userData.email}`);
- } else {
- console.log(`Demo user already exists: ${userData.email}`);
- }
- } catch (error) {
- console.error(`Error seeding user ${userData.email}:`, error);
- }
+ for (const res of residents) {
+ await prisma.resident.upsert({
+ where: { nik: res.nik },
+ update: res,
+ create: res,
+ });
+ }
+}
+
+async function seedActivities(divisionIds: string[]) {
+ console.log("Seeding Activities...");
+ const activities = [
+ {
+ title: "Rapat Koordinasi 2025",
+ description: "Penyusunan rencana kerja tahunan",
+ divisionId: divisionIds[0],
+ progress: 100,
+ status: ActivityStatus.SELESAI,
+ priority: Priority.TINGGI,
+ },
+ {
+ title: "Pemutakhiran Indeks Desa",
+ description: "Pendataan SDG's Desa 2025",
+ divisionId: divisionIds[0],
+ progress: 65,
+ status: ActivityStatus.BERJALAN,
+ priority: Priority.SEDANG,
+ },
+ {
+ title: "Pembangunan Jalan Banjar Cabe",
+ description: "Pengaspalan jalan utama",
+ divisionId: divisionIds[1],
+ progress: 40,
+ status: ActivityStatus.BERJALAN,
+ priority: Priority.DARURAT,
+ },
+ ];
+
+ for (const act of activities) {
+ await prisma.activity.create({
+ data: act,
+ });
+ }
+}
+
+async function seedComplaints(adminId: string) {
+ console.log("Seeding Complaints...");
+ const complaints = [
+ {
+ complaintNumber: `COMP-20250326-001`,
+ title: "Lampu Jalan Mati",
+ description:
+ "Lampu jalan di depan Balai Banjar Manesa mati sejak 3 hari lalu.",
+ category: ComplaintCategory.INFRASTRUKTUR,
+ status: ComplaintStatus.BARU,
+ priority: Priority.SEDANG,
+ location: "Banjar Manesa",
+ reporterId: adminId,
+ },
+ {
+ complaintNumber: `COMP-20250326-002`,
+ title: "Sampah Menumpuk",
+ description: "Tumpukan sampah di area pasar Darmasaba belum diangkut.",
+ category: ComplaintCategory.KETERTIBAN_UMUM,
+ status: ComplaintStatus.DIPROSES,
+ priority: Priority.TINGGI,
+ location: "Pasar Darmasaba",
+ assignedTo: adminId,
+ },
+ ];
+
+ for (const comp of complaints) {
+ await prisma.complaint.upsert({
+ where: { complaintNumber: comp.complaintNumber },
+ update: comp,
+ create: comp,
+ });
+ }
+}
+
+async function seedEvents(adminId: string) {
+ console.log("Seeding Events...");
+ const events = [
+ {
+ title: "Rapat Pleno Desa",
+ description: "Pembahasan anggaran belanja desa",
+ eventType: EventType.RAPAT,
+ startDate: new Date(),
+ location: "Balai Desa Darmasaba",
+ createdBy: adminId,
+ },
+ {
+ title: "Gotong Royong Kebersihan",
+ description: "Kegiatan rutin mingguan",
+ eventType: EventType.SOSIAL,
+ startDate: new Date(Date.now() + 86400000), // Besok
+ location: "Seluruh Banjar",
+ createdBy: adminId,
+ },
+ ];
+
+ for (const event of events) {
+ await prisma.event.create({
+ data: event,
+ });
}
}
async function main() {
- console.log("Seeding database...");
+ console.log("Starting seed...");
- await seedAdminUser();
- await seedDemoUsers();
+ const adminId = await seedAdminUser();
+ await seedBanjars();
+ const banjars = await prisma.banjar.findMany();
+ const banjarIds = banjars.map((b) => b.id);
- console.log("Database seeding completed.");
+ const divisions = await seedDivisions();
+ const divisionIds = divisions.map((d) => d.id);
+
+ await seedResidents(banjarIds);
+ await seedActivities(divisionIds);
+ await seedComplaints(adminId);
+ await seedEvents(adminId);
+
+ console.log("Seed finished successfully!");
}
-// Only auto-execute when run directly (not when imported)
-const isMainModule =
- typeof require !== "undefined"
- ? require.main === module
- : import.meta.path.endsWith("seed.ts");
-
-if (isMainModule) {
- main().catch((error) => {
- console.error("Error during seeding:", error);
+main()
+ .catch((e) => {
+ console.error(e);
process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
});
-}
-
-// Export for programmatic use
-export { seedAdminUser, seedDemoUsers, main as runSeed };
diff --git a/src/api/complaint.ts b/src/api/complaint.ts
new file mode 100644
index 0000000..77b977c
--- /dev/null
+++ b/src/api/complaint.ts
@@ -0,0 +1,66 @@
+import Elysia from "elysia";
+import { prisma } from "../utils/db";
+import logger from "../utils/logger";
+
+export const complaint = new Elysia({
+ prefix: "/complaint",
+})
+ .get(
+ "/stats",
+ async ({ set }) => {
+ try {
+ const [total, baru, proses, selesai] = await Promise.all([
+ prisma.complaint.count(),
+ prisma.complaint.count({ where: { status: "BARU" } }),
+ prisma.complaint.count({ where: { status: "DIPROSES" } }),
+ prisma.complaint.count({ where: { status: "SELESAI" } }),
+ ]);
+ return { data: { total, baru, proses, selesai } };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch complaint stats");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get complaint statistics" },
+ },
+ )
+ .get(
+ "/recent",
+ async ({ set }) => {
+ try {
+ const recent = await prisma.complaint.findMany({
+ orderBy: { createdAt: "desc" },
+ take: 10,
+ });
+ return { data: recent };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch recent complaints");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get recent complaints" },
+ },
+ )
+ .get(
+ "/service-stats",
+ async ({ set }) => {
+ try {
+ const serviceStats = await prisma.serviceLetter.groupBy({
+ by: ["letterType"],
+ _count: { _all: true },
+ });
+ return { data: serviceStats };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch service stats");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get service letter statistics by type" },
+ },
+ );
diff --git a/src/api/division.ts b/src/api/division.ts
new file mode 100644
index 0000000..ac9d8c9
--- /dev/null
+++ b/src/api/division.ts
@@ -0,0 +1,71 @@
+import Elysia from "elysia";
+import { prisma } from "../utils/db";
+import logger from "../utils/logger";
+
+export const division = new Elysia({
+ prefix: "/division",
+})
+ .get(
+ "/",
+ async ({ set }) => {
+ try {
+ const divisions = await prisma.division.findMany({
+ include: {
+ _count: {
+ select: { activities: true },
+ },
+ },
+ });
+ return { data: divisions };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch divisions");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get all divisions" },
+ },
+ )
+ .get(
+ "/activities",
+ async ({ set }) => {
+ try {
+ const activities = await prisma.activity.findMany({
+ include: {
+ division: {
+ select: { name: true, color: true },
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ take: 10,
+ });
+ return { data: activities };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch activities");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get recent activities" },
+ },
+ )
+ .get(
+ "/metrics",
+ async ({ set }) => {
+ try {
+ const metrics = await prisma.divisionMetric.findMany({
+ include: { division: true },
+ });
+ return { data: metrics };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch division metrics");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get division performance metrics" },
+ },
+ );
diff --git a/src/api/event.ts b/src/api/event.ts
new file mode 100644
index 0000000..54e57ae
--- /dev/null
+++ b/src/api/event.ts
@@ -0,0 +1,54 @@
+import Elysia from "elysia";
+import { prisma } from "../utils/db";
+import logger from "../utils/logger";
+
+export const event = new Elysia({
+ prefix: "/event",
+})
+ .get(
+ "/",
+ async ({ set }) => {
+ try {
+ const events = await prisma.event.findMany({
+ orderBy: { startDate: "asc" },
+ take: 20,
+ });
+ return { data: events };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch events");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get upcoming events" },
+ },
+ )
+ .get(
+ "/today",
+ async ({ set }) => {
+ try {
+ const start = new Date();
+ start.setHours(0, 0, 0, 0);
+ const end = new Date();
+ end.setHours(23, 59, 59, 999);
+
+ const events = await prisma.event.findMany({
+ where: {
+ startDate: {
+ gte: start,
+ lte: end,
+ },
+ },
+ });
+ return { data: events };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch today's events");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get events for today" },
+ },
+ );
diff --git a/src/api/index.tsx b/src/api/index.tsx
index 5b51a72..e44bd6d 100644
--- a/src/api/index.tsx
+++ b/src/api/index.tsx
@@ -4,7 +4,11 @@ import Elysia from "elysia";
import { apiMiddleware } from "../middleware/apiMiddleware";
import { auth } from "../utils/auth";
import { apikey } from "./apikey";
+import { complaint } from "./complaint";
+import { division } from "./division";
+import { event } from "./event";
import { profile } from "./profile";
+import { resident } from "./resident";
const isProduction = process.env.NODE_ENV === "production";
@@ -20,7 +24,11 @@ const api = new Elysia({
})
.use(apiMiddleware)
.use(apikey)
- .use(profile);
+ .use(profile)
+ .use(division)
+ .use(complaint)
+ .use(resident)
+ .use(event);
if (!isProduction) {
api.use(
diff --git a/src/api/profile.ts b/src/api/profile.ts
index cca62d3..9f3dd33 100644
--- a/src/api/profile.ts
+++ b/src/api/profile.ts
@@ -2,12 +2,25 @@ import Elysia, { t } from "elysia";
import { prisma } from "../utils/db";
import logger from "../utils/logger";
+interface AuthenticatedUser {
+ id: string;
+ email: string;
+ name?: string | null;
+}
+
export const profile = new Elysia({
prefix: "/profile",
}).post(
"/update",
- async (ctx) => {
- const { body, set, user } = ctx as any;
+ async ({
+ body,
+ set,
+ user,
+ }: {
+ body: { name?: string; image?: string };
+ set: any;
+ user?: AuthenticatedUser;
+ }) => {
try {
if (!user) {
set.status = 401;
diff --git a/src/api/resident.ts b/src/api/resident.ts
new file mode 100644
index 0000000..36eba90
--- /dev/null
+++ b/src/api/resident.ts
@@ -0,0 +1,76 @@
+import Elysia from "elysia";
+import { prisma } from "../utils/db";
+import logger from "../utils/logger";
+
+export const resident = new Elysia({
+ prefix: "/resident",
+})
+ .get(
+ "/stats",
+ async ({ set }) => {
+ try {
+ const [total, heads, poor] = await Promise.all([
+ prisma.resident.count(),
+ prisma.resident.count({ where: { isHeadOfHousehold: true } }),
+ prisma.resident.count({ where: { isPoor: true } }),
+ ]);
+ return { data: { total, heads, poor } };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch resident stats");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get resident statistics" },
+ },
+ )
+ .get(
+ "/banjar-stats",
+ async ({ set }) => {
+ try {
+ const banjarStats = await prisma.banjar.findMany({
+ select: {
+ id: true,
+ name: true,
+ totalPopulation: true,
+ totalKK: true,
+ totalPoor: true,
+ },
+ });
+ return { data: banjarStats };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch banjar stats");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get population data per banjar" },
+ },
+ )
+ .get(
+ "/demographics",
+ async ({ set }) => {
+ try {
+ const [religion, gender] = await Promise.all([
+ prisma.resident.groupBy({
+ by: ["religion"],
+ _count: { _all: true },
+ }),
+ prisma.resident.groupBy({
+ by: ["gender"],
+ _count: { _all: true },
+ }),
+ ]);
+ return { data: { religion, gender } };
+ } catch (error) {
+ logger.error({ error }, "Failed to fetch demographics");
+ set.status = 500;
+ return { error: "Internal Server Error" };
+ }
+ },
+ {
+ detail: { summary: "Get religious and gender demographics" },
+ },
+ );
diff --git a/src/components/dashboard-content.tsx b/src/components/dashboard-content.tsx
index 7407098..1173e83 100644
--- a/src/components/dashboard-content.tsx
+++ b/src/components/dashboard-content.tsx
@@ -1,5 +1,7 @@
-import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
+import { Grid, Image, Stack } from "@mantine/core";
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
+import { useEffect, useState } from "react";
+import { apiClient } from "@/utils/api-client";
import { ActivityList } from "./dashboard/activity-list";
import { ChartAPBDes } from "./dashboard/chart-apbdes";
import { ChartSurat } from "./dashboard/chart-surat";
@@ -32,8 +34,38 @@ const sdgsData = [
];
export function DashboardContent() {
- const { colorScheme } = useMantineColorScheme();
- const dark = colorScheme === "dark";
+ const [stats, setStats] = useState({
+ complaints: { total: 0, baru: 0, proses: 0, selesai: 0 },
+ residents: { total: 0, heads: 0, poor: 0 },
+ loading: true,
+ });
+
+ useEffect(() => {
+ async function fetchStats() {
+ try {
+ const [complaintRes, residentRes] = await Promise.all([
+ apiClient.GET("/api/complaint/stats"),
+ apiClient.GET("/api/resident/stats"),
+ ]);
+
+ setStats({
+ complaints: (complaintRes.data as any)?.data || {
+ total: 0,
+ baru: 0,
+ proses: 0,
+ selesai: 0,
+ },
+ residents: (residentRes.data as any)?.data || { total: 0, heads: 0, poor: 0 },
+ loading: false,
+ });
+ } catch (error) {
+ console.error("Failed to fetch stats", error);
+ setStats((prev) => ({ ...prev, loading: false }));
+ }
+ }
+
+ fetchStats();
+ }, []);
return (
@@ -42,36 +74,36 @@ export function DashboardContent() {
}
/>
}
/>
}
/>
}
/>
@@ -102,8 +134,8 @@ export function DashboardContent() {
{/* Section 6: SDGs Desa Cards */}
- {sdgsData.map((sdg, index) => (
-
+ {sdgsData.map((sdg) => (
+
}
title={sdg.title}
diff --git a/src/components/dashboard/activity-list.tsx b/src/components/dashboard/activity-list.tsx
index 648192e..9c1e95b 100644
--- a/src/components/dashboard/activity-list.tsx
+++ b/src/components/dashboard/activity-list.tsx
@@ -48,9 +48,9 @@ export function ActivityList() {
- {events.map((event, index) => (
+ {events.map((event) => (
- {apbdesData.map((item, index) => (
-
+ {apbdesData.map((item) => (
+
{item.name}
diff --git a/src/components/dashboard/division-progress.tsx b/src/components/dashboard/division-progress.tsx
index e92e59e..fbf90e5 100644
--- a/src/components/dashboard/division-progress.tsx
+++ b/src/components/dashboard/division-progress.tsx
@@ -45,8 +45,8 @@ export function DivisionProgress() {
Divisi Teraktif
- {divisionData.map((divisi, index) => (
-
+ {divisionData.map((divisi) => (
+
{divisi.name}
diff --git a/src/components/dashboard/satisfaction-chart.tsx b/src/components/dashboard/satisfaction-chart.tsx
index 8e79a2c..862ccfa 100644
--- a/src/components/dashboard/satisfaction-chart.tsx
+++ b/src/components/dashboard/satisfaction-chart.tsx
@@ -2,7 +2,6 @@ import {
Box,
Card,
Group,
- Stack,
Text,
Title,
useMantineColorScheme,
@@ -51,8 +50,8 @@ export function SatisfactionChart() {
paddingAngle={2}
dataKey="value"
>
- {satisfactionData.map((entry, index) => (
- |
+ {satisfactionData.map((entry) => (
+ |
))}
- {satisfactionData.map((item, index) => (
-
+ {satisfactionData.map((item) => (
+
(null);
+ const tooltipRef = useRef(null);
+ const lastInfoRef = useRef(null);
+
+ const updateOverlay = useCallback((target: HTMLElement | null) => {
+ const ov = overlayRef.current;
+ const tt = tooltipRef.current;
+ if (!ov || !tt) return;
+
+ if (!target) {
+ ov.style.display = "none";
+ tt.style.display = "none";
+ lastInfoRef.current = null;
+ return;
+ }
+
+ const info = findCodeInfo(target);
+ if (!info) {
+ ov.style.display = "none";
+ tt.style.display = "none";
+ lastInfoRef.current = null;
+ return;
+ }
+
+ lastInfoRef.current = info;
+
+ const rect = target.getBoundingClientRect();
+ ov.style.display = "block";
+ ov.style.top = `${rect.top + window.scrollY}px`;
+ ov.style.left = `${rect.left + window.scrollX}px`;
+ ov.style.width = `${rect.width}px`;
+ ov.style.height = `${rect.height}px`;
+
+ tt.style.display = "block";
+ tt.textContent = `${info.relativePath}:${info.line}`;
+ const ttTop = rect.top + window.scrollY - 24;
+ tt.style.top = `${ttTop > 0 ? ttTop : rect.bottom + window.scrollY + 4}px`;
+ tt.style.left = `${rect.left + window.scrollX}px`;
+ }, []);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: updateOverlay is stable
+ useEffect(() => {
+ if (!active) return;
+
+ const onMouseOver = (e: MouseEvent) =>
+ updateOverlay(e.target as HTMLElement);
+
+ const onClick = (e: MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const info = lastInfoRef.current ?? findCodeInfo(e.target as HTMLElement);
+ if (info) {
+ const loc = `${info.relativePath}:${info.line}:${info.column}`;
+ console.log("[DevInspector] Open:", loc);
+ openInEditor(info);
+ }
+ setActive(false);
+ };
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") setActive(false);
+ };
+
+ document.addEventListener("mouseover", onMouseOver, true);
+ document.addEventListener("click", onClick, true);
+ document.addEventListener("keydown", onKeyDown);
+ document.body.style.cursor = "crosshair";
+
+ return () => {
+ document.removeEventListener("mouseover", onMouseOver, true);
+ document.removeEventListener("click", onClick, true);
+ document.removeEventListener("keydown", onKeyDown);
+ document.body.style.cursor = "";
+ if (overlayRef.current) overlayRef.current.style.display = "none";
+ if (tooltipRef.current) tooltipRef.current.style.display = "none";
+ };
+ }, [active, updateOverlay]);
+
+ // Hotkey: Ctrl+Shift+Cmd+C (macOS) / Ctrl+Shift+Alt+C
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (
+ e.key.toLowerCase() === "c" &&
+ e.ctrlKey &&
+ e.shiftKey &&
+ (e.metaKey || e.altKey)
+ ) {
+ e.preventDefault();
+ setActive((prev) => !prev);
+ }
+ };
+ document.addEventListener("keydown", onKeyDown);
+ return () => document.removeEventListener("keydown", onKeyDown);
+ }, []);
+
+ return (
+ <>
+ {children}
+
+
+ >
+ );
+}
diff --git a/src/components/header.tsx b/src/components/header.tsx
index aa77820..6983c7a 100644
--- a/src/components/header.tsx
+++ b/src/components/header.tsx
@@ -6,7 +6,6 @@ import {
Divider,
Group,
Text,
- Title,
useMantineColorScheme,
} from "@mantine/core";
import {
@@ -21,44 +20,10 @@ interface HeaderProps {
}
export function Header({ onSidebarToggle }: HeaderProps) {
- const location = useLocation();
+ const _location = useLocation();
+ const navigate = useNavigate();
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
- const navigate = useNavigate();
-
- // Define page titles based on route
- const getPageTitle = () => {
- switch (location.pathname) {
- case "/":
- return "Beranda";
- case "/kinerja-divisi":
- return "Kinerja Divisi";
- case "/pengaduan-layanan-publik":
- return "Pengaduan & Layanan Publik";
- case "/jenna-analytic":
- return "Jenna Analytic";
- case "/demografi-pekerjaan":
- return "Demografi & Kependudukan";
- case "/keuangan-anggaran":
- return "Keuangan & Anggaran";
- case "/bumdes":
- return "Bumdes & UMKM Desa";
- case "/sosial":
- return "Sosial";
- case "/keamanan":
- return "Keamanan";
- case "/bantuan":
- return "Bantuan";
- case "/pengaturan":
- case "/pengaturan/umum":
- case "/pengaturan/notifikasi":
- case "/pengaturan/keamanan":
- case "/pengaturan/akses-dan-tim":
- return "Pengaturan";
- default:
- return "Desa Darmasaba";
- }
- };
return (
@@ -77,9 +42,6 @@ export function Header({ onSidebarToggle }: HeaderProps) {
style={{ width: "70%", height: "70%" }}
/>
- {/*
- {getPageTitle()}
- */}
{/* Right Section */}
diff --git a/src/components/help-page.tsx b/src/components/help-page.tsx
index d1183e4..b616e1d 100644
--- a/src/components/help-page.tsx
+++ b/src/components/help-page.tsx
@@ -152,9 +152,9 @@ const HelpPage = () => {
{/* Statistics Section */}
- {stats.map((stat, index) => (
+ {stats.map((stat) => (
{
h="100%"
>
- {guideItems.map((item, index) => (
+ {guideItems.map((item) => (
{
h="100%"
>
- {videoItems.map((item, index) => (
+ {videoItems.map((item) => (
{
h="100%"
>
- {faqItems.map((item, index) => (
+ {faqItems.map((item) => (
{item.question}
@@ -335,9 +335,9 @@ const HelpPage = () => {
h="100%"
>
- {documentationItems.map((item, index) => (
+ {documentationItems.map((item) => (
{
disabled={isLoading}
/>
@@ -328,42 +325,52 @@ const PengaduanLayananPublik = () => {
Pengajuan Terbaru
- {pengajuanTerbaru.map((item, index) => (
-
-
-
-
- {item.nama}
-
-
- {item.jenis}
-
-
-
-
- {item.status}
-
-
- {item.waktu}
-
-
-
-
- ))}
+ {loading ? (
+
+
+
+ ) : recentComplaints.length > 0 ? (
+ recentComplaints.map((item, index) => (
+
+
+
+
+ {item.title}
+
+
+ {item.category}
+
+
+
+
+ {item.status}
+
+
+ {dayjs(item.createdAt).fromNow()}
+
+
+
+
+ ))
+ ) : (
+
+ Tidak ada pengajuan terbaru
+
+ )}
diff --git a/src/frontend.tsx b/src/frontend.tsx
index 347e765..bd238d0 100644
--- a/src/frontend.tsx
+++ b/src/frontend.tsx
@@ -10,12 +10,11 @@
import { createTheme, MantineProvider } from "@mantine/core";
import { ModalsProvider } from "@mantine/modals";
import { createRouter, RouterProvider } from "@tanstack/react-router";
-import { Inspector } from "react-dev-inspector";
import { createRoot } from "react-dom/client";
import { routeTree } from "./routeTree.gen";
import "./index.css";
import "@mantine/charts/styles.css";
-import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env";
+import { IS_DEV } from "./utils/env";
// Create a new router instance
export const router = createRouter({
@@ -101,29 +100,14 @@ const theme = createTheme({
primaryColor: "darmasaba-blue",
});
+// Use dynamic import for DevInspector to avoid including it in production bundle
const InspectorWrapper = IS_DEV
- ? Inspector
+ ? (await import("./components/dev-inspector")).DevInspector
: ({ children }: { children: React.ReactNode }) => <>{children}>;
const elem = document.getElementById("root")!;
const app = (
- {
- if (!e.codeInfo) return;
-
- const url = VITE_PUBLIC_URL;
- fetch(`${url}/__open-in-editor`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- relativePath: e.codeInfo.relativePath,
- lineNumber: e.codeInfo.lineNumber,
- columnNumber: e.codeInfo.columnNumber,
- }),
- });
- }}
- >
+
diff --git a/src/index.ts b/src/index.ts
index 13e163c..e6a0ad7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,7 +6,7 @@ import { Elysia } from "elysia";
import api from "./api";
import { openInEditor } from "./utils/open-in-editor";
-const PORT = process.env.PORT || 3000;
+const PORT = Number(process.env.PORT || 3000);
const isProduction = process.env.NODE_ENV === "production";
@@ -35,14 +35,16 @@ if (!isProduction) {
app.post("/__open-in-editor", ({ body }) => {
const { relativePath, lineNumber, columnNumber } = body as {
relativePath: string;
- lineNumber: number;
- columnNumber: number;
+ lineNumber: string;
+ columnNumber: string;
};
+ const editor = (process.env.REACT_EDITOR || "code") as any;
+
openInEditor(relativePath, {
- line: lineNumber,
- column: columnNumber,
- editor: "antigravity",
+ line: Number(lineNumber),
+ column: Number(columnNumber),
+ editor: editor,
});
return { ok: true };
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 7396804..dc45ab2 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -11,7 +11,7 @@ export const Route = createRootRoute({
// Apply protected route middleware for all routes
// The middleware will determine which routes are public vs protected
const context = await protectedRouteMiddleware({ location });
-
+
// Only set auth store if we have user data (for protected routes)
if (context?.user) {
authStore.user = context?.user as any;
diff --git a/src/utils/dev-inspector-plugin.ts b/src/utils/dev-inspector-plugin.ts
new file mode 100644
index 0000000..e1cea55
--- /dev/null
+++ b/src/utils/dev-inspector-plugin.ts
@@ -0,0 +1,53 @@
+import path from "node:path";
+import type { Plugin } from "vite";
+
+/**
+ * Vite Plugin to inject data-inspector-* attributes into JSX elements.
+ * This enables click-to-source functionality in the browser.
+ */
+export function inspectorPlugin(): Plugin {
+ const rootDir = process.cwd();
+
+ return {
+ name: "inspector-inject",
+ enforce: "pre",
+ transform(code, id) {
+ // Only process .tsx and .jsx files, skip node_modules
+ if (!/\.[jt]sx(\?|$)/.test(id) || id.includes("node_modules"))
+ return null;
+ if (!code.includes("<")) return null;
+
+ const relativePath = path.relative(rootDir, id);
+ let modified = false;
+ const lines = code.split("\n");
+ const result: string[] = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ let line = lines[i];
+ // Match JSX opening tags: 0 ? line[match.index - 1] : "";
+ if (/[a-zA-Z0-9_$.]/.test(charBefore)) continue;
+
+ const col = match.index + 1;
+ const attr = ` data-inspector-line="${i + 1}" data-inspector-column="${col}" data-inspector-relative-path="${relativePath}"`;
+ const insertPos = match.index + match[0].length;
+ line = line.slice(0, insertPos) + attr + line.slice(insertPos);
+ modified = true;
+ jsxPattern.lastIndex += attr.length;
+ }
+
+ result.push(line);
+ }
+
+ if (!modified) return null;
+ return result.join("\n");
+ },
+ };
+}
diff --git a/src/vite.ts b/src/vite.ts
index 5af027a..87ff28e 100644
--- a/src/vite.ts
+++ b/src/vite.ts
@@ -1,9 +1,9 @@
import path from "node:path";
-import { inspectorServer } from "@react-dev-inspector/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-vite-plugin";
import react from "@vitejs/plugin-react";
import { createServer as createViteServer } from "vite";
+import { inspectorPlugin } from "./utils/dev-inspector-plugin";
export async function createVite() {
return createViteServer({
@@ -14,23 +14,7 @@ export async function createVite() {
"@": path.resolve(process.cwd(), "./src"),
},
},
- plugins: [
- tailwindcss(),
- react({
- babel: {
- plugins: [
- [
- "@react-dev-inspector/babel-plugin",
- {
- relativePath: true,
- },
- ],
- ],
- },
- }),
- inspectorServer(),
- tanstackRouter(),
- ],
+ plugins: [tailwindcss(), inspectorPlugin(), react(), tanstackRouter()],
server: {
middlewareMode: true,
hmr: {