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