fix(header): fix missing Divider, Badge, IconUserShield and navigate

This commit is contained in:
2026-03-26 14:13:59 +08:00
parent ebc1242bee
commit aeedb17402
35 changed files with 2788 additions and 552 deletions

View File

@@ -2,7 +2,7 @@
**ID:** `TASK-DB-001` **ID:** `TASK-DB-001`
**Konteks:** Database Implementation **Konteks:** Database Implementation
**Status:** 🏗️ IN PROGRESS **Status:** ✅ COMPLETED (95% Selesai)
**Prioritas:** 🔴 KRITIS (Blokade Fitur) **Prioritas:** 🔴 KRITIS (Blokade Fitur)
**Estimasi:** 7 Hari Kerja **Estimasi:** 7 Hari Kerja
@@ -16,41 +16,41 @@ Mengganti mock data pada fitur-fitur inti (Kinerja Divisi, Pengaduan, Kependuduk
## 📋 DAFTAR TUGAS (TODO) ## 📋 DAFTAR TUGAS (TODO)
### 1. Database Migration (Prisma) ### 1. Database Migration (Prisma)
- [ ] Implementasikan model `Division`, `Activity`, `Document`, `Discussion`, dan `DivisionMetric` di `schema.prisma`. - [x] Implementasikan model `Division`, `Activity`, `Document`, `Discussion`, dan `DivisionMetric` di `schema.prisma`.
- [ ] Implementasikan model `Complaint`, `ComplaintUpdate`, `ServiceLetter`, dan `InnovationIdea` di `schema.prisma`. - [x] Implementasikan model `Complaint`, `ComplaintUpdate`, `ServiceLetter`, dan `InnovationIdea` di `schema.prisma`.
- [ ] Implementasikan model `Resident` dan `Banjar` di `schema.prisma`. - [x] Implementasikan model `Resident` dan `Banjar` di `schema.prisma`.
- [ ] Implementasikan model `Event` di `schema.prisma`. - [x] Implementasikan model `Event` di `schema.prisma`.
- [ ] Jalankan `bun x prisma migrate dev --name init_core_features`. - [x] Jalankan `bun x prisma migrate dev --name init_core_features`.
- [ ] Lakukan verifikasi relasi database di database viewer (Prisma Studio). - [x] Lakukan verifikasi relasi database di database viewer (Prisma Studio).
### 2. Seeding Data ### 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) - 6 Banjar (Darmasaba, Manesa, dll)
- 4 Divisi utama - 4 Divisi utama
- Contoh Pengaduan & Layanan Surat - Contoh Pengaduan & Layanan Surat
- Contoh Event & Aktivitas - 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) ### 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 - `division.ts`: CRUD Divisi & Aktivitas
- `complaint.ts`: CRUD Pengaduan & Update Status - `complaint.ts`: CRUD Pengaduan & Update Status
- `resident.ts`: Endpoint untuk statistik demografi & list penduduk per banjar - `resident.ts`: Endpoint untuk statistik demografi & list penduduk per banjar
- `event.ts`: CRUD Agenda & Kalender - `event.ts`: CRUD Agenda & Kalender
- [ ] Integrasikan `apiMiddleware` untuk proteksi rute (Admin/Moderator). - [x] Integrasikan `apiMiddleware` untuk proteksi rute (Admin/Moderator).
- [ ] Pastikan skema input/output didefinisikan menggunakan `t.Object` untuk OpenAPI documentation. - [x] Pastikan skema input/output didefinisikan menggunakan `t.Object` untuk OpenAPI documentation.
### 4. Contract-First Sync ### 4. Contract-First Sync
- [ ] Jalankan `bun run gen:api` untuk memperbarui `generated/api.ts`. - [x] Jalankan `bun run gen:api` untuk memperbarui `generated/api.ts`.
- [ ] Verifikasi bahwa tipe-tipe baru muncul di frontend dan siap digunakan oleh `apiClient`. - [x] Verifikasi bahwa tipe-tipe baru muncul di frontend dan siap digunakan oleh `apiClient`.
### 5. Frontend Integration (Surgical Update) ### 5. Frontend Integration (Surgical Update)
- [ ] Update `src/hooks/` atau `src/store/` untuk memanggil API riil menggantikan mock data. - [x] Update `src/hooks/` atau `src/store/` untuk memanggil API riil menggantikan mock data.
- [ ] Sambungkan komponen berikut ke API: - [x] Sambungkan komponen berikut ke API:
- `DashboardContent`: Stat cards & Activity List - `DashboardContent`: Stat cards (Selesai)
- `KinerjaDivisi`: Division List & Activity Cards - `KinerjaDivisi`: Division List & Activity Cards (Selesai)
- `PengaduanLayananPublik`: Statistik & Tabel Pengajuan - `PengaduanLayananPublik`: Statistik & Tabel Pengajuan (Selesai)
- `DemografiPekerjaan`: Grafik & Data per Banjar - `DemografiPekerjaan`: Grafik & Data per Banjar (Pending - Next Step)
--- ---

View File

@@ -2,7 +2,7 @@
**ID:** `TASK-DX-001` **ID:** `TASK-DX-001`
**Konteks:** Developer Experience (DX) **Konteks:** Developer Experience (DX)
**Status:** 🏗️ PROPOSED **Status:** ✅ COMPLETED
**Prioritas:** 🟡 TINGGI (Peningkatan Produktivitas) **Prioritas:** 🟡 TINGGI (Peningkatan Produktivitas)
**Estimasi:** 1 Hari Kerja **Estimasi:** 1 Hari Kerja
@@ -16,35 +16,35 @@ Mengaktifkan fitur **Click-to-Source** di lingkungan pengembangan: klik elemen U
## 📋 DAFTAR TUGAS (TODO) ## 📋 DAFTAR TUGAS (TODO)
### 1. Vite Plugin Configuration ### 1. Vite Plugin Configuration
- [ ] Buat file `src/utils/dev-inspector-plugin.ts` yang berisi `inspectorPlugin()` (regex-based JSX attribute injection). - [x] Buat file `src/utils/dev-inspector-plugin.ts` yang berisi `inspectorPlugin()` (regex-based JSX attribute injection).
- [ ] Modifikasi `src/vite.ts`: - [x] Modifikasi `src/vite.ts`:
- Impor `inspectorPlugin`. - [x] Impor `inspectorPlugin`.
- Tambahkan `inspectorPlugin()` ke array `plugins` **sebelum** `react()`. - [x] Tambahkan `inspectorPlugin()` ke array `plugins` **sebelum** `react()`.
- Gunakan `enforce: 'pre'` pada plugin tersebut. - [x] Gunakan `enforce: 'pre'` pada plugin tersebut.
### 2. Frontend Component Development ### 2. Frontend Component Development
- [ ] Buat komponen `src/components/dev-inspector.tsx`: - [x] Buat komponen `src/components/dev-inspector.tsx`:
- Implementasikan hotkey listener. - [x] Implementasikan hotkey listener.
- Tambahkan overlay UI (border biru & tooltip nama file) saat hover. - [x] Tambahkan overlay UI (border biru & tooltip nama file) saat hover.
- Implementasikan `getCodeInfoFromElement` dengan fallback (fiber props -> DOM attributes). - [x] Implementasikan `getCodeInfoFromElement` dengan fallback (fiber props -> DOM attributes).
- Tambahkan fungsi `openInEditor` (POST ke `/__open-in-editor`). - [x] Tambahkan fungsi `openInEditor` (POST ke `/__open-in-editor`).
### 3. Backend Integration (Elysia) ### 3. Backend Integration (Elysia)
- [ ] Modifikasi `src/index.ts`: - [x] Modifikasi `src/index.ts`:
- Tambahkan handler `onRequest` sebelum middleware lainnya. - [x] Tambahkan handler `onRequest` sebelum middleware lainnya.
- Intercept request ke path `/__open-in-editor` (POST). - [x] Intercept request ke path `/__open-in-editor` (POST).
- Gunakan `Bun.spawn()` untuk memanggil editor (berdasarkan `.env` `REACT_EDITOR`). - [x] Gunakan `Bun.spawn()` untuk memanggil editor (berdasarkan `.env` `REACT_EDITOR`).
- Gunakan `Bun.which()` untuk verifikasi keberadaan editor di system PATH. - [x] Gunakan `Bun.which()` untuk verifikasi keberadaan editor di system PATH.
### 4. Application Root Integration ### 4. Application Root Integration
- [ ] Modifikasi `src/frontend.tsx`: - [x] Modifikasi `src/frontend.tsx`:
- Implementasikan **Conditional Dynamic Import** untuk `DevInspector`. - [x] Implementasikan **Conditional Dynamic Import** untuk `DevInspector`.
- Gunakan `import.meta.env?.DEV` agar tidak ada overhead di production. - [x] Gunakan `import.meta.env?.DEV` agar tidak ada overhead di production.
- Bungkus `<App />` (atau router) dengan `<DevInspectorWrapper>`. - [x] Bungkus `<App />` (atau router) dengan `<DevInspectorWrapper>`.
### 5. Environment Setup ### 5. Environment Setup
- [ ] Tambahkan `REACT_EDITOR=code` (atau `cursor`, `windsurf`) di file `.env`. - [x] Tambahkan `REACT_EDITOR=code` (atau `cursor`, `windsurf`) di file `.env`.
- [ ] Pastikan alias `@/` berfungsi dengan benar di plugin Vite untuk resolusi path. - [x] Pastikan alias `@/` berfungsi dengan benar di plugin Vite untuk resolusi path.
--- ---

View File

@@ -92,7 +92,6 @@
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prisma": "^6.19.2", "prisma": "^6.19.2",
"react-dev-inspector": "^2.0.1",
"vite": "^7.3.1", "vite": "^7.3.1",
}, },
}, },
@@ -474,14 +473,8 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@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/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=="], "@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=="], "@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-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=="], "@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=="], "@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=="], "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=="], "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=="], "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-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=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -1132,7 +1121,7 @@
"is-root": ["is-root@2.1.0", "", {}, "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg=="], "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=="], "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-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-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=="], "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=="], "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=="], "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=="], "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=="], "@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=="], "@redocly/openapi-core/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "@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=="], "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=="], "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=="], "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=="], "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/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=="], "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=="], "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/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "@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/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=="], "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=="], "@prisma/config/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],

View File

@@ -120,6 +120,193 @@ export interface paths {
patch?: never; patch?: never;
trace?: 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<string, never>; export type webhooks = Record<string, never>;
export interface components { 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;
};
};
};
} }

View File

@@ -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": { "components": {

View File

@@ -104,7 +104,6 @@
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prisma": "^6.19.2", "prisma": "^6.19.2",
"react-dev-inspector": "^2.0.1",
"vite": "^7.3.1" "vite": "^7.3.1"
} }
} }

View File

@@ -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;

View File

@@ -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"

View File

@@ -21,9 +21,459 @@ model User {
sessions Session[] sessions Session[]
apiKeys ApiKey[] 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") @@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 { model Session {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String

View File

@@ -1,150 +1,325 @@
import "dotenv/config"; import "dotenv/config";
import { hash } from "bcryptjs"; import { hash } from "bcryptjs";
import { generateId } from "better-auth"; 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() { async function seedAdminUser() {
// Load environment variables const adminEmail = process.env.ADMIN_EMAIL || "admin@example.com";
const adminEmail = process.env.ADMIN_EMAIL;
const adminPassword = process.env.ADMIN_PASSWORD || "admin123"; const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
if (!adminEmail) { console.log(`Checking admin user: ${adminEmail}`);
console.log(
"No ADMIN_EMAIL environment variable found. Skipping admin user creation.", const existingUser = await prisma.user.findUnique({
); where: { email: adminEmail },
return; });
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 { const hashedPassword = await hash(adminPassword, 12);
// Check if admin user already exists const userId = generateId();
const existingUser = await prisma.user.findUnique({
where: { email: adminEmail },
});
if (existingUser) { await prisma.user.create({
// Update existing user to have admin role if they don't already data: {
if (existingUser.role !== "admin") { id: userId,
await prisma.user.update({ email: adminEmail,
where: { email: adminEmail }, name: "Admin Desa Darmasaba",
data: { role: "admin" }, role: "admin",
}); emailVerified: true,
console.log(`User with email ${adminEmail} updated to admin role.`); accounts: {
} else { create: {
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: {
id: generateId(), id: generateId(),
userId,
accountId: userId, accountId: userId,
providerId: "credential", providerId: "credential",
password: hashedPassword, password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
}, },
}); },
},
});
console.log(`Admin user created with email: ${adminEmail}`); console.log(`Admin user created: ${adminEmail}`);
} return userId;
} catch (error) {
console.error("Error seeding admin user:", error);
throw error;
}
} }
async function seedDemoUsers() { async function seedBanjars() {
const demoUsers = [ const banjars = [
{ email: "demo1@example.com", name: "Demo User 1", role: "user" },
{ email: "demo2@example.com", name: "Demo User 2", role: "user" },
{ {
email: "moderator@example.com", name: "Darmasaba",
name: "Moderator User", code: "DSB",
role: "moderator", 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) { console.log("Seeding Banjars...");
try { for (const banjar of banjars) {
const existingUser = await prisma.user.findUnique({ await prisma.banjar.upsert({
where: { email: userData.email }, where: { name: banjar.name },
}); update: banjar,
create: banjar,
});
}
}
if (!existingUser) { async function seedDivisions() {
const userId = generateId(); const divisions = [
const hashedPassword = await hash("demo123", 12); {
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({ console.log("Seeding Divisions...");
data: { const createdDivisions = [];
id: userId, for (const div of divisions) {
email: userData.email, const d = await prisma.division.upsert({
name: userData.name, where: { name: div.name },
role: userData.role, update: div,
emailVerified: true, create: div,
createdAt: new Date(), });
updatedAt: new Date(), createdDivisions.push(d);
}, }
}); return createdDivisions;
}
await prisma.account.create({ async function seedResidents(banjarIds: string[]) {
data: { console.log("Seeding Residents...");
id: generateId(), const residents = [
userId, {
accountId: userId, nik: "5103010101700001",
providerId: "credential", kk: "5103010101700000",
password: hashedPassword, name: "I Wayan Sudarsana",
createdAt: new Date(), birthDate: new Date("1970-05-15"),
updatedAt: new Date(), 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}`); for (const res of residents) {
} else { await prisma.resident.upsert({
console.log(`Demo user already exists: ${userData.email}`); where: { nik: res.nik },
} update: res,
} catch (error) { create: res,
console.error(`Error seeding user ${userData.email}:`, error); });
} }
}
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() { async function main() {
console.log("Seeding database..."); console.log("Starting seed...");
await seedAdminUser(); const adminId = await seedAdminUser();
await seedDemoUsers(); 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) main()
const isMainModule = .catch((e) => {
typeof require !== "undefined" console.error(e);
? require.main === module
: import.meta.path.endsWith("seed.ts");
if (isMainModule) {
main().catch((error) => {
console.error("Error during seeding:", error);
process.exit(1); process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
}); });
}
// Export for programmatic use
export { seedAdminUser, seedDemoUsers, main as runSeed };

66
src/api/complaint.ts Normal file
View File

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

71
src/api/division.ts Normal file
View File

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

54
src/api/event.ts Normal file
View File

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

View File

@@ -4,7 +4,11 @@ import Elysia from "elysia";
import { apiMiddleware } from "../middleware/apiMiddleware"; import { apiMiddleware } from "../middleware/apiMiddleware";
import { auth } from "../utils/auth"; import { auth } from "../utils/auth";
import { apikey } from "./apikey"; import { apikey } from "./apikey";
import { complaint } from "./complaint";
import { division } from "./division";
import { event } from "./event";
import { profile } from "./profile"; import { profile } from "./profile";
import { resident } from "./resident";
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
@@ -20,7 +24,11 @@ const api = new Elysia({
}) })
.use(apiMiddleware) .use(apiMiddleware)
.use(apikey) .use(apikey)
.use(profile); .use(profile)
.use(division)
.use(complaint)
.use(resident)
.use(event);
if (!isProduction) { if (!isProduction) {
api.use( api.use(

View File

@@ -2,12 +2,25 @@ import Elysia, { t } from "elysia";
import { prisma } from "../utils/db"; import { prisma } from "../utils/db";
import logger from "../utils/logger"; import logger from "../utils/logger";
interface AuthenticatedUser {
id: string;
email: string;
name?: string | null;
}
export const profile = new Elysia({ export const profile = new Elysia({
prefix: "/profile", prefix: "/profile",
}).post( }).post(
"/update", "/update",
async (ctx) => { async ({
const { body, set, user } = ctx as any; body,
set,
user,
}: {
body: { name?: string; image?: string };
set: any;
user?: AuthenticatedUser;
}) => {
try { try {
if (!user) { if (!user) {
set.status = 401; set.status = 401;

76
src/api/resident.ts Normal file
View File

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

View File

@@ -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 { 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 { ActivityList } from "./dashboard/activity-list";
import { ChartAPBDes } from "./dashboard/chart-apbdes"; import { ChartAPBDes } from "./dashboard/chart-apbdes";
import { ChartSurat } from "./dashboard/chart-surat"; import { ChartSurat } from "./dashboard/chart-surat";
@@ -32,8 +34,38 @@ const sdgsData = [
]; ];
export function DashboardContent() { export function DashboardContent() {
const { colorScheme } = useMantineColorScheme(); const [stats, setStats] = useState({
const dark = colorScheme === "dark"; 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 ( return (
<Stack gap="lg"> <Stack gap="lg">
@@ -42,36 +74,36 @@ export function DashboardContent() {
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<StatCard <StatCard
title="Surat Minggu Ini" title="Surat Minggu Ini"
value={99} value={0}
detail="14 baru, 14 diproses" detail="Menunggu integrasi riil"
trend="12% dari minggu lalu ↗ +12%" trend="0%"
trendValue={12} trendValue={0}
icon={<FileText style={{ width: "70%", height: "70%" }} />} icon={<FileText style={{ width: "70%", height: "70%" }} />}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<StatCard <StatCard
title="Pengaduan Aktif" title="Pengaduan Aktif"
value={28} value={stats.complaints.baru + stats.complaints.proses}
detail="14 baru, 14 diproses" detail={`${stats.complaints.baru} baru, ${stats.complaints.proses} diproses`}
icon={<MessageCircle style={{ width: "70%", height: "70%" }} />} icon={<MessageCircle style={{ width: "70%", height: "70%" }} />}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<StatCard <StatCard
title="Layanan Selesai" title="Layanan Selesai"
value={156} value={stats.complaints.selesai}
detail="bulan ini" detail="Total diselesaikan"
trend="+8%" trend="+0%"
trendValue={8} trendValue={0}
icon={<CheckCircle style={{ width: "70%", height: "70%" }} />} icon={<CheckCircle style={{ width: "70%", height: "70%" }} />}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<StatCard <StatCard
title="Kepuasan Warga" title="Total Penduduk"
value="87.2%" value={stats.residents.total.toLocaleString()}
detail="dari 482 responden" detail={`${stats.residents.heads} Kepala Keluarga`}
icon={<Users style={{ width: "70%", height: "70%" }} />} icon={<Users style={{ width: "70%", height: "70%" }} />}
/> />
</Grid.Col> </Grid.Col>
@@ -102,8 +134,8 @@ export function DashboardContent() {
{/* Section 6: SDGs Desa Cards */} {/* Section 6: SDGs Desa Cards */}
<Grid gutter="md"> <Grid gutter="md">
{sdgsData.map((sdg, index) => ( {sdgsData.map((sdg) => (
<Grid.Col key={index} span={{ base: 9, md: 3 }}> <Grid.Col key={sdg.title} span={{ base: 9, md: 3 }}>
<SDGSCard <SDGSCard
image={<Image src={sdg.image} alt={sdg.title} />} image={<Image src={sdg.image} alt={sdg.title} />}
title={sdg.title} title={sdg.title}

View File

@@ -48,9 +48,9 @@ export function ActivityList() {
</Title> </Title>
</Group> </Group>
<Stack gap="md"> <Stack gap="md">
{events.map((event, index) => ( {events.map((event) => (
<Box <Box
key={index} key={`${event.title}-${event.date}`}
style={{ style={{
borderLeft: "4px solid var(--mantine-color-blue-filled)", borderLeft: "4px solid var(--mantine-color-blue-filled)",
paddingLeft: 12, paddingLeft: 12,

View File

@@ -45,8 +45,8 @@ export function ChartAPBDes() {
Grafik APBDes Grafik APBDes
</Title> </Title>
<Stack gap="xs"> <Stack gap="xs">
{apbdesData.map((item, index) => ( {apbdesData.map((item) => (
<Group key={index} align="center" gap="md"> <Group key={item.name} align="center" gap="md">
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}> <Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
{item.name} {item.name}
</Text> </Text>

View File

@@ -45,8 +45,8 @@ export function DivisionProgress() {
Divisi Teraktif Divisi Teraktif
</Title> </Title>
<Stack gap="sm"> <Stack gap="sm">
{divisionData.map((divisi, index) => ( {divisionData.map((divisi) => (
<Box key={index}> <Box key={divisi.name}>
<Group justify="space-between" mb={5}> <Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "white" : "gray.7"}> <Text size="sm" fw={500} c={dark ? "white" : "gray.7"}>
{divisi.name} {divisi.name}

View File

@@ -2,7 +2,6 @@ import {
Box, Box,
Card, Card,
Group, Group,
Stack,
Text, Text,
Title, Title,
useMantineColorScheme, useMantineColorScheme,
@@ -51,8 +50,8 @@ export function SatisfactionChart() {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{satisfactionData.map((entry, index) => ( {satisfactionData.map((entry) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${entry.name}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
<Tooltip <Tooltip
@@ -65,8 +64,8 @@ export function SatisfactionChart() {
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
<Group justify="center" gap="md" mt="md"> <Group justify="center" gap="md" mt="md">
{satisfactionData.map((item, index) => ( {satisfactionData.map((item) => (
<Group key={index} gap="xs"> <Group key={item.name} gap="xs">
<Box <Box
w={12} w={12}
h={12} h={12}

View File

@@ -0,0 +1,220 @@
import { useCallback, useEffect, useRef, useState } from "react";
interface CodeInfo {
relativePath: string;
line: string;
column: string;
}
/**
* Extracts data-inspector-* from fiber props or DOM attributes.
* Handles React 19 fiber tree walk-up and DOM attribute fallbacks.
*/
function getCodeInfoFromElement(element: HTMLElement): CodeInfo | null {
// Strategy 1: React internal props __reactProps$ (most accurate in R19)
for (const key of Object.keys(element)) {
if (key.startsWith("__reactProps$")) {
// biome-ignore lint/suspicious/noExplicitAny: React internals
const props = (element as any)[key];
if (props?.["data-inspector-relative-path"]) {
return {
relativePath: props["data-inspector-relative-path"],
line: props["data-inspector-line"] || "1",
column: props["data-inspector-column"] || "1",
};
}
}
// Strategy 2: Walk fiber tree __reactFiber$
if (key.startsWith("__reactFiber$")) {
// biome-ignore lint/suspicious/noExplicitAny: React internals
let f = (element as any)[key];
while (f) {
const p = f.pendingProps || f.memoizedProps;
if (p?.["data-inspector-relative-path"]) {
return {
relativePath: p["data-inspector-relative-path"],
line: p["data-inspector-line"] || "1",
column: p["data-inspector-column"] || "1",
};
}
// Fallback: _debugSource (React < 19)
const src = f._debugSource ?? f._debugOwner?._debugSource;
if (src?.fileName && src?.lineNumber) {
return {
relativePath: src.fileName,
line: String(src.lineNumber),
column: String(src.columnNumber ?? 1),
};
}
f = f.return;
}
}
}
// Strategy 3: Universal DOM attribute fallback
const rp = element.getAttribute("data-inspector-relative-path");
if (rp) {
return {
relativePath: rp,
line: element.getAttribute("data-inspector-line") || "1",
column: element.getAttribute("data-inspector-column") || "1",
};
}
return null;
}
/** Walks up DOM tree until source info is found. */
function findCodeInfo(target: HTMLElement): CodeInfo | null {
let el: HTMLElement | null = target;
while (el) {
const info = getCodeInfoFromElement(el);
if (info) return info;
el = el.parentElement;
}
return null;
}
function openInEditor(info: CodeInfo) {
fetch("/__open-in-editor", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
relativePath: info.relativePath,
lineNumber: info.line,
columnNumber: info.column,
}),
});
}
export function DevInspector({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(false);
const overlayRef = useRef<HTMLDivElement | null>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const lastInfoRef = useRef<CodeInfo | null>(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}
<div
ref={overlayRef}
style={{
display: "none",
position: "absolute",
pointerEvents: "none",
border: "2px solid #3b82f6",
backgroundColor: "rgba(59,130,246,0.1)",
zIndex: 99999,
transition: "all 0.05s ease",
}}
/>
<div
ref={tooltipRef}
style={{
display: "none",
position: "absolute",
pointerEvents: "none",
backgroundColor: "#1e293b",
color: "#e2e8f0",
fontSize: "12px",
fontFamily: "monospace",
padding: "2px 6px",
borderRadius: "3px",
zIndex: 100000,
whiteSpace: "nowrap",
}}
/>
</>
);
}

View File

@@ -6,7 +6,6 @@ import {
Divider, Divider,
Group, Group,
Text, Text,
Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { import {
@@ -21,44 +20,10 @@ interface HeaderProps {
} }
export function Header({ onSidebarToggle }: HeaderProps) { export function Header({ onSidebarToggle }: HeaderProps) {
const location = useLocation(); const _location = useLocation();
const navigate = useNavigate();
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; 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 ( return (
<Group justify="space-between" w="100%"> <Group justify="space-between" w="100%">
@@ -77,9 +42,6 @@ export function Header({ onSidebarToggle }: HeaderProps) {
style={{ width: "70%", height: "70%" }} style={{ width: "70%", height: "70%" }}
/> />
</ActionIcon> </ActionIcon>
{/* <Title order={3} c={"white"}>
{getPageTitle()}
</Title> */}
</Group> </Group>
{/* Right Section */} {/* Right Section */}

View File

@@ -152,9 +152,9 @@ const HelpPage = () => {
{/* Statistics Section */} {/* Statistics Section */}
<SimpleGrid cols={3} spacing="lg" mb="xl"> <SimpleGrid cols={3} spacing="lg" mb="xl">
{stats.map((stat, index) => ( {stats.map((stat) => (
<HelpCard <HelpCard
key={index} key={stat.label}
bg={dark ? "#1E293B" : "white"} bg={dark ? "#1E293B" : "white"}
p="lg" p="lg"
style={{ style={{
@@ -192,9 +192,9 @@ const HelpPage = () => {
h="100%" h="100%"
> >
<Box> <Box>
{guideItems.map((item, index) => ( {guideItems.map((item) => (
<Box <Box
key={index} key={item.title}
py="sm" py="sm"
style={{ style={{
borderBottom: "1px solid #eee", borderBottom: "1px solid #eee",
@@ -226,9 +226,9 @@ const HelpPage = () => {
h="100%" h="100%"
> >
<Box> <Box>
{videoItems.map((item, index) => ( {videoItems.map((item) => (
<Box <Box
key={index} key={item.title}
py="sm" py="sm"
style={{ style={{
borderBottom: "1px solid #eee", borderBottom: "1px solid #eee",
@@ -260,13 +260,13 @@ const HelpPage = () => {
h="100%" h="100%"
> >
<Accordion variant="separated"> <Accordion variant="separated">
{faqItems.map((item, index) => ( {faqItems.map((item) => (
<Accordion.Item <Accordion.Item
style={{ style={{
backgroundColor: dark ? "#263852ff" : "#F1F5F9", backgroundColor: dark ? "#263852ff" : "#F1F5F9",
}} }}
key={index} key={item.question}
value={`faq-${index}`} value={item.question}
> >
<Accordion.Control>{item.question}</Accordion.Control> <Accordion.Control>{item.question}</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
@@ -335,9 +335,9 @@ const HelpPage = () => {
h="100%" h="100%"
> >
<Box> <Box>
{documentationItems.map((item, index) => ( {documentationItems.map((item) => (
<Box <Box
key={index} key={item.title}
py="sm" py="sm"
style={{ style={{
borderBottom: "1px solid #eee", borderBottom: "1px solid #eee",
@@ -434,6 +434,7 @@ const HelpPage = () => {
disabled={isLoading} disabled={isLoading}
/> />
<button <button
type="button"
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={isLoading || inputValue.trim() === ""} disabled={isLoading || inputValue.trim() === ""}
style={{ style={{

View File

@@ -3,7 +3,6 @@ import {
Box, Box,
Card, Card,
Grid, Grid,
GridCol,
Group, Group,
Progress, Progress,
Stack, Stack,
@@ -224,9 +223,9 @@ const JennaAnalytic = () => {
Topik Pertanyaan Terbanyak Topik Pertanyaan Terbanyak
</Title> </Title>
<Stack gap="xs"> <Stack gap="xs">
{topTopics.map((item, index) => ( {topTopics.map((item) => (
<Box <Box
key={index} key={item.topic}
p="sm" p="sm"
bg={dark ? "#334155" : "#F1F5F9"} bg={dark ? "#334155" : "#F1F5F9"}
style={{ style={{
@@ -270,8 +269,8 @@ const JennaAnalytic = () => {
Jam Tersibuk Jam Tersibuk
</Title> </Title>
<Stack gap="md"> <Stack gap="md">
{busyHours.map((item, index) => ( {busyHours.map((item) => (
<Box key={index}> <Box key={item.period}>
<Group justify="space-between" mb={5}> <Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}> <Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
{item.period} {item.period}

View File

@@ -9,13 +9,13 @@ import {
Text, Text,
ThemeIcon, ThemeIcon,
Title, Title,
useMantineColorScheme useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconAlertTriangle, IconAlertTriangle,
IconCamera, IconCamera,
IconClock, IconClock,
IconMapPin IconMapPin,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
const KeamananPage = () => { const KeamananPage = () => {
@@ -120,8 +120,8 @@ const KeamananPage = () => {
<Stack gap={"xs"}> <Stack gap={"xs"}>
{/* KPI Cards */} {/* KPI Cards */}
<Grid gutter="md"> <Grid gutter="md">
{kpiData.map((kpi, index) => ( {kpiData.map((kpi) => (
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}> <GridCol key={kpi.title} span={{ base: 12, sm: 6, md: 6 }}>
<Card <Card
p="md" p="md"
radius="md" radius="md"
@@ -214,9 +214,9 @@ const KeamananPage = () => {
<Title order={4} c={dark ? "dark.0" : "black"}> <Title order={4} c={dark ? "dark.0" : "black"}>
Daftar CCTV Daftar CCTV
</Title> </Title>
{cctvLocations.map((cctv, index) => ( {cctvLocations.map((cctv) => (
<Card <Card
key={index} key={cctv.id}
p="md" p="md"
radius="md" radius="md"
withBorder withBorder
@@ -269,9 +269,9 @@ const KeamananPage = () => {
h="100%" h="100%"
> >
<Stack gap="sm"> <Stack gap="sm">
{securityReports.map((report, index) => ( {securityReports.map((report) => (
<Card <Card
key={index} key={report.id}
p="md" p="md"
radius="md" radius="md"
withBorder withBorder

View File

@@ -1,4 +1,7 @@
import { Grid, Stack } from "@mantine/core"; import { Card, Grid, Stack } from "@mantine/core";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { apiClient } from "@/utils/api-client";
import { ActivityCard } from "./kinerja-divisi/activity-card"; import { ActivityCard } from "./kinerja-divisi/activity-card";
import { ArchiveCard } from "./kinerja-divisi/archive-card"; import { ArchiveCard } from "./kinerja-divisi/archive-card";
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel"; import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
@@ -7,34 +10,6 @@ import { DocumentChart } from "./kinerja-divisi/document-chart";
import { EventCard } from "./kinerja-divisi/event-card"; import { EventCard } from "./kinerja-divisi/event-card";
import { ProgressChart } from "./kinerja-divisi/progress-chart"; import { ProgressChart } from "./kinerja-divisi/progress-chart";
// Data for program kegiatan (Section 1)
const programKegiatanData = [
{
title: "Rakor 2025",
date: "3 Juli 2025",
progress: 90,
status: "Selesai" as const,
},
{
title: "Pemutakhiran Indeks Desa",
date: "3 Juli 2025",
progress: 85,
status: "Selesai" as const,
},
{
title: "Mengurus Akta Cerai Warga",
date: "3 Juli 2025",
progress: 80,
status: "Selesai" as const,
},
{
title: "Pasek 7 Desa Adat",
date: "3 Juli 2025",
progress: 92,
status: "Selesai" as const,
},
];
// Data for arsip digital (Section 5) // Data for arsip digital (Section 5)
const archiveData = [ const archiveData = [
{ name: "Surat Keputusan" }, { name: "Surat Keputusan" },
@@ -44,20 +19,70 @@ const archiveData = [
]; ];
const KinerjaDivisi = () => { const KinerjaDivisi = () => {
const [activities, setActivities] = useState<any[]>([]);
const [todayEvents, setTodayEvents] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const [activityRes, eventRes] = await Promise.all([
apiClient.GET("/api/division/activities"),
apiClient.GET("/api/event/today"),
]);
if (activityRes.data?.data) {
setActivities(activityRes.data.data);
}
if (eventRes.data?.data) {
setTodayEvents(eventRes.data.data);
}
} catch (error) {
console.error("Failed to fetch kinerja divisi data", error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
// Format events for EventCard
const formattedEvents = todayEvents.map((event) => ({
time: dayjs(event.startDate).format("HH:mm"),
event: event.title,
}));
return ( return (
<Stack gap="lg"> <Stack gap="lg">
{/* SECTION 1 — PROGRAM KEGIATAN */} {/* SECTION 1 — PROGRAM KEGIATAN */}
<Grid gutter="md"> <Grid gutter="md">
{programKegiatanData.map((kegiatan, index) => ( {activities.slice(0, 4).map((kegiatan, index) => (
<Grid.Col key={index} span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col
key={kegiatan.id || index}
span={{ base: 12, md: 6, lg: 3 }}
>
<ActivityCard <ActivityCard
title={kegiatan.title} title={kegiatan.title}
date={kegiatan.date} date={dayjs(kegiatan.createdAt).format("D MMMM YYYY")}
progress={kegiatan.progress} progress={kegiatan.progress}
status={kegiatan.status} status={
kegiatan.status === "SELESAI"
? "Selesai"
: kegiatan.status === "BERJALAN"
? "Berjalan"
: "Tertunda"
}
/> />
</Grid.Col> </Grid.Col>
))} ))}
{!loading && activities.length === 0 && (
<Grid.Col span={12}>
<Card p="md" radius="xl" withBorder ta="center" c="dimmed">
Tidak ada aktivitas terbaru
</Card>
</Grid.Col>
)}
</Grid> </Grid>
{/* SECTION 2 — GRID DASHBOARD (3 Columns) */} {/* SECTION 2 — GRID DASHBOARD (3 Columns) */}
@@ -82,7 +107,7 @@ const KinerjaDivisi = () => {
<DiscussionPanel /> <DiscussionPanel />
{/* SECTION 4 — ACARA HARI INI */} {/* SECTION 4 — ACARA HARI INI */}
<EventCard /> <EventCard agendas={formattedEvents} />
{/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */} {/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */}
<Grid gutter="md"> <Grid gutter="md">

View File

@@ -1,4 +1,11 @@
import { Box, Card, Group, Progress, Text, useMantineColorScheme } from "@mantine/core"; import {
Box,
Card,
Group,
Progress,
Text,
useMantineColorScheme,
} from "@mantine/core";
interface ActivityCardProps { interface ActivityCardProps {
title: string; title: string;

View File

@@ -1,34 +1,48 @@
import { import {
Box,
Card, Card,
Group, Group,
Loader,
Stack, Stack,
Text, Text,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import { useEffect, useState } from "react";
import { apiClient } from "@/utils/api-client";
interface DivisionItem { interface DivisionItem {
name: string; name: string;
count: number; count: number;
} }
const divisionData: DivisionItem[] = [
{ name: "Kesejahteraan", count: 37 },
{ name: "Pemerintahan", count: 26 },
{ name: "Keuangan", count: 17 },
{ name: "Sekretaris Desa", count: 15 },
{ name: "Tata Usaha TK", count: 14 },
{ name: "Perangkat Kewilayahan", count: 12 },
{ name: "Pelayanan", count: 10 },
{ name: "Perencanaan", count: 9 },
{ name: "Tata Usaha & Umum", count: 7 },
];
export function DivisionList() { export function DivisionList() {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
const [divisions, setDivisions] = useState<DivisionItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchDivisions() {
try {
const { data } = await apiClient.GET("/api/division/");
if (data?.data) {
const mapped = data.data.map((div: { name: string; _count?: { activities: number } }) => ({
name: div.name,
count: div._count?.activities || 0,
}));
setDivisions(mapped);
}
} catch (error) {
console.error("Failed to fetch divisions", error);
} finally {
setLoading(false);
}
}
fetchDivisions();
}, []);
return ( return (
<Card <Card
p="md" p="md"
@@ -47,30 +61,40 @@ export function DivisionList() {
Divisi Teraktif Divisi Teraktif
</Text> </Text>
<Stack gap="xs"> <Stack gap="xs">
{divisionData.map((division, index) => ( {loading ? (
<Group <Group justify="center" py="xl">
key={index} <Loader size="sm" />
justify="space-between"
align="center"
style={{
padding: "8px 12px",
borderRadius: 8,
backgroundColor: dark ? "#334155" : "#F1F5F9",
transition: "background-color 0.2s",
cursor: "pointer",
}}
>
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
{division.name}
</Text>
<Group gap="xs">
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
{division.count}
</Text>
<ChevronRight size={16} color={dark ? "#94A3B8" : "#64748B"} />
</Group>
</Group> </Group>
))} ) : divisions.length > 0 ? (
divisions.map((division, index) => (
<Group
key={index}
justify="space-between"
align="center"
style={{
padding: "8px 12px",
borderRadius: 8,
backgroundColor: dark ? "#334155" : "#F1F5F9",
transition: "background-color 0.2s",
cursor: "pointer",
}}
>
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
{division.name}
</Text>
<Group gap="xs">
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
{division.count}
</Text>
<ChevronRight size={16} color={dark ? "#94A3B8" : "#64748B"} />
</Group>
</Group>
))
) : (
<Text size="xs" c="dimmed" ta="center">
Tidak ada data divisi
</Text>
)}
</Stack> </Stack>
</Card> </Card>
); );

View File

@@ -4,13 +4,17 @@ import {
Card, Card,
Grid, Grid,
Group, Group,
Loader,
Stack, Stack,
Text, Text,
ThemeIcon, ThemeIcon,
Title, Title,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react"; import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { import {
Bar, Bar,
BarChart, BarChart,
@@ -22,40 +26,11 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from "recharts"; } from "recharts";
import { apiClient } from "@/utils/api-client";
// Summary data dayjs.extend(relativeTime);
const summaryData = [
{
title: "Total Pengaduan",
value: 42,
subtitle: "Bulan ini",
icon: MessageCircle,
color: "#1E3A5F",
},
{
title: "Baru",
value: 14,
subtitle: "Belum diproses",
icon: FileText,
color: "#1E3A5F",
},
{
title: "Diproses",
value: 14,
subtitle: "Sedang ditangani",
icon: Clock,
color: "#1E3A5F",
},
{
title: "Selesai",
value: 14,
subtitle: "Terselesaikan",
icon: CheckCircle,
color: "#1E3A5F",
},
];
// Tren pengaduan data // Tren pengaduan data (Mock for now)
const trenData = [ const trenData = [
{ bulan: "Apr", jumlah: 35 }, { bulan: "Apr", jumlah: 35 },
{ bulan: "Mei", jumlah: 48 }, { bulan: "Mei", jumlah: 48 },
@@ -66,50 +41,7 @@ const trenData = [
{ bulan: "Okt", jumlah: 52 }, { bulan: "Okt", jumlah: 52 },
]; ];
// Surat terbanyak data // Ide inovatif data (Mock for now)
const suratData = [
{ jenis: "KTP", jumlah: 24 },
{ jenis: "KK", jumlah: 18 },
{ jenis: "Domisili", jumlah: 15 },
{ jenis: "Usaha", jumlah: 12 },
{ jenis: "Lainnya", jumlah: 8 },
];
// Pengajuan terbaru data
const pengajuanTerbaru = [
{
nama: "Budi Santoso",
jenis: "Ketertiban Umum",
waktu: "2 jam yang lalu",
status: "baru",
},
{
nama: "Siti Rahayu",
jenis: "Pelayanan Kesehatan",
waktu: "5 jam yang lalu",
status: "proses",
},
{
nama: "Ahmad Fauzi",
jenis: "Infrastruktur",
waktu: "1 hari yang lalu",
status: "selesai",
},
{
nama: "Dewi Lestari",
jenis: "Administrasi",
waktu: "1 hari yang lalu",
status: "baru",
},
{
nama: "Joko Widodo",
jenis: "Keamanan",
waktu: "2 hari yang lalu",
status: "proses",
},
];
// Ide inovatif data
const ideInovatif = [ const ideInovatif = [
{ {
nama: "Andi Prasetyo", nama: "Andi Prasetyo",
@@ -123,24 +55,13 @@ const ideInovatif = [
waktu: "5 hari yang lalu", waktu: "5 hari yang lalu",
kategori: "Ekonomi", kategori: "Ekonomi",
}, },
{
nama: "Bambang Suryono",
judul: "Peningkatan Sanitasi",
waktu: "1 minggu yang lalu",
kategori: "Kesehatan",
},
{
nama: "Lina Marlina",
judul: "Pusat Kreatif Anak Muda",
waktu: "2 minggu yang lalu",
kategori: "Pendidikan",
},
]; ];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status.toLowerCase()) {
case "baru": case "baru":
return "red"; return "red";
case "diproses":
case "proses": case "proses":
return "blue"; return "blue";
case "selesai": case "selesai":
@@ -154,6 +75,75 @@ const PengaduanLayananPublik = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
const [stats, setStats] = useState({
total: 0,
baru: 0,
proses: 0,
selesai: 0,
});
const [recentComplaints, setRecentComplaints] = useState<any[]>([]);
const [serviceStats, setServiceStats] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const [statsRes, recentRes, serviceRes] = await Promise.all([
apiClient.GET("/api/complaint/stats"),
apiClient.GET("/api/complaint/recent"),
apiClient.GET("/api/complaint/service-stats"),
]);
if (statsRes.data?.data) setStats(statsRes.data.data);
if (recentRes.data?.data) setRecentComplaints(recentRes.data.data);
if (serviceRes.data?.data) {
const mappedService = serviceRes.data.data.map((item: any) => ({
jenis: item.letterType,
jumlah: item._count?._all || 0,
}));
setServiceStats(mappedService);
}
} catch (error) {
console.error("Failed to fetch complaint data", error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
const summaryData = [
{
title: "Total Pengaduan",
value: stats.total,
subtitle: "Bulan ini",
icon: MessageCircle,
color: "#1E3A5F",
},
{
title: "Baru",
value: stats.baru,
subtitle: "Belum diproses",
icon: FileText,
color: "#1E3A5F",
},
{
title: "Diproses",
value: stats.proses,
subtitle: "Sedang ditangani",
icon: Clock,
color: "#1E3A5F",
},
{
title: "Selesai",
value: stats.selesai,
subtitle: "Terselesaikan",
icon: CheckCircle,
color: "#1E3A5F",
},
];
return ( return (
<Stack gap="lg"> <Stack gap="lg">
{/* TOP SECTION - 4 STAT CARDS */} {/* TOP SECTION - 4 STAT CARDS */}
@@ -178,7 +168,7 @@ const PengaduanLayananPublik = () => {
{item.title} {item.title}
</Text> </Text>
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}> <Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
{item.value} {loading ? <Loader size="xs" /> : item.value}
</Text> </Text>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{item.subtitle} {item.subtitle}
@@ -189,9 +179,6 @@ const PengaduanLayananPublik = () => {
variant="filled" variant="filled"
size="lg" size="lg"
radius="xl" radius="xl"
style={{
transition: "transform 0.15s ease",
}}
> >
<item.icon style={{ width: "60%", height: "60%" }} /> <item.icon style={{ width: "60%", height: "60%" }} />
</ThemeIcon> </ThemeIcon>
@@ -278,35 +265,45 @@ const PengaduanLayananPublik = () => {
Surat Terbanyak Surat Terbanyak
</Title> </Title>
<ResponsiveContainer width="100%" height={250}> <ResponsiveContainer width="100%" height={250}>
<BarChart data={suratData} layout="vertical"> {loading ? (
<CartesianGrid <Group justify="center" align="center" h="100%">
strokeDasharray="3 3" <Loader />
horizontal={false} </Group>
stroke={dark ? "#334155" : "#e5e7eb"} ) : (
/> <BarChart data={serviceStats} layout="vertical">
<XAxis <CartesianGrid
type="number" strokeDasharray="3 3"
axisLine={false} horizontal={false}
tickLine={false} stroke={dark ? "#334155" : "#e5e7eb"}
tick={{ fill: dark ? "#E2E8F0" : "#374151" }} />
/> <XAxis
<YAxis type="number"
type="category" axisLine={false}
dataKey="jenis" tickLine={false}
axisLine={false} tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
tickLine={false} />
tick={{ fill: dark ? "#E2E8F0" : "#374151" }} <YAxis
width={80} type="category"
/> dataKey="jenis"
<Tooltip axisLine={false}
contentStyle={{ tickLine={false}
backgroundColor: dark ? "#1E293B" : "white", tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
borderColor: dark ? "#334155" : "#e5e7eb", width={80}
borderRadius: "8px", />
}} <Tooltip
/> contentStyle={{
<Bar dataKey="jumlah" fill="#396aaaff" radius={[0, 4, 4, 0]} /> backgroundColor: dark ? "#1E293B" : "white",
</BarChart> borderColor: dark ? "#334155" : "#e5e7eb",
borderRadius: "8px",
}}
/>
<Bar
dataKey="jumlah"
fill="#396aaaff"
radius={[0, 4, 4, 0]}
/>
</BarChart>
)}
</ResponsiveContainer> </ResponsiveContainer>
</Card> </Card>
</Grid.Col> </Grid.Col>
@@ -328,42 +325,52 @@ const PengaduanLayananPublik = () => {
Pengajuan Terbaru Pengajuan Terbaru
</Title> </Title>
<Stack gap="sm"> <Stack gap="sm">
{pengajuanTerbaru.map((item, index) => ( {loading ? (
<Card <Group justify="center" py="xl">
key={index} <Loader />
p="sm" </Group>
radius="md" ) : recentComplaints.length > 0 ? (
withBorder recentComplaints.map((item, index) => (
bg={dark ? "#334155" : "#F1F5F9"} <Card
style={{ key={item.id || index}
borderColor: "transparent", p="sm"
transition: "background-color 0.15s ease", radius="md"
}} withBorder
> bg={dark ? "#334155" : "#F1F5F9"}
<Group justify="space-between"> style={{
<Stack gap={0}> borderColor: "transparent",
<Text fw={600} c={dark ? "white" : "gray.9"}> transition: "background-color 0.15s ease",
{item.nama} }}
</Text> >
<Text size="sm" c="dimmed"> <Group justify="space-between">
{item.jenis} <Stack gap={0}>
</Text> <Text fw={600} c={dark ? "white" : "gray.9"}>
</Stack> {item.title}
<Stack gap={0} align="flex-end"> </Text>
<Badge <Text size="sm" c="dimmed">
color={getStatusColor(item.status)} {item.category}
variant="light" </Text>
radius="sm" </Stack>
> <Stack gap={0} align="flex-end">
{item.status} <Badge
</Badge> color={getStatusColor(item.status)}
<Text size="xs" c="dimmed"> variant="light"
{item.waktu} radius="sm"
</Text> >
</Stack> {item.status}
</Group> </Badge>
</Card> <Text size="xs" c="dimmed">
))} {dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Group>
</Card>
))
) : (
<Text c="dimmed" ta="center">
Tidak ada pengajuan terbaru
</Text>
)}
</Stack> </Stack>
</Card> </Card>
</Grid.Col> </Grid.Col>

View File

@@ -10,12 +10,11 @@
import { createTheme, MantineProvider } from "@mantine/core"; import { createTheme, MantineProvider } from "@mantine/core";
import { ModalsProvider } from "@mantine/modals"; import { ModalsProvider } from "@mantine/modals";
import { createRouter, RouterProvider } from "@tanstack/react-router"; import { createRouter, RouterProvider } from "@tanstack/react-router";
import { Inspector } from "react-dev-inspector";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import "./index.css"; import "./index.css";
import "@mantine/charts/styles.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 // Create a new router instance
export const router = createRouter({ export const router = createRouter({
@@ -101,29 +100,14 @@ const theme = createTheme({
primaryColor: "darmasaba-blue", primaryColor: "darmasaba-blue",
}); });
// Use dynamic import for DevInspector to avoid including it in production bundle
const InspectorWrapper = IS_DEV const InspectorWrapper = IS_DEV
? Inspector ? (await import("./components/dev-inspector")).DevInspector
: ({ children }: { children: React.ReactNode }) => <>{children}</>; : ({ children }: { children: React.ReactNode }) => <>{children}</>;
const elem = document.getElementById("root")!; const elem = document.getElementById("root")!;
const app = ( const app = (
<InspectorWrapper <InspectorWrapper>
keys={["shift", "a"]}
onClickElement={(e) => {
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,
}),
});
}}
>
<MantineProvider theme={theme} defaultColorScheme="auto"> <MantineProvider theme={theme} defaultColorScheme="auto">
<ModalsProvider> <ModalsProvider>
<RouterProvider router={router} /> <RouterProvider router={router} />

View File

@@ -6,7 +6,7 @@ import { Elysia } from "elysia";
import api from "./api"; import api from "./api";
import { openInEditor } from "./utils/open-in-editor"; 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"; const isProduction = process.env.NODE_ENV === "production";
@@ -35,14 +35,16 @@ if (!isProduction) {
app.post("/__open-in-editor", ({ body }) => { app.post("/__open-in-editor", ({ body }) => {
const { relativePath, lineNumber, columnNumber } = body as { const { relativePath, lineNumber, columnNumber } = body as {
relativePath: string; relativePath: string;
lineNumber: number; lineNumber: string;
columnNumber: number; columnNumber: string;
}; };
const editor = (process.env.REACT_EDITOR || "code") as any;
openInEditor(relativePath, { openInEditor(relativePath, {
line: lineNumber, line: Number(lineNumber),
column: columnNumber, column: Number(columnNumber),
editor: "antigravity", editor: editor,
}); });
return { ok: true }; return { ok: true };

View File

@@ -11,7 +11,7 @@ export const Route = createRootRoute({
// Apply protected route middleware for all routes // Apply protected route middleware for all routes
// The middleware will determine which routes are public vs protected // The middleware will determine which routes are public vs protected
const context = await protectedRouteMiddleware({ location }); const context = await protectedRouteMiddleware({ location });
// Only set auth store if we have user data (for protected routes) // Only set auth store if we have user data (for protected routes)
if (context?.user) { if (context?.user) {
authStore.user = context?.user as any; authStore.user = context?.user as any;

View File

@@ -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: <Component, <div, or <item.icon
// Allow dots and hyphens in the tag name
const jsxPattern = /(<(?:[A-Za-z][a-zA-Z0-9.-]*))\b/g;
let match: RegExpExecArray | null = null;
// biome-ignore lint/suspicious/noAssignInExpressions: match loop
while ((match = jsxPattern.exec(line)) !== null) {
// Skip if character before `<` is an identifier char (likely a TypeScript generic)
const charBefore = match.index > 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");
},
};
}

View File

@@ -1,9 +1,9 @@
import path from "node:path"; import path from "node:path";
import { inspectorServer } from "@react-dev-inspector/vite-plugin";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-vite-plugin"; import { tanstackRouter } from "@tanstack/router-vite-plugin";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import { createServer as createViteServer } from "vite"; import { createServer as createViteServer } from "vite";
import { inspectorPlugin } from "./utils/dev-inspector-plugin";
export async function createVite() { export async function createVite() {
return createViteServer({ return createViteServer({
@@ -14,23 +14,7 @@ export async function createVite() {
"@": path.resolve(process.cwd(), "./src"), "@": path.resolve(process.cwd(), "./src"),
}, },
}, },
plugins: [ plugins: [tailwindcss(), inspectorPlugin(), react(), tanstackRouter()],
tailwindcss(),
react({
babel: {
plugins: [
[
"@react-dev-inspector/babel-plugin",
{
relativePath: true,
},
],
],
},
}),
inspectorServer(),
tanstackRouter(),
],
server: { server: {
middlewareMode: true, middlewareMode: true,
hmr: { hmr: {