fix(header): fix missing Divider, Badge, IconUserShield and navigate
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
**ID:** `TASK-DB-001`
|
||||
**Konteks:** Database Implementation
|
||||
**Status:** 🏗️ IN PROGRESS
|
||||
**Status:** ✅ COMPLETED (95% Selesai)
|
||||
**Prioritas:** 🔴 KRITIS (Blokade Fitur)
|
||||
**Estimasi:** 7 Hari Kerja
|
||||
|
||||
@@ -16,41 +16,41 @@ Mengganti mock data pada fitur-fitur inti (Kinerja Divisi, Pengaduan, Kependuduk
|
||||
## 📋 DAFTAR TUGAS (TODO)
|
||||
|
||||
### 1. Database Migration (Prisma)
|
||||
- [ ] Implementasikan model `Division`, `Activity`, `Document`, `Discussion`, dan `DivisionMetric` di `schema.prisma`.
|
||||
- [ ] Implementasikan model `Complaint`, `ComplaintUpdate`, `ServiceLetter`, dan `InnovationIdea` di `schema.prisma`.
|
||||
- [ ] Implementasikan model `Resident` dan `Banjar` di `schema.prisma`.
|
||||
- [ ] Implementasikan model `Event` di `schema.prisma`.
|
||||
- [ ] Jalankan `bun x prisma migrate dev --name init_core_features`.
|
||||
- [ ] Lakukan verifikasi relasi database di database viewer (Prisma Studio).
|
||||
- [x] Implementasikan model `Division`, `Activity`, `Document`, `Discussion`, dan `DivisionMetric` di `schema.prisma`.
|
||||
- [x] Implementasikan model `Complaint`, `ComplaintUpdate`, `ServiceLetter`, dan `InnovationIdea` di `schema.prisma`.
|
||||
- [x] Implementasikan model `Resident` dan `Banjar` di `schema.prisma`.
|
||||
- [x] Implementasikan model `Event` di `schema.prisma`.
|
||||
- [x] Jalankan `bun x prisma migrate dev --name init_core_features`.
|
||||
- [x] Lakukan verifikasi relasi database di database viewer (Prisma Studio).
|
||||
|
||||
### 2. Seeding Data
|
||||
- [ ] Update `prisma/seed.ts` untuk menyertakan data dummy yang realistis untuk:
|
||||
- [x] Update `prisma/seed.ts` untuk menyertakan data dummy yang realistis untuk:
|
||||
- 6 Banjar (Darmasaba, Manesa, dll)
|
||||
- 4 Divisi utama
|
||||
- Contoh Pengaduan & Layanan Surat
|
||||
- Contoh Event & Aktivitas
|
||||
- [ ] Jalankan `bun run seed` dan pastikan tidak ada error relasi.
|
||||
- [x] Jalankan `bun run seed` dan pastikan tidak ada error relasi.
|
||||
|
||||
### 3. Backend API Development (ElysiaJS)
|
||||
- [ ] Buat route handler di `src/api/` untuk setiap modul:
|
||||
- [x] Buat route handler di `src/api/` untuk setiap modul:
|
||||
- `division.ts`: CRUD Divisi & Aktivitas
|
||||
- `complaint.ts`: CRUD Pengaduan & Update Status
|
||||
- `resident.ts`: Endpoint untuk statistik demografi & list penduduk per banjar
|
||||
- `event.ts`: CRUD Agenda & Kalender
|
||||
- [ ] Integrasikan `apiMiddleware` untuk proteksi rute (Admin/Moderator).
|
||||
- [ ] Pastikan skema input/output didefinisikan menggunakan `t.Object` untuk OpenAPI documentation.
|
||||
- [x] Integrasikan `apiMiddleware` untuk proteksi rute (Admin/Moderator).
|
||||
- [x] Pastikan skema input/output didefinisikan menggunakan `t.Object` untuk OpenAPI documentation.
|
||||
|
||||
### 4. Contract-First Sync
|
||||
- [ ] Jalankan `bun run gen:api` untuk memperbarui `generated/api.ts`.
|
||||
- [ ] Verifikasi bahwa tipe-tipe baru muncul di frontend dan siap digunakan oleh `apiClient`.
|
||||
- [x] Jalankan `bun run gen:api` untuk memperbarui `generated/api.ts`.
|
||||
- [x] Verifikasi bahwa tipe-tipe baru muncul di frontend dan siap digunakan oleh `apiClient`.
|
||||
|
||||
### 5. Frontend Integration (Surgical Update)
|
||||
- [ ] Update `src/hooks/` atau `src/store/` untuk memanggil API riil menggantikan mock data.
|
||||
- [ ] Sambungkan komponen berikut ke API:
|
||||
- `DashboardContent`: Stat cards & Activity List
|
||||
- `KinerjaDivisi`: Division List & Activity Cards
|
||||
- `PengaduanLayananPublik`: Statistik & Tabel Pengajuan
|
||||
- `DemografiPekerjaan`: Grafik & Data per Banjar
|
||||
- [x] Update `src/hooks/` atau `src/store/` untuk memanggil API riil menggantikan mock data.
|
||||
- [x] Sambungkan komponen berikut ke API:
|
||||
- `DashboardContent`: Stat cards (Selesai)
|
||||
- `KinerjaDivisi`: Division List & Activity Cards (Selesai)
|
||||
- `PengaduanLayananPublik`: Statistik & Tabel Pengajuan (Selesai)
|
||||
- `DemografiPekerjaan`: Grafik & Data per Banjar (Pending - Next Step)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**ID:** `TASK-DX-001`
|
||||
**Konteks:** Developer Experience (DX)
|
||||
**Status:** 🏗️ PROPOSED
|
||||
**Status:** ✅ COMPLETED
|
||||
**Prioritas:** 🟡 TINGGI (Peningkatan Produktivitas)
|
||||
**Estimasi:** 1 Hari Kerja
|
||||
|
||||
@@ -16,35 +16,35 @@ Mengaktifkan fitur **Click-to-Source** di lingkungan pengembangan: klik elemen U
|
||||
## 📋 DAFTAR TUGAS (TODO)
|
||||
|
||||
### 1. Vite Plugin Configuration
|
||||
- [ ] Buat file `src/utils/dev-inspector-plugin.ts` yang berisi `inspectorPlugin()` (regex-based JSX attribute injection).
|
||||
- [ ] Modifikasi `src/vite.ts`:
|
||||
- Impor `inspectorPlugin`.
|
||||
- Tambahkan `inspectorPlugin()` ke array `plugins` **sebelum** `react()`.
|
||||
- Gunakan `enforce: 'pre'` pada plugin tersebut.
|
||||
- [x] Buat file `src/utils/dev-inspector-plugin.ts` yang berisi `inspectorPlugin()` (regex-based JSX attribute injection).
|
||||
- [x] Modifikasi `src/vite.ts`:
|
||||
- [x] Impor `inspectorPlugin`.
|
||||
- [x] Tambahkan `inspectorPlugin()` ke array `plugins` **sebelum** `react()`.
|
||||
- [x] Gunakan `enforce: 'pre'` pada plugin tersebut.
|
||||
|
||||
### 2. Frontend Component Development
|
||||
- [ ] Buat komponen `src/components/dev-inspector.tsx`:
|
||||
- Implementasikan hotkey listener.
|
||||
- Tambahkan overlay UI (border biru & tooltip nama file) saat hover.
|
||||
- Implementasikan `getCodeInfoFromElement` dengan fallback (fiber props -> DOM attributes).
|
||||
- Tambahkan fungsi `openInEditor` (POST ke `/__open-in-editor`).
|
||||
- [x] Buat komponen `src/components/dev-inspector.tsx`:
|
||||
- [x] Implementasikan hotkey listener.
|
||||
- [x] Tambahkan overlay UI (border biru & tooltip nama file) saat hover.
|
||||
- [x] Implementasikan `getCodeInfoFromElement` dengan fallback (fiber props -> DOM attributes).
|
||||
- [x] Tambahkan fungsi `openInEditor` (POST ke `/__open-in-editor`).
|
||||
|
||||
### 3. Backend Integration (Elysia)
|
||||
- [ ] Modifikasi `src/index.ts`:
|
||||
- Tambahkan handler `onRequest` sebelum middleware lainnya.
|
||||
- Intercept request ke path `/__open-in-editor` (POST).
|
||||
- Gunakan `Bun.spawn()` untuk memanggil editor (berdasarkan `.env` `REACT_EDITOR`).
|
||||
- Gunakan `Bun.which()` untuk verifikasi keberadaan editor di system PATH.
|
||||
- [x] Modifikasi `src/index.ts`:
|
||||
- [x] Tambahkan handler `onRequest` sebelum middleware lainnya.
|
||||
- [x] Intercept request ke path `/__open-in-editor` (POST).
|
||||
- [x] Gunakan `Bun.spawn()` untuk memanggil editor (berdasarkan `.env` `REACT_EDITOR`).
|
||||
- [x] Gunakan `Bun.which()` untuk verifikasi keberadaan editor di system PATH.
|
||||
|
||||
### 4. Application Root Integration
|
||||
- [ ] Modifikasi `src/frontend.tsx`:
|
||||
- Implementasikan **Conditional Dynamic Import** untuk `DevInspector`.
|
||||
- Gunakan `import.meta.env?.DEV` agar tidak ada overhead di production.
|
||||
- Bungkus `<App />` (atau router) dengan `<DevInspectorWrapper>`.
|
||||
- [x] Modifikasi `src/frontend.tsx`:
|
||||
- [x] Implementasikan **Conditional Dynamic Import** untuk `DevInspector`.
|
||||
- [x] Gunakan `import.meta.env?.DEV` agar tidak ada overhead di production.
|
||||
- [x] Bungkus `<App />` (atau router) dengan `<DevInspectorWrapper>`.
|
||||
|
||||
### 5. Environment Setup
|
||||
- [ ] Tambahkan `REACT_EDITOR=code` (atau `cursor`, `windsurf`) di file `.env`.
|
||||
- [ ] Pastikan alias `@/` berfungsi dengan benar di plugin Vite untuk resolusi path.
|
||||
- [x] Tambahkan `REACT_EDITOR=code` (atau `cursor`, `windsurf`) di file `.env`.
|
||||
- [x] Pastikan alias `@/` berfungsi dengan benar di plugin Vite untuk resolusi path.
|
||||
|
||||
---
|
||||
|
||||
|
||||
31
bun.lock
31
bun.lock
@@ -92,7 +92,6 @@
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prisma": "^6.19.2",
|
||||
"react-dev-inspector": "^2.0.1",
|
||||
"vite": "^7.3.1",
|
||||
},
|
||||
},
|
||||
@@ -474,14 +473,8 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@react-dev-inspector/babel-plugin": ["@react-dev-inspector/babel-plugin@2.0.1", "", { "dependencies": { "@babel/core": "^7.20.5", "@babel/generator": "^7.20.5", "@babel/parser": "^7.20.5", "@babel/traverse": "^7.20.5", "@babel/types": "7.20.5" } }, "sha512-V2MzN9dj3uZu6NvAjSxXwa3+FOciVIuwAUwPLpO6ji5xpUyx8E6UiEng1QqzttdpacKHFKtkNYjtQAE+Lsqa5A=="],
|
||||
|
||||
"@react-dev-inspector/middleware": ["@react-dev-inspector/middleware@2.0.1", "", { "dependencies": { "react-dev-utils": "12.0.1" } }, "sha512-qDMtBzAxNNAX01jjU1THZVuNiVB7J1Hjk42k8iLSSwfinc3hk667iqgdzeq1Za1a0V2bF5Ev6D4+nkZ+E1YUrQ=="],
|
||||
|
||||
"@react-dev-inspector/umi3-plugin": ["@react-dev-inspector/umi3-plugin@2.0.1", "", { "dependencies": { "@react-dev-inspector/babel-plugin": "2.0.1", "@react-dev-inspector/middleware": "2.0.1" } }, "sha512-lRw65yKQdI/1BwrRXWJEHDJel4DWboOartGmR3S5xiTF+EiOLjmndxdA5LoVSdqbcggdtq5SWcsoZqI0TkhH7Q=="],
|
||||
|
||||
"@react-dev-inspector/umi4-plugin": ["@react-dev-inspector/umi4-plugin@2.0.1", "", { "dependencies": { "@react-dev-inspector/babel-plugin": "2.0.1", "@react-dev-inspector/middleware": "2.0.1" } }, "sha512-vTefsJVAZsgpuO9IZ1ZFIoyryVUU+hjV8OPD8DfDU+po5LjVXc5Uncn+MkFOsT24AMpNdDvCnTRYiuSkFn8EsA=="],
|
||||
|
||||
"@react-dev-inspector/vite-plugin": ["@react-dev-inspector/vite-plugin@2.0.1", "", { "dependencies": { "@react-dev-inspector/middleware": "2.0.1" } }, "sha512-J1eI7cIm2IXE6EwhHR1OyoefvobUJEn/vJWEBwOM5uW4JkkLwuVoV9vk++XJyAmKUNQ87gdWZvSWrI2LjfrSug=="],
|
||||
|
||||
"@redocly/ajv": ["@redocly/ajv@8.17.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-NQsbJbB/GV7JVO88ebFkMndrnuGp/dTm5/2NISeg+JGcLzTfGBJZ01+V5zD8nKBOpi/dLLNFT+Ql6IcUk8ehng=="],
|
||||
@@ -674,8 +667,6 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/react-reconciler": ["@types/react-reconciler@0.33.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
|
||||
@@ -1084,8 +1075,6 @@
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"hotkeys-js": ["hotkeys-js@3.13.15", "", {}, "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
@@ -1116,7 +1105,7 @@
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
@@ -1132,7 +1121,7 @@
|
||||
|
||||
"is-root": ["is-root@2.1.0", "", {}, "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="],
|
||||
|
||||
@@ -1396,8 +1385,6 @@
|
||||
|
||||
"react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="],
|
||||
|
||||
"react-dev-inspector": ["react-dev-inspector@2.0.1", "", { "dependencies": { "@react-dev-inspector/babel-plugin": "2.0.1", "@react-dev-inspector/middleware": "2.0.1", "@react-dev-inspector/umi3-plugin": "2.0.1", "@react-dev-inspector/umi4-plugin": "2.0.1", "@react-dev-inspector/vite-plugin": "2.0.1", "@types/react-reconciler": ">=0.26.6", "hotkeys-js": "^3.8.1", "picocolors": "1.0.0", "react-dev-utils": "12.0.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-b8PAmbwGFrWcxeaX8wYveqO+VTwTXGJaz/yl9RO31LK1zeLKJVlkkbeLExLnJ6IvhXY1TwL8Q4+gR2GKJ8BI6Q=="],
|
||||
|
||||
"react-dev-utils": ["react-dev-utils@12.0.1", "", { "dependencies": { "@babel/code-frame": "^7.16.0", "address": "^1.1.2", "browserslist": "^4.18.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "detect-port-alt": "^1.1.6", "escape-string-regexp": "^4.0.0", "filesize": "^8.0.6", "find-up": "^5.0.0", "fork-ts-checker-webpack-plugin": "^6.5.0", "global-modules": "^2.0.0", "globby": "^11.0.4", "gzip-size": "^6.0.0", "immer": "^9.0.7", "is-root": "^2.1.0", "loader-utils": "^3.2.0", "open": "^8.4.0", "pkg-up": "^3.1.0", "prompts": "^2.4.2", "react-error-overlay": "^6.0.11", "recursive-readdir": "^2.2.2", "shell-quote": "^1.7.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" } }, "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
@@ -1570,8 +1557,6 @@
|
||||
|
||||
"tldts-core": ["tldts-core@7.0.22", "", {}, "sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw=="],
|
||||
|
||||
"to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
@@ -1730,8 +1715,6 @@
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@react-dev-inspector/babel-plugin/@babel/types": ["@babel/types@7.20.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", "to-fast-properties": "^2.0.0" } }, "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg=="],
|
||||
|
||||
"@redocly/openapi-core/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
|
||||
|
||||
"@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
||||
@@ -1794,8 +1777,6 @@
|
||||
|
||||
"global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
|
||||
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@@ -1816,8 +1797,6 @@
|
||||
|
||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"react-dev-inspector/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="],
|
||||
|
||||
"react-dev-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"react-dev-utils/immer": ["immer@9.0.21", "", {}, "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="],
|
||||
@@ -1838,8 +1817,6 @@
|
||||
|
||||
"webpack/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||
|
||||
"wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"@prisma/config/c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"@prisma/config/c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
@@ -1874,6 +1851,10 @@
|
||||
|
||||
"react-dev-utils/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"react-dev-utils/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"react-dev-utils/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"recursive-readdir/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@prisma/config/c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
374
generated/api.ts
374
generated/api.ts
@@ -120,6 +120,193 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/division/": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get all divisions */
|
||||
get: operations["getApiDivision"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/division/activities": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get recent activities */
|
||||
get: operations["getApiDivisionActivities"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/division/metrics": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get division performance metrics */
|
||||
get: operations["getApiDivisionMetrics"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/complaint/stats": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get complaint statistics */
|
||||
get: operations["getApiComplaintStats"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/complaint/recent": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get recent complaints */
|
||||
get: operations["getApiComplaintRecent"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/complaint/service-stats": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get service letter statistics by type */
|
||||
get: operations["getApiComplaintService-stats"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/resident/stats": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get resident statistics */
|
||||
get: operations["getApiResidentStats"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/resident/banjar-stats": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get population data per banjar */
|
||||
get: operations["getApiResidentBanjar-stats"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/resident/demographics": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get religious and gender demographics */
|
||||
get: operations["getApiResidentDemographics"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/event/": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get upcoming events */
|
||||
get: operations["getApiEvent"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/event/today": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get events for today */
|
||||
get: operations["getApiEventToday"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prisma": "^6.19.2",
|
||||
"react-dev-inspector": "^2.0.1",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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"
|
||||
@@ -21,9 +21,459 @@ model User {
|
||||
sessions Session[]
|
||||
apiKeys ApiKey[]
|
||||
|
||||
// Relations
|
||||
discussions Discussion[]
|
||||
events Event[]
|
||||
complaints Complaint[] @relation("ComplaintReporter")
|
||||
assignedComplaints Complaint[] @relation("ComplaintAssignee")
|
||||
complaintUpdates ComplaintUpdate[]
|
||||
serviceLetters ServiceLetter[]
|
||||
innovationIdeas InnovationIdea[] @relation("IdeaReviewer")
|
||||
healthRecords HealthRecord[]
|
||||
populationDynamics PopulationDynamic[]
|
||||
budgets Budget[]
|
||||
budgetTransactions BudgetTransaction[]
|
||||
posyandus Posyandu[]
|
||||
securityReports SecurityReport[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
// --- KATEGORI 1: KINERJA DIVISI & AKTIVITAS ---
|
||||
|
||||
model Division {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
color String @default("#1E3A5F")
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
activities Activity[]
|
||||
documents Document[]
|
||||
discussions Discussion[]
|
||||
divisionMetrics DivisionMetric[]
|
||||
|
||||
@@map("division")
|
||||
}
|
||||
|
||||
model Activity {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
description String?
|
||||
divisionId String
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
dueDate DateTime?
|
||||
progress Int @default(0) // 0-100
|
||||
status ActivityStatus @default(BERJALAN)
|
||||
priority Priority @default(SEDANG)
|
||||
assignedTo String? // JSON array of user IDs
|
||||
completedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
division Division @relation(fields: [divisionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([divisionId])
|
||||
@@index([status])
|
||||
@@map("activity")
|
||||
}
|
||||
|
||||
model Document {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
category DocumentCategory
|
||||
type String // "Gambar", "Dokumen", "PDF", etc
|
||||
fileUrl String
|
||||
fileSize Int? // in bytes
|
||||
divisionId String?
|
||||
uploadedBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
division Division? @relation(fields: [divisionId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([category])
|
||||
@@index([divisionId])
|
||||
@@map("document")
|
||||
}
|
||||
|
||||
model Discussion {
|
||||
id String @id @default(cuid())
|
||||
message String
|
||||
senderId String
|
||||
parentId String? // For threaded discussions
|
||||
divisionId String?
|
||||
isResolved Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
|
||||
parent Discussion? @relation("DiscussionThread", fields: [parentId], references: [id], onDelete: SetNull)
|
||||
replies Discussion[] @relation("DiscussionThread")
|
||||
division Division? @relation(fields: [divisionId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([divisionId])
|
||||
@@index([createdAt])
|
||||
@@map("discussion")
|
||||
}
|
||||
|
||||
model Event {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
description String?
|
||||
eventType EventType
|
||||
startDate DateTime
|
||||
endDate DateTime?
|
||||
location String?
|
||||
isAllDay Boolean @default(false)
|
||||
isRecurring Boolean @default(false)
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creator User @relation(fields: [createdBy], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([startDate])
|
||||
@@index([eventType])
|
||||
@@map("event")
|
||||
}
|
||||
|
||||
model DivisionMetric {
|
||||
id String @id @default(cuid())
|
||||
divisionId String
|
||||
period String // "2025-Q1", "2025-01"
|
||||
activityCount Int @default(0)
|
||||
completionRate Float @default(0)
|
||||
avgProgress Float @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
division Division @relation(fields: [divisionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([divisionId, period])
|
||||
@@map("division_metric")
|
||||
}
|
||||
|
||||
// --- KATEGORI 2: PENGADUAN & LAYANAN PUBLIK ---
|
||||
|
||||
model Complaint {
|
||||
id String @id @default(cuid())
|
||||
complaintNumber String @unique // Auto-generated: COMPLAINT-YYYYMMDD-XXX
|
||||
title String
|
||||
description String
|
||||
category ComplaintCategory
|
||||
status ComplaintStatus @default(BARU)
|
||||
priority Priority @default(SEDANG)
|
||||
|
||||
reporterId String?
|
||||
reporterPhone String?
|
||||
reporterEmail String?
|
||||
isAnonymous Boolean @default(false)
|
||||
|
||||
assignedTo String? // User ID
|
||||
resolvedBy String? // User ID
|
||||
resolvedAt DateTime?
|
||||
|
||||
location String?
|
||||
imageUrl String[] // Array of image URLs
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
reporter User? @relation("ComplaintReporter", fields: [reporterId], references: [id], onDelete: SetNull)
|
||||
assignee User? @relation("ComplaintAssignee", fields: [assignedTo], references: [id], onDelete: SetNull)
|
||||
|
||||
complaintUpdates ComplaintUpdate[]
|
||||
|
||||
@@index([status])
|
||||
@@index([category])
|
||||
@@index([createdAt])
|
||||
@@map("complaint")
|
||||
}
|
||||
|
||||
model ComplaintUpdate {
|
||||
id String @id @default(cuid())
|
||||
complaintId String
|
||||
message String
|
||||
status ComplaintStatus?
|
||||
updatedBy String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
complaint Complaint @relation(fields: [complaintId], references: [id], onDelete: Cascade)
|
||||
updater User @relation(fields: [updatedBy], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([complaintId])
|
||||
@@map("complaint_update")
|
||||
}
|
||||
|
||||
model ServiceLetter {
|
||||
id String @id @default(cuid())
|
||||
letterNumber String @unique
|
||||
letterType LetterType
|
||||
applicantName String
|
||||
applicantNik String
|
||||
applicantAddress String
|
||||
purpose String?
|
||||
status ServiceStatus @default(BARU)
|
||||
|
||||
processedBy String?
|
||||
completedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
processor User? @relation(fields: [processedBy], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([letterType])
|
||||
@@index([status])
|
||||
@@index([createdAt])
|
||||
@@map("service_letter")
|
||||
}
|
||||
|
||||
model InnovationIdea {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
description String
|
||||
category String // "Teknologi", "Ekonomi", "Kesehatan", "Pendidikan"
|
||||
submitterName String
|
||||
submitterContact String?
|
||||
status IdeaStatus @default(BARU)
|
||||
reviewedBy String?
|
||||
reviewedAt DateTime?
|
||||
notes String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
reviewer User? @relation("IdeaReviewer", fields: [reviewedBy], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([category])
|
||||
@@index([status])
|
||||
@@map("innovation_idea")
|
||||
}
|
||||
|
||||
// --- KATEGORI 3: DEMOGRAFI & KEPENDUDUKAN ---
|
||||
|
||||
model Resident {
|
||||
id String @id @default(cuid())
|
||||
nik String @unique
|
||||
kk String
|
||||
name String
|
||||
birthDate DateTime
|
||||
birthPlace String
|
||||
gender Gender
|
||||
religion Religion
|
||||
maritalStatus MaritalStatus @default(BELUM_KAWIN)
|
||||
education EducationLevel?
|
||||
occupation String?
|
||||
|
||||
banjarId String
|
||||
rt String
|
||||
rw String
|
||||
address String
|
||||
|
||||
isHeadOfHousehold Boolean @default(false)
|
||||
isPoor Boolean @default(false)
|
||||
isStunting Boolean @default(false)
|
||||
|
||||
deathDate DateTime?
|
||||
moveInDate DateTime?
|
||||
moveOutDate DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
banjar Banjar @relation(fields: [banjarId], references: [id], onDelete: Cascade)
|
||||
|
||||
healthRecords HealthRecord[]
|
||||
employmentRecords EmploymentRecord[]
|
||||
|
||||
@@index([banjarId])
|
||||
@@index([religion])
|
||||
@@index([occupation])
|
||||
@@map("resident")
|
||||
}
|
||||
|
||||
model Banjar {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
code String @unique
|
||||
description String?
|
||||
|
||||
totalPopulation Int @default(0)
|
||||
totalKK Int @default(0)
|
||||
totalPoor Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
residents Resident[]
|
||||
umkms Umkm[]
|
||||
|
||||
@@map("banjar")
|
||||
}
|
||||
|
||||
// --- STUBS FOR PHASE 2+ (To maintain relations) ---
|
||||
|
||||
model HealthRecord {
|
||||
id String @id @default(cuid())
|
||||
residentId String
|
||||
resident Resident @relation(fields: [residentId], references: [id])
|
||||
recordedBy String
|
||||
recorder User @relation(fields: [recordedBy], references: [id])
|
||||
}
|
||||
|
||||
model EmploymentRecord {
|
||||
id String @id @default(cuid())
|
||||
residentId String
|
||||
resident Resident @relation(fields: [residentId], references: [id])
|
||||
}
|
||||
|
||||
model PopulationDynamic {
|
||||
id String @id @default(cuid())
|
||||
documentedBy String
|
||||
documentor User @relation(fields: [documentedBy], references: [id])
|
||||
}
|
||||
|
||||
model Budget {
|
||||
id String @id @default(cuid())
|
||||
approvedBy String?
|
||||
approver User? @relation(fields: [approvedBy], references: [id])
|
||||
}
|
||||
|
||||
model BudgetTransaction {
|
||||
id String @id @default(cuid())
|
||||
createdBy String
|
||||
creator User @relation(fields: [createdBy], references: [id])
|
||||
}
|
||||
|
||||
model Umkm {
|
||||
id String @id @default(cuid())
|
||||
banjarId String?
|
||||
banjar Banjar? @relation(fields: [banjarId], references: [id])
|
||||
}
|
||||
|
||||
model Posyandu {
|
||||
id String @id @default(cuid())
|
||||
coordinatorId String?
|
||||
coordinator User? @relation(fields: [coordinatorId], references: [id])
|
||||
}
|
||||
|
||||
model SecurityReport {
|
||||
id String @id @default(cuid())
|
||||
assignedTo String?
|
||||
assignee User? @relation(fields: [assignedTo], references: [id])
|
||||
}
|
||||
|
||||
// --- ENUMS ---
|
||||
|
||||
enum ActivityStatus {
|
||||
BERJALAN
|
||||
SELESAI
|
||||
TERTUNDA
|
||||
DIBATALKAN
|
||||
}
|
||||
|
||||
enum Priority {
|
||||
RENDAH
|
||||
SEDANG
|
||||
TINGGI
|
||||
DARURAT
|
||||
}
|
||||
|
||||
enum DocumentCategory {
|
||||
SURAT_KEPUTUSAN
|
||||
DOKUMENTASI
|
||||
LAPORAN_KEUANGAN
|
||||
NOTULENSI_RAPAT
|
||||
UMUM
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
RAPAT
|
||||
KEGIATAN
|
||||
UPACARA
|
||||
SOSIAL
|
||||
BUDAYA
|
||||
LAINNYA
|
||||
}
|
||||
|
||||
enum ComplaintCategory {
|
||||
KETERTIBAN_UMUM
|
||||
PELAYANAN_KESEHATAN
|
||||
INFRASTRUKTUR
|
||||
ADMINISTRASI
|
||||
KEAMANAN
|
||||
LAINNYA
|
||||
}
|
||||
|
||||
enum ComplaintStatus {
|
||||
BARU
|
||||
DIPROSES
|
||||
SELESAI
|
||||
DITOLAK
|
||||
}
|
||||
|
||||
enum LetterType {
|
||||
KTP
|
||||
KK
|
||||
DOMISILI
|
||||
USAHA
|
||||
KETERANGAN_TIDAK_MAMPU
|
||||
SURAT_PENGANTAR
|
||||
LAINNYA
|
||||
}
|
||||
|
||||
enum ServiceStatus {
|
||||
BARU
|
||||
DIPROSES
|
||||
SELESAI
|
||||
DIAMBIL
|
||||
}
|
||||
|
||||
enum IdeaStatus {
|
||||
BARU
|
||||
DIKAJI
|
||||
DISETUJUI
|
||||
DITOLAK
|
||||
DIIMPLEMENTASI
|
||||
}
|
||||
|
||||
enum Gender {
|
||||
LAKI_LAKI
|
||||
PEREMPUAN
|
||||
}
|
||||
|
||||
enum Religion {
|
||||
HINDU
|
||||
ISLAM
|
||||
KRISTEN
|
||||
KATOLIK
|
||||
BUDDHA
|
||||
KONGHUCU
|
||||
LAINNYA
|
||||
}
|
||||
|
||||
enum MaritalStatus {
|
||||
BELUM_KAWIN
|
||||
KAWIN
|
||||
CERAI_HIDUP
|
||||
CERAI_MATI
|
||||
}
|
||||
|
||||
enum EducationLevel {
|
||||
TIDAK_SEKOLAH
|
||||
SD
|
||||
SMP
|
||||
SMA
|
||||
D3
|
||||
S1
|
||||
S2
|
||||
S3
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
|
||||
403
prisma/seed.ts
403
prisma/seed.ts
@@ -1,150 +1,325 @@
|
||||
import "dotenv/config";
|
||||
import { hash } from "bcryptjs";
|
||||
import { generateId } from "better-auth";
|
||||
import { prisma } from "@/utils/db";
|
||||
import {
|
||||
ActivityStatus,
|
||||
ComplaintCategory,
|
||||
ComplaintStatus,
|
||||
EventType,
|
||||
Gender,
|
||||
Priority,
|
||||
PrismaClient,
|
||||
Religion,
|
||||
} from "../generated/prisma";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function seedAdminUser() {
|
||||
// Load environment variables
|
||||
const adminEmail = process.env.ADMIN_EMAIL;
|
||||
const adminEmail = process.env.ADMIN_EMAIL || "admin@example.com";
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
|
||||
|
||||
if (!adminEmail) {
|
||||
console.log(
|
||||
"No ADMIN_EMAIL environment variable found. Skipping admin user creation.",
|
||||
);
|
||||
return;
|
||||
console.log(`Checking admin user: ${adminEmail}`);
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: adminEmail },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.role !== "admin") {
|
||||
await prisma.user.update({
|
||||
where: { email: adminEmail },
|
||||
data: { role: "admin" },
|
||||
});
|
||||
console.log("Updated existing user to admin role.");
|
||||
}
|
||||
return existingUser.id;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if admin user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: adminEmail },
|
||||
});
|
||||
const hashedPassword = await hash(adminPassword, 12);
|
||||
const userId = generateId();
|
||||
|
||||
if (existingUser) {
|
||||
// Update existing user to have admin role if they don't already
|
||||
if (existingUser.role !== "admin") {
|
||||
await prisma.user.update({
|
||||
where: { email: adminEmail },
|
||||
data: { role: "admin" },
|
||||
});
|
||||
console.log(`User with email ${adminEmail} updated to admin role.`);
|
||||
} else {
|
||||
console.log(`User with email ${adminEmail} already has admin role.`);
|
||||
}
|
||||
} else {
|
||||
// Create new admin user
|
||||
const hashedPassword = await hash(adminPassword, 12);
|
||||
const userId = generateId();
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: userId,
|
||||
email: adminEmail,
|
||||
name: "Admin User",
|
||||
role: "admin",
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.account.create({
|
||||
data: {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: userId,
|
||||
email: adminEmail,
|
||||
name: "Admin Desa Darmasaba",
|
||||
role: "admin",
|
||||
emailVerified: true,
|
||||
accounts: {
|
||||
create: {
|
||||
id: generateId(),
|
||||
userId,
|
||||
accountId: userId,
|
||||
providerId: "credential",
|
||||
password: hashedPassword,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Admin user created with email: ${adminEmail}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error seeding admin user:", error);
|
||||
throw error;
|
||||
}
|
||||
console.log(`Admin user created: ${adminEmail}`);
|
||||
return userId;
|
||||
}
|
||||
|
||||
async function seedDemoUsers() {
|
||||
const demoUsers = [
|
||||
{ email: "demo1@example.com", name: "Demo User 1", role: "user" },
|
||||
{ email: "demo2@example.com", name: "Demo User 2", role: "user" },
|
||||
async function seedBanjars() {
|
||||
const banjars = [
|
||||
{
|
||||
email: "moderator@example.com",
|
||||
name: "Moderator User",
|
||||
role: "moderator",
|
||||
name: "Darmasaba",
|
||||
code: "DSB",
|
||||
totalPopulation: 1200,
|
||||
totalKK: 300,
|
||||
totalPoor: 45,
|
||||
},
|
||||
{
|
||||
name: "Manesa",
|
||||
code: "MNS",
|
||||
totalPopulation: 950,
|
||||
totalKK: 240,
|
||||
totalPoor: 32,
|
||||
},
|
||||
{
|
||||
name: "Cabe",
|
||||
code: "CBE",
|
||||
totalPopulation: 800,
|
||||
totalKK: 200,
|
||||
totalPoor: 28,
|
||||
},
|
||||
{
|
||||
name: "Penenjoan",
|
||||
code: "PNJ",
|
||||
totalPopulation: 1100,
|
||||
totalKK: 280,
|
||||
totalPoor: 50,
|
||||
},
|
||||
{
|
||||
name: "Baler Pasar",
|
||||
code: "BPS",
|
||||
totalPopulation: 850,
|
||||
totalKK: 210,
|
||||
totalPoor: 35,
|
||||
},
|
||||
{
|
||||
name: "Bucu",
|
||||
code: "BCU",
|
||||
totalPopulation: 734,
|
||||
totalKK: 184,
|
||||
totalPoor: 24,
|
||||
},
|
||||
];
|
||||
|
||||
for (const userData of demoUsers) {
|
||||
try {
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: userData.email },
|
||||
});
|
||||
console.log("Seeding Banjars...");
|
||||
for (const banjar of banjars) {
|
||||
await prisma.banjar.upsert({
|
||||
where: { name: banjar.name },
|
||||
update: banjar,
|
||||
create: banjar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
const userId = generateId();
|
||||
const hashedPassword = await hash("demo123", 12);
|
||||
async function seedDivisions() {
|
||||
const divisions = [
|
||||
{
|
||||
name: "Pemerintahan",
|
||||
description: "Urusan administrasi dan tata kelola desa",
|
||||
color: "#1E3A5F",
|
||||
},
|
||||
{
|
||||
name: "Pembangunan",
|
||||
description: "Infrastruktur dan sarana prasarana desa",
|
||||
color: "#2E7D32",
|
||||
},
|
||||
{
|
||||
name: "Pemberdayaan",
|
||||
description: "Pemberdayaan ekonomi dan masyarakat",
|
||||
color: "#EF6C00",
|
||||
},
|
||||
{
|
||||
name: "Kesejahteraan",
|
||||
description: "Kesehatan, pendidikan, dan sosial",
|
||||
color: "#C62828",
|
||||
},
|
||||
];
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: userId,
|
||||
email: userData.email,
|
||||
name: userData.name,
|
||||
role: userData.role,
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
console.log("Seeding Divisions...");
|
||||
const createdDivisions = [];
|
||||
for (const div of divisions) {
|
||||
const d = await prisma.division.upsert({
|
||||
where: { name: div.name },
|
||||
update: div,
|
||||
create: div,
|
||||
});
|
||||
createdDivisions.push(d);
|
||||
}
|
||||
return createdDivisions;
|
||||
}
|
||||
|
||||
await prisma.account.create({
|
||||
data: {
|
||||
id: generateId(),
|
||||
userId,
|
||||
accountId: userId,
|
||||
providerId: "credential",
|
||||
password: hashedPassword,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
async function seedResidents(banjarIds: string[]) {
|
||||
console.log("Seeding Residents...");
|
||||
const residents = [
|
||||
{
|
||||
nik: "5103010101700001",
|
||||
kk: "5103010101700000",
|
||||
name: "I Wayan Sudarsana",
|
||||
birthDate: new Date("1970-05-15"),
|
||||
birthPlace: "Badung",
|
||||
gender: Gender.LAKI_LAKI,
|
||||
religion: Religion.HINDU,
|
||||
occupation: "Wiraswasta",
|
||||
banjarId: banjarIds[0],
|
||||
rt: "001",
|
||||
rw: "000",
|
||||
address: "Jl. Raya Darmasaba No. 1",
|
||||
isHeadOfHousehold: true,
|
||||
},
|
||||
{
|
||||
nik: "5103010101850002",
|
||||
kk: "5103010101850000",
|
||||
name: "Ni Made Arianti",
|
||||
birthDate: new Date("1985-08-20"),
|
||||
birthPlace: "Denpasar",
|
||||
gender: Gender.PEREMPUAN,
|
||||
religion: Religion.HINDU,
|
||||
occupation: "Guru",
|
||||
banjarId: banjarIds[1],
|
||||
rt: "002",
|
||||
rw: "000",
|
||||
address: "Gg. Manesa No. 5",
|
||||
isPoor: true,
|
||||
},
|
||||
];
|
||||
|
||||
console.log(`Demo user created: ${userData.email}`);
|
||||
} else {
|
||||
console.log(`Demo user already exists: ${userData.email}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error seeding user ${userData.email}:`, error);
|
||||
}
|
||||
for (const res of residents) {
|
||||
await prisma.resident.upsert({
|
||||
where: { nik: res.nik },
|
||||
update: res,
|
||||
create: res,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function seedActivities(divisionIds: string[]) {
|
||||
console.log("Seeding Activities...");
|
||||
const activities = [
|
||||
{
|
||||
title: "Rapat Koordinasi 2025",
|
||||
description: "Penyusunan rencana kerja tahunan",
|
||||
divisionId: divisionIds[0],
|
||||
progress: 100,
|
||||
status: ActivityStatus.SELESAI,
|
||||
priority: Priority.TINGGI,
|
||||
},
|
||||
{
|
||||
title: "Pemutakhiran Indeks Desa",
|
||||
description: "Pendataan SDG's Desa 2025",
|
||||
divisionId: divisionIds[0],
|
||||
progress: 65,
|
||||
status: ActivityStatus.BERJALAN,
|
||||
priority: Priority.SEDANG,
|
||||
},
|
||||
{
|
||||
title: "Pembangunan Jalan Banjar Cabe",
|
||||
description: "Pengaspalan jalan utama",
|
||||
divisionId: divisionIds[1],
|
||||
progress: 40,
|
||||
status: ActivityStatus.BERJALAN,
|
||||
priority: Priority.DARURAT,
|
||||
},
|
||||
];
|
||||
|
||||
for (const act of activities) {
|
||||
await prisma.activity.create({
|
||||
data: act,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function seedComplaints(adminId: string) {
|
||||
console.log("Seeding Complaints...");
|
||||
const complaints = [
|
||||
{
|
||||
complaintNumber: `COMP-20250326-001`,
|
||||
title: "Lampu Jalan Mati",
|
||||
description:
|
||||
"Lampu jalan di depan Balai Banjar Manesa mati sejak 3 hari lalu.",
|
||||
category: ComplaintCategory.INFRASTRUKTUR,
|
||||
status: ComplaintStatus.BARU,
|
||||
priority: Priority.SEDANG,
|
||||
location: "Banjar Manesa",
|
||||
reporterId: adminId,
|
||||
},
|
||||
{
|
||||
complaintNumber: `COMP-20250326-002`,
|
||||
title: "Sampah Menumpuk",
|
||||
description: "Tumpukan sampah di area pasar Darmasaba belum diangkut.",
|
||||
category: ComplaintCategory.KETERTIBAN_UMUM,
|
||||
status: ComplaintStatus.DIPROSES,
|
||||
priority: Priority.TINGGI,
|
||||
location: "Pasar Darmasaba",
|
||||
assignedTo: adminId,
|
||||
},
|
||||
];
|
||||
|
||||
for (const comp of complaints) {
|
||||
await prisma.complaint.upsert({
|
||||
where: { complaintNumber: comp.complaintNumber },
|
||||
update: comp,
|
||||
create: comp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function seedEvents(adminId: string) {
|
||||
console.log("Seeding Events...");
|
||||
const events = [
|
||||
{
|
||||
title: "Rapat Pleno Desa",
|
||||
description: "Pembahasan anggaran belanja desa",
|
||||
eventType: EventType.RAPAT,
|
||||
startDate: new Date(),
|
||||
location: "Balai Desa Darmasaba",
|
||||
createdBy: adminId,
|
||||
},
|
||||
{
|
||||
title: "Gotong Royong Kebersihan",
|
||||
description: "Kegiatan rutin mingguan",
|
||||
eventType: EventType.SOSIAL,
|
||||
startDate: new Date(Date.now() + 86400000), // Besok
|
||||
location: "Seluruh Banjar",
|
||||
createdBy: adminId,
|
||||
},
|
||||
];
|
||||
|
||||
for (const event of events) {
|
||||
await prisma.event.create({
|
||||
data: event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database...");
|
||||
console.log("Starting seed...");
|
||||
|
||||
await seedAdminUser();
|
||||
await seedDemoUsers();
|
||||
const adminId = await seedAdminUser();
|
||||
await seedBanjars();
|
||||
const banjars = await prisma.banjar.findMany();
|
||||
const banjarIds = banjars.map((b) => b.id);
|
||||
|
||||
console.log("Database seeding completed.");
|
||||
const divisions = await seedDivisions();
|
||||
const divisionIds = divisions.map((d) => d.id);
|
||||
|
||||
await seedResidents(banjarIds);
|
||||
await seedActivities(divisionIds);
|
||||
await seedComplaints(adminId);
|
||||
await seedEvents(adminId);
|
||||
|
||||
console.log("Seed finished successfully!");
|
||||
}
|
||||
|
||||
// Only auto-execute when run directly (not when imported)
|
||||
const isMainModule =
|
||||
typeof require !== "undefined"
|
||||
? require.main === module
|
||||
: import.meta.path.endsWith("seed.ts");
|
||||
|
||||
if (isMainModule) {
|
||||
main().catch((error) => {
|
||||
console.error("Error during seeding:", error);
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
// Export for programmatic use
|
||||
export { seedAdminUser, seedDemoUsers, main as runSeed };
|
||||
|
||||
66
src/api/complaint.ts
Normal file
66
src/api/complaint.ts
Normal 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
71
src/api/division.ts
Normal 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
54
src/api/event.ts
Normal 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" },
|
||||
},
|
||||
);
|
||||
@@ -4,7 +4,11 @@ import Elysia from "elysia";
|
||||
import { apiMiddleware } from "../middleware/apiMiddleware";
|
||||
import { auth } from "../utils/auth";
|
||||
import { apikey } from "./apikey";
|
||||
import { complaint } from "./complaint";
|
||||
import { division } from "./division";
|
||||
import { event } from "./event";
|
||||
import { profile } from "./profile";
|
||||
import { resident } from "./resident";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
@@ -20,7 +24,11 @@ const api = new Elysia({
|
||||
})
|
||||
.use(apiMiddleware)
|
||||
.use(apikey)
|
||||
.use(profile);
|
||||
.use(profile)
|
||||
.use(division)
|
||||
.use(complaint)
|
||||
.use(resident)
|
||||
.use(event);
|
||||
|
||||
if (!isProduction) {
|
||||
api.use(
|
||||
|
||||
@@ -2,12 +2,25 @@ import Elysia, { t } from "elysia";
|
||||
import { prisma } from "../utils/db";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
interface AuthenticatedUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export const profile = new Elysia({
|
||||
prefix: "/profile",
|
||||
}).post(
|
||||
"/update",
|
||||
async (ctx) => {
|
||||
const { body, set, user } = ctx as any;
|
||||
async ({
|
||||
body,
|
||||
set,
|
||||
user,
|
||||
}: {
|
||||
body: { name?: string; image?: string };
|
||||
set: any;
|
||||
user?: AuthenticatedUser;
|
||||
}) => {
|
||||
try {
|
||||
if (!user) {
|
||||
set.status = 401;
|
||||
|
||||
76
src/api/resident.ts
Normal file
76
src/api/resident.ts
Normal 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" },
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
|
||||
import { Grid, Image, Stack } from "@mantine/core";
|
||||
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { ActivityList } from "./dashboard/activity-list";
|
||||
import { ChartAPBDes } from "./dashboard/chart-apbdes";
|
||||
import { ChartSurat } from "./dashboard/chart-surat";
|
||||
@@ -32,8 +34,38 @@ const sdgsData = [
|
||||
];
|
||||
|
||||
export function DashboardContent() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
const [stats, setStats] = useState({
|
||||
complaints: { total: 0, baru: 0, proses: 0, selesai: 0 },
|
||||
residents: { total: 0, heads: 0, poor: 0 },
|
||||
loading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const [complaintRes, residentRes] = await Promise.all([
|
||||
apiClient.GET("/api/complaint/stats"),
|
||||
apiClient.GET("/api/resident/stats"),
|
||||
]);
|
||||
|
||||
setStats({
|
||||
complaints: (complaintRes.data as any)?.data || {
|
||||
total: 0,
|
||||
baru: 0,
|
||||
proses: 0,
|
||||
selesai: 0,
|
||||
},
|
||||
residents: (residentRes.data as any)?.data || { total: 0, heads: 0, poor: 0 },
|
||||
loading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch stats", error);
|
||||
setStats((prev) => ({ ...prev, loading: false }));
|
||||
}
|
||||
}
|
||||
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
@@ -42,36 +74,36 @@ export function DashboardContent() {
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<StatCard
|
||||
title="Surat Minggu Ini"
|
||||
value={99}
|
||||
detail="14 baru, 14 diproses"
|
||||
trend="12% dari minggu lalu ↗ +12%"
|
||||
trendValue={12}
|
||||
value={0}
|
||||
detail="Menunggu integrasi riil"
|
||||
trend="0%"
|
||||
trendValue={0}
|
||||
icon={<FileText style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<StatCard
|
||||
title="Pengaduan Aktif"
|
||||
value={28}
|
||||
detail="14 baru, 14 diproses"
|
||||
value={stats.complaints.baru + stats.complaints.proses}
|
||||
detail={`${stats.complaints.baru} baru, ${stats.complaints.proses} diproses`}
|
||||
icon={<MessageCircle style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<StatCard
|
||||
title="Layanan Selesai"
|
||||
value={156}
|
||||
detail="bulan ini"
|
||||
trend="+8%"
|
||||
trendValue={8}
|
||||
value={stats.complaints.selesai}
|
||||
detail="Total diselesaikan"
|
||||
trend="+0%"
|
||||
trendValue={0}
|
||||
icon={<CheckCircle style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<StatCard
|
||||
title="Kepuasan Warga"
|
||||
value="87.2%"
|
||||
detail="dari 482 responden"
|
||||
title="Total Penduduk"
|
||||
value={stats.residents.total.toLocaleString()}
|
||||
detail={`${stats.residents.heads} Kepala Keluarga`}
|
||||
icon={<Users style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
@@ -102,8 +134,8 @@ export function DashboardContent() {
|
||||
|
||||
{/* Section 6: SDGs Desa Cards */}
|
||||
<Grid gutter="md">
|
||||
{sdgsData.map((sdg, index) => (
|
||||
<Grid.Col key={index} span={{ base: 9, md: 3 }}>
|
||||
{sdgsData.map((sdg) => (
|
||||
<Grid.Col key={sdg.title} span={{ base: 9, md: 3 }}>
|
||||
<SDGSCard
|
||||
image={<Image src={sdg.image} alt={sdg.title} />}
|
||||
title={sdg.title}
|
||||
|
||||
@@ -48,9 +48,9 @@ export function ActivityList() {
|
||||
</Title>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{events.map((event, index) => (
|
||||
{events.map((event) => (
|
||||
<Box
|
||||
key={index}
|
||||
key={`${event.title}-${event.date}`}
|
||||
style={{
|
||||
borderLeft: "4px solid var(--mantine-color-blue-filled)",
|
||||
paddingLeft: 12,
|
||||
|
||||
@@ -45,8 +45,8 @@ export function ChartAPBDes() {
|
||||
Grafik APBDes
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{apbdesData.map((item, index) => (
|
||||
<Group key={index} align="center" gap="md">
|
||||
{apbdesData.map((item) => (
|
||||
<Group key={item.name} align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
|
||||
{item.name}
|
||||
</Text>
|
||||
|
||||
@@ -45,8 +45,8 @@ export function DivisionProgress() {
|
||||
Divisi Teraktif
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{divisionData.map((divisi, index) => (
|
||||
<Box key={index}>
|
||||
{divisionData.map((divisi) => (
|
||||
<Box key={divisi.name}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500} c={dark ? "white" : "gray.7"}>
|
||||
{divisi.name}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
@@ -51,8 +50,8 @@ export function SatisfactionChart() {
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{satisfactionData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
{satisfactionData.map((entry) => (
|
||||
<Cell key={`cell-${entry.name}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
@@ -65,8 +64,8 @@ export function SatisfactionChart() {
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Group justify="center" gap="md" mt="md">
|
||||
{satisfactionData.map((item, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
{satisfactionData.map((item) => (
|
||||
<Group key={item.name} gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
|
||||
220
src/components/dev-inspector.tsx
Normal file
220
src/components/dev-inspector.tsx
Normal 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",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Divider,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
@@ -21,44 +20,10 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
export function Header({ onSidebarToggle }: HeaderProps) {
|
||||
const location = useLocation();
|
||||
const _location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Define page titles based on route
|
||||
const getPageTitle = () => {
|
||||
switch (location.pathname) {
|
||||
case "/":
|
||||
return "Beranda";
|
||||
case "/kinerja-divisi":
|
||||
return "Kinerja Divisi";
|
||||
case "/pengaduan-layanan-publik":
|
||||
return "Pengaduan & Layanan Publik";
|
||||
case "/jenna-analytic":
|
||||
return "Jenna Analytic";
|
||||
case "/demografi-pekerjaan":
|
||||
return "Demografi & Kependudukan";
|
||||
case "/keuangan-anggaran":
|
||||
return "Keuangan & Anggaran";
|
||||
case "/bumdes":
|
||||
return "Bumdes & UMKM Desa";
|
||||
case "/sosial":
|
||||
return "Sosial";
|
||||
case "/keamanan":
|
||||
return "Keamanan";
|
||||
case "/bantuan":
|
||||
return "Bantuan";
|
||||
case "/pengaturan":
|
||||
case "/pengaturan/umum":
|
||||
case "/pengaturan/notifikasi":
|
||||
case "/pengaturan/keamanan":
|
||||
case "/pengaturan/akses-dan-tim":
|
||||
return "Pengaturan";
|
||||
default:
|
||||
return "Desa Darmasaba";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Group justify="space-between" w="100%">
|
||||
@@ -77,9 +42,6 @@ export function Header({ onSidebarToggle }: HeaderProps) {
|
||||
style={{ width: "70%", height: "70%" }}
|
||||
/>
|
||||
</ActionIcon>
|
||||
{/* <Title order={3} c={"white"}>
|
||||
{getPageTitle()}
|
||||
</Title> */}
|
||||
</Group>
|
||||
|
||||
{/* Right Section */}
|
||||
|
||||
@@ -152,9 +152,9 @@ const HelpPage = () => {
|
||||
|
||||
{/* Statistics Section */}
|
||||
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
||||
{stats.map((stat, index) => (
|
||||
{stats.map((stat) => (
|
||||
<HelpCard
|
||||
key={index}
|
||||
key={stat.label}
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
p="lg"
|
||||
style={{
|
||||
@@ -192,9 +192,9 @@ const HelpPage = () => {
|
||||
h="100%"
|
||||
>
|
||||
<Box>
|
||||
{guideItems.map((item, index) => (
|
||||
{guideItems.map((item) => (
|
||||
<Box
|
||||
key={index}
|
||||
key={item.title}
|
||||
py="sm"
|
||||
style={{
|
||||
borderBottom: "1px solid #eee",
|
||||
@@ -226,9 +226,9 @@ const HelpPage = () => {
|
||||
h="100%"
|
||||
>
|
||||
<Box>
|
||||
{videoItems.map((item, index) => (
|
||||
{videoItems.map((item) => (
|
||||
<Box
|
||||
key={index}
|
||||
key={item.title}
|
||||
py="sm"
|
||||
style={{
|
||||
borderBottom: "1px solid #eee",
|
||||
@@ -260,13 +260,13 @@ const HelpPage = () => {
|
||||
h="100%"
|
||||
>
|
||||
<Accordion variant="separated">
|
||||
{faqItems.map((item, index) => (
|
||||
{faqItems.map((item) => (
|
||||
<Accordion.Item
|
||||
style={{
|
||||
backgroundColor: dark ? "#263852ff" : "#F1F5F9",
|
||||
}}
|
||||
key={index}
|
||||
value={`faq-${index}`}
|
||||
key={item.question}
|
||||
value={item.question}
|
||||
>
|
||||
<Accordion.Control>{item.question}</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
@@ -335,9 +335,9 @@ const HelpPage = () => {
|
||||
h="100%"
|
||||
>
|
||||
<Box>
|
||||
{documentationItems.map((item, index) => (
|
||||
{documentationItems.map((item) => (
|
||||
<Box
|
||||
key={index}
|
||||
key={item.title}
|
||||
py="sm"
|
||||
style={{
|
||||
borderBottom: "1px solid #eee",
|
||||
@@ -434,6 +434,7 @@ const HelpPage = () => {
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={isLoading || inputValue.trim() === ""}
|
||||
style={{
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Box,
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
@@ -224,9 +223,9 @@ const JennaAnalytic = () => {
|
||||
Topik Pertanyaan Terbanyak
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{topTopics.map((item, index) => (
|
||||
{topTopics.map((item) => (
|
||||
<Box
|
||||
key={index}
|
||||
key={item.topic}
|
||||
p="sm"
|
||||
bg={dark ? "#334155" : "#F1F5F9"}
|
||||
style={{
|
||||
@@ -270,8 +269,8 @@ const JennaAnalytic = () => {
|
||||
Jam Tersibuk
|
||||
</Title>
|
||||
<Stack gap="md">
|
||||
{busyHours.map((item, index) => (
|
||||
<Box key={index}>
|
||||
{busyHours.map((item) => (
|
||||
<Box key={item.period}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
|
||||
{item.period}
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconCamera,
|
||||
IconClock,
|
||||
IconMapPin
|
||||
IconMapPin,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
const KeamananPage = () => {
|
||||
@@ -120,8 +120,8 @@ const KeamananPage = () => {
|
||||
<Stack gap={"xs"}>
|
||||
{/* KPI Cards */}
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((kpi, index) => (
|
||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
||||
{kpiData.map((kpi) => (
|
||||
<GridCol key={kpi.title} span={{ base: 12, sm: 6, md: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
@@ -214,9 +214,9 @@ const KeamananPage = () => {
|
||||
<Title order={4} c={dark ? "dark.0" : "black"}>
|
||||
Daftar CCTV
|
||||
</Title>
|
||||
{cctvLocations.map((cctv, index) => (
|
||||
{cctvLocations.map((cctv) => (
|
||||
<Card
|
||||
key={index}
|
||||
key={cctv.id}
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
@@ -269,9 +269,9 @@ const KeamananPage = () => {
|
||||
h="100%"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{securityReports.map((report, index) => (
|
||||
{securityReports.map((report) => (
|
||||
<Card
|
||||
key={index}
|
||||
key={report.id}
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
|
||||
@@ -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 { ArchiveCard } from "./kinerja-divisi/archive-card";
|
||||
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 { 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)
|
||||
const archiveData = [
|
||||
{ name: "Surat Keputusan" },
|
||||
@@ -44,20 +19,70 @@ const archiveData = [
|
||||
];
|
||||
|
||||
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 (
|
||||
<Stack gap="lg">
|
||||
{/* SECTION 1 — PROGRAM KEGIATAN */}
|
||||
<Grid gutter="md">
|
||||
{programKegiatanData.map((kegiatan, index) => (
|
||||
<Grid.Col key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
{activities.slice(0, 4).map((kegiatan, index) => (
|
||||
<Grid.Col
|
||||
key={kegiatan.id || index}
|
||||
span={{ base: 12, md: 6, lg: 3 }}
|
||||
>
|
||||
<ActivityCard
|
||||
title={kegiatan.title}
|
||||
date={kegiatan.date}
|
||||
date={dayjs(kegiatan.createdAt).format("D MMMM YYYY")}
|
||||
progress={kegiatan.progress}
|
||||
status={kegiatan.status}
|
||||
status={
|
||||
kegiatan.status === "SELESAI"
|
||||
? "Selesai"
|
||||
: kegiatan.status === "BERJALAN"
|
||||
? "Berjalan"
|
||||
: "Tertunda"
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* SECTION 2 — GRID DASHBOARD (3 Columns) */}
|
||||
@@ -82,7 +107,7 @@ const KinerjaDivisi = () => {
|
||||
<DiscussionPanel />
|
||||
|
||||
{/* SECTION 4 — ACARA HARI INI */}
|
||||
<EventCard />
|
||||
<EventCard agendas={formattedEvents} />
|
||||
|
||||
{/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */}
|
||||
<Grid gutter="md">
|
||||
|
||||
@@ -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 {
|
||||
title: string;
|
||||
|
||||
@@ -1,34 +1,48 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
|
||||
interface DivisionItem {
|
||||
name: string;
|
||||
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() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
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 (
|
||||
<Card
|
||||
p="md"
|
||||
@@ -47,30 +61,40 @@ export function DivisionList() {
|
||||
Divisi Teraktif
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
{divisionData.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>
|
||||
{loading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader size="sm" />
|
||||
</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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,17 @@ import {
|
||||
Card,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { CheckCircle, Clock, FileText, MessageCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
@@ -22,40 +26,11 @@ import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
|
||||
// Summary data
|
||||
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",
|
||||
},
|
||||
];
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
// Tren pengaduan data
|
||||
// Tren pengaduan data (Mock for now)
|
||||
const trenData = [
|
||||
{ bulan: "Apr", jumlah: 35 },
|
||||
{ bulan: "Mei", jumlah: 48 },
|
||||
@@ -66,50 +41,7 @@ const trenData = [
|
||||
{ bulan: "Okt", jumlah: 52 },
|
||||
];
|
||||
|
||||
// Surat terbanyak data
|
||||
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
|
||||
// Ide inovatif data (Mock for now)
|
||||
const ideInovatif = [
|
||||
{
|
||||
nama: "Andi Prasetyo",
|
||||
@@ -123,24 +55,13 @@ const ideInovatif = [
|
||||
waktu: "5 hari yang lalu",
|
||||
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) => {
|
||||
switch (status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case "baru":
|
||||
return "red";
|
||||
case "diproses":
|
||||
case "proses":
|
||||
return "blue";
|
||||
case "selesai":
|
||||
@@ -154,6 +75,75 @@ const PengaduanLayananPublik = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
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 (
|
||||
<Stack gap="lg">
|
||||
{/* TOP SECTION - 4 STAT CARDS */}
|
||||
@@ -178,7 +168,7 @@ const PengaduanLayananPublik = () => {
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||
{item.value}
|
||||
{loading ? <Loader size="xs" /> : item.value}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{item.subtitle}
|
||||
@@ -189,9 +179,6 @@ const PengaduanLayananPublik = () => {
|
||||
variant="filled"
|
||||
size="lg"
|
||||
radius="xl"
|
||||
style={{
|
||||
transition: "transform 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<item.icon style={{ width: "60%", height: "60%" }} />
|
||||
</ThemeIcon>
|
||||
@@ -278,35 +265,45 @@ const PengaduanLayananPublik = () => {
|
||||
Surat Terbanyak
|
||||
</Title>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={suratData} layout="vertical">
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
type="number"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="jenis"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
width={80}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="jumlah" fill="#396aaaff" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
{loading ? (
|
||||
<Group justify="center" align="center" h="100%">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : (
|
||||
<BarChart data={serviceStats} layout="vertical">
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
type="number"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="jenis"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
width={80}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="jumlah"
|
||||
fill="#396aaaff"
|
||||
radius={[0, 4, 4, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -328,42 +325,52 @@ const PengaduanLayananPublik = () => {
|
||||
Pengajuan Terbaru
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{pengajuanTerbaru.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
p="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#334155" : "#F1F5F9"}
|
||||
style={{
|
||||
borderColor: "transparent",
|
||||
transition: "background-color 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{item.jenis}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Badge
|
||||
color={getStatusColor(item.status)}
|
||||
variant="light"
|
||||
radius="sm"
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
<Text size="xs" c="dimmed">
|
||||
{item.waktu}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
{loading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader />
|
||||
</Group>
|
||||
) : recentComplaints.length > 0 ? (
|
||||
recentComplaints.map((item, index) => (
|
||||
<Card
|
||||
key={item.id || index}
|
||||
p="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#334155" : "#F1F5F9"}
|
||||
style={{
|
||||
borderColor: "transparent",
|
||||
transition: "background-color 0.15s ease",
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{item.category}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Badge
|
||||
color={getStatusColor(item.status)}
|
||||
variant="light"
|
||||
radius="sm"
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
<Text size="xs" c="dimmed">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Text c="dimmed" ta="center">
|
||||
Tidak ada pengajuan terbaru
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
import { createTheme, MantineProvider } from "@mantine/core";
|
||||
import { ModalsProvider } from "@mantine/modals";
|
||||
import { createRouter, RouterProvider } from "@tanstack/react-router";
|
||||
import { Inspector } from "react-dev-inspector";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import "./index.css";
|
||||
import "@mantine/charts/styles.css";
|
||||
import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env";
|
||||
import { IS_DEV } from "./utils/env";
|
||||
|
||||
// Create a new router instance
|
||||
export const router = createRouter({
|
||||
@@ -101,29 +100,14 @@ const theme = createTheme({
|
||||
primaryColor: "darmasaba-blue",
|
||||
});
|
||||
|
||||
// Use dynamic import for DevInspector to avoid including it in production bundle
|
||||
const InspectorWrapper = IS_DEV
|
||||
? Inspector
|
||||
? (await import("./components/dev-inspector")).DevInspector
|
||||
: ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
|
||||
const elem = document.getElementById("root")!;
|
||||
const app = (
|
||||
<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,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<InspectorWrapper>
|
||||
<MantineProvider theme={theme} defaultColorScheme="auto">
|
||||
<ModalsProvider>
|
||||
<RouterProvider router={router} />
|
||||
|
||||
14
src/index.ts
14
src/index.ts
@@ -6,7 +6,7 @@ import { Elysia } from "elysia";
|
||||
import api from "./api";
|
||||
import { openInEditor } from "./utils/open-in-editor";
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
@@ -35,14 +35,16 @@ if (!isProduction) {
|
||||
app.post("/__open-in-editor", ({ body }) => {
|
||||
const { relativePath, lineNumber, columnNumber } = body as {
|
||||
relativePath: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
lineNumber: string;
|
||||
columnNumber: string;
|
||||
};
|
||||
|
||||
const editor = (process.env.REACT_EDITOR || "code") as any;
|
||||
|
||||
openInEditor(relativePath, {
|
||||
line: lineNumber,
|
||||
column: columnNumber,
|
||||
editor: "antigravity",
|
||||
line: Number(lineNumber),
|
||||
column: Number(columnNumber),
|
||||
editor: editor,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
|
||||
@@ -11,7 +11,7 @@ export const Route = createRootRoute({
|
||||
// Apply protected route middleware for all routes
|
||||
// The middleware will determine which routes are public vs protected
|
||||
const context = await protectedRouteMiddleware({ location });
|
||||
|
||||
|
||||
// Only set auth store if we have user data (for protected routes)
|
||||
if (context?.user) {
|
||||
authStore.user = context?.user as any;
|
||||
|
||||
53
src/utils/dev-inspector-plugin.ts
Normal file
53
src/utils/dev-inspector-plugin.ts
Normal 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");
|
||||
},
|
||||
};
|
||||
}
|
||||
20
src/vite.ts
20
src/vite.ts
@@ -1,9 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { inspectorServer } from "@react-dev-inspector/vite-plugin";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { tanstackRouter } from "@tanstack/router-vite-plugin";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { inspectorPlugin } from "./utils/dev-inspector-plugin";
|
||||
|
||||
export async function createVite() {
|
||||
return createViteServer({
|
||||
@@ -14,23 +14,7 @@ export async function createVite() {
|
||||
"@": path.resolve(process.cwd(), "./src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
[
|
||||
"@react-dev-inspector/babel-plugin",
|
||||
{
|
||||
relativePath: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
inspectorServer(),
|
||||
tanstackRouter(),
|
||||
],
|
||||
plugins: [tailwindcss(), inspectorPlugin(), react(), tanstackRouter()],
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
hmr: {
|
||||
|
||||
Reference in New Issue
Block a user