Compare commits

...

514 Commits

Author SHA1 Message Date
d84edc44f5 fix: rename DistribusiUmur.kelompok to rentangUmur to match UI/API
- Update Prisma schema: kelompok -> rentangUmur
- Update seed data JSON: kelompok -> rentangUmur
- Update seeder file: use rentangUmur field
- This fixes the empty data issue in distribusi-umur admin page

Note: Run 'bunx prisma db push' to apply schema migration

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-10 12:03:25 +08:00
8b14c6ce44 feat: add kependudukan seeders, API routes, year filter, and navbar menu
- Add Prisma models: DataBanjar, DistribusiAgama, DistribusiUmur, MigrasiPenduduk, DinamikaPenduduk
- Create seeders for all kependudukan models with year 2026 data
- Register Kependudukan API routes in route.ts
- Update API findMany endpoints to make tahun parameter optional
- Add YearFilter reusable component for admin pages
- Update 4 kependudukan admin pages with year filter UI
- Fix Mantine color array in AdminThemeProvider (add 10th element)
- Fix invalid Mantine color scale in paguTable.tsx (gray.50 -> gray.1)
- Add Kependudukan menu to navbar-list-menu.ts
- Fix Bun JSON import resolution with loadJsonData helper
- Update 74 seeder files to use dynamic JSON loading

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-10 11:54:36 +08:00
5e822f0b05 feat: implement Kependudukan menu with CRUD admin pages
- Add Distribusi Umur admin pages (list, create, edit)
- Add Data Banjar admin pages (list, create, edit)
- Add Migrasi Penduduk admin pages (list, create, edit)
- Update state management with full CRUD operations for all modules
- Add Kependudukan menu to admin sidebar (devBar, navBar, role1)
- Add public pages for Distribusi Umur with age range sorting
- Update Dinamika Penduduk to use real-time birth/death data
- Add Biome configuration for code linting
- Create API routes for all Kependudukan modules

Features:
- Pagination and search for all admin list pages
- Responsive design (table for desktop, cards for mobile)
- Delete confirmation modal
- Toast notifications for user feedback
- Zod validation for all forms
- Age range auto-sorting in public Distribusi Umur chart

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-09 17:10:29 +08:00
34a37dc63b chore(dev-dependency): add playwright-mcp package for testing
- Add playwright-mcp v0.0.19 as dev dependency
- Update bun.lock with new dependency tree
- Add .qwen configuration files

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-09 11:33:19 +08:00
0e6f7e1769 fix(landing-page): remove incorrect national holiday entry for April 9, 2026
- Remove '2026-04-09' from national holidays list as it's not an actual holiday
- Fix office status showing 'Tutup' and 'Tidak Beroperasi' incorrectly on regular workday
- Office now correctly shows 'Buka' status during working hours (07:30-15:30 on Thursday)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-09 10:49:10 +08:00
feb853d06e fix(auth): improve OTP error handling and add health check endpoint
Option 2 - Improve Error Handling:
- Track WA success status and error messages in login route
- Return debug info (including OTP code) only in non-production
- Show descriptive message when WhatsApp fails to send
- Better error categorization (HTTP error vs logic error vs connection)

Option 3 - Health Check Endpoint:
- Create /api/health/otp endpoint for OTP service diagnostics
- Support test mode with query params: ?test=true&number=6281234567890
- Check token configuration (configured vs placeholder)
- Measure response time and validate service response
- Return comprehensive status for debugging OTP issues

Usage:
- Basic check: GET /api/health/otp
- Test send: GET /api/health/otp?test=true&number=6281234567890

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 16:27:29 +08:00
3de412afe0 fix(dockerfile): remove duplicate prisma copy in runner stage
- Remove unnecessary COPY of src/prisma as prisma is already copied
- Simplify Dockerfile by avoiding redundant file copies

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 15:05:03 +08:00
87d234e57f fix(dockerfile): optimize build and improve security
- Add blank line before COPY for readability
- Add PORT and HOSTNAME env vars in runner stage
- Use --chown flag instead of separate chown RUN layer
- Copy only src/prisma instead of entire src directory
- Use glob pattern for next.config.* files
- Move PORT and HOSTNAME before EXPOSE
- Add newline at end of file

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 14:48:14 +08:00
fd18a22834 merge: resolve Dockerfile conflicts between stg and deploy/stg
- Keep optimized multi-stage build from stg
- Add gen:api step from deploy/stg
- Maintain security best practices (non-root user, minimal deps)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 14:33:30 +08:00
3e8b961e52 chore: remove .env from git tracking and simplify .gitignore rules
- Remove .env file from git tracking (was accidentally committed)
- Simplify .gitignore to use .env* pattern instead of multiple specific patterns
- Update Dockerfile for optimized multi-stage build
- Add gen:api script placeholder to package.json

Security: Ensure sensitive environment variables are not exposed in repository

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 14:30:38 +08:00
82d779e5e0 Update Dockerfile 2026-04-06 14:00:17 +08:00
a6517166cb Merge pull request #19 from bipprojectbali/revert-11-tasks/fix-docker-build/optimize-config-and-eslint-ignore/2026-04-02-15-45
Revert 11 tasks/fix docker build/optimize config and eslint ignore/2026 04 02 15 45
2026-04-06 12:27:17 +08:00
483b6be677 Revert "fix(build): ignore ESLint and TypeScript errors during build for CI/CD" 2026-04-06 12:23:07 +08:00
f8dad0dbcd Merge pull request #17 from bipprojectbali/stg
Stg
2026-04-06 12:14:49 +08:00
74301fe074 Merge pull request #16 from bipprojectbali/tasks/prisma/regenerate-client-fix-types/06-04-2026-1500
Tasks/prisma/regenerate client fix types/06 04 2026 1500
2026-04-06 12:14:23 +08:00
8b19abc628 fix(prisma): regenerate Prisma client to resolve TypeScript type errors
- Regenerate Prisma client to fix missing GetPayload types
- Resolve RespondenGetPayload, JenisKelaminRespondenGetPayload errors
- Resolve PilihanRatingRespondenGetPayload and UmurRespondenGetPayload errors
- Add initial migration files
- Update bun lockfile

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 12:08:49 +08:00
e73798a0f2 chore: regenerate bun lockfile 2026-04-06 11:52:47 +08:00
1a91f3c9ad Merge pull request #13 from bipprojectbali/stg
Stg
2026-04-06 11:34:48 +08:00
9b74592101 Merge branch 'main' into stg 2026-04-06 11:34:38 +08:00
55f4b94082 Merge pull request #12 from bipprojectbali/tasks/auth/implement-otp-whatsapp-function/06-04-2026-1430
feat(auth): implement WhatsApp OTP sending function for login
2026-04-06 11:31:30 +08:00
b403bc754c feat(auth): implement WhatsApp OTP sending function for login
- Create sendCodeOtp utility function using otp.wibudev.com API
- Update login route to use new sendCodeOtp function
- Replace old URL-based WhatsApp approach with authenticated API call
- Add WA_SERVER_TOKEN environment variable documentation
- Support flexible codeOtp type (string | number) for better reusability

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 11:27:55 +08:00
67b87f145e Merge remote-tracking branch 'deploy/stg' into stg 2026-04-04 08:39:01 +08:00
dd09d7c90a fix(build): remove eslint-disable and replace 'any' with MantineColor in grafikRealisasi 2026-04-04 08:36:41 +08:00
59ae8ad039 Merge pull request #11 from bipprojectbali/tasks/fix-docker-build/optimize-config-and-eslint-ignore/2026-04-02-15-45
fix(build): ignore ESLint and TypeScript errors during build for CI/CD
2026-04-02 16:20:32 +08:00
c012d5778c fix(build): ignore ESLint and TypeScript errors during build for CI/CD 2026-04-02 16:16:17 +08:00
af31bd8aef Merge pull request #10 from bipprojectbali/nico/2-apr-26/default-docker-setting
Ganti ke settingan awal Docker
2026-04-02 15:55:01 +08:00
721357adcf Ganti ke settingan awal Docker 2026-04-02 15:54:27 +08:00
39776ec355 Merge pull request #9 from bipprojectbali/tasks/fix-build-and-lint/resolve-errors-in-auth-and-apbdes/2026-04-02-15-15
fix(apbdes): remove redundant eslint-disable and improve type safety …
2026-04-02 13:03:29 +08:00
50a7356618 fix(apbdes): remove redundant eslint-disable and improve type safety in GrafikRealisasi 2026-04-02 12:47:28 +08:00
4494dd98ef Merge pull request #8 from bipprojectbali/tasks/fix-docker-build/optimize-config-and-prisma-handlers/02-04-2026-15-00
Tasks/fix docker build/optimize config and prisma handlers/02 04 2026 15 00
2026-04-02 11:26:13 +08:00
970949a68b fix: resolve Docker build failure by optimizing configuration and prisma signal handling
- Added .dockerignore to prevent build poisoning from local artifacts.
- Updated Dockerfile with stable Bun version, memory limits, and missing config files.
- Refined prisma.ts signal handlers to avoid process termination during Next.js build phases.
- Synchronized eslint-config-next with Next.js version.
2026-04-02 11:24:49 +08:00
8777c45a44 fix(build): resolve ESLint and type errors causing build failure 2026-04-01 17:44:48 +08:00
42bcba6c96 Merge pull request #7 from bipprojectbali/stg
Stg
2026-04-01 17:10:09 +08:00
d1d54e5c25 Merge branch 'main' into stg 2026-04-01 17:09:58 +08:00
0a4b85fd82 Merge pull request #6 from bipprojectbali/tasks/api/fix-swagger-path-group/01-04-2026-1215
Tasks/api/fix swagger path group/01 04 2026 1215
2026-04-01 17:07:25 +08:00
b751f031cd fix(auth/swagger): make WA failure non-fatal and include /api prefix in docs 2026-04-01 17:04:25 +08:00
a3940321a7 fix(api): move swagger to /api group to prevent double prefixing 2026-04-01 15:29:26 +08:00
3cd6fcbd81 fix(api): clean up redundant /api prefixes and fix swagger documentation 2026-04-01 15:24:12 +08:00
7d9b7b0c60 feat(apbdes): finalize modernization and update config 2026-04-01 15:15:01 +08:00
0806eb2308 feat(apbdes): modernize ui, charts and refactor (Phase 1, 2, 4) 2026-04-01 15:09:40 +08:00
github-actions[bot]
6064ef0759 chore: sync workflows from base-template 2026-03-12 06:48:18 +00:00
1c00c326c9 Merge pull request #5 from bipprojectbali/fix-error-music-stg
Fix: Use window.location.origin for API base URL in browser
2026-03-12 14:16:32 +08:00
51ce823b45 Fix: Use window.location.origin for API base URL in browser
treaty from @elysiajs/eden doesn't support relative URLs like '/'
This caused 'ERR_NAME_NOT_RESOLVED' when trying to access 'https://api/fileStorage/create'

Solution:
- Client-side: Use window.location.origin (e.g., https://desa-darmasaba-stg.wibudev.com)
- Server-side dev: Use localhost:3000
- Server-side prod: Use NEXT_PUBLIC_BASE_URL env var

This ensures the API calls use the correct domain in all environments.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-12 14:14:55 +08:00
4eba96140d Merge pull request #4 from bipprojectbali/fix-error-music-stg
Fix error music stg
2026-03-12 12:13:50 +08:00
f6f0e10935 Fix Url API Route 2026-03-12 12:11:10 +08:00
2108f403aa Update .env.example to use relative URL '/' as default for NEXT_PUBLIC_BASE_URL
This ensures the API uses the same protocol and domain as the frontend,
preventing mixed content blocking in staging/production environments.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-12 12:06:16 +08:00
4dfcf20322 Merge pull request #3 from bipprojectbali/fix-error-music-stg
Fix: CORS and API base URL for music create in staging
2026-03-12 11:37:19 +08:00
c6c3eebadf Fix: CORS and API base URL for music create in staging
- Update CORS config to allow all origins (wildcard first) for better staging support
- Change API fetch base URL from absolute to relative (/) to prevent mixed content blocking
- Add detailed logging in music create page for better debugging
- Update .env.example with better NEXT_PUBLIC_BASE_URL documentation
- Add MUSIK_CREATE_ANALYSIS.md with comprehensive error analysis

Fixes ERR_BLOCKED_BY_CLIENT error when creating music in staging environment

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-12 11:27:51 +08:00
github-actions[bot]
6d26ace8ab chore: sync workflows from base-template 2026-03-10 08:16:42 +00:00
bipproduction
0dabc204bc Revert standalone output, keep serverExternalPackages fix
- Remove output: standalone to keep migration/seed workflow
- Restore original Dockerfile structure with full node_modules copy
- Keep serverExternalPackages for @elysiajs/static and elysia to fix
  the Html prerender error caused by dynamic import in @elysiajs/static
- Keep NODE_ENV=production before build step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:53:18 +08:00
bipproduction
e8f8b51686 Fix build: externalize elysia packages, use standalone output, fix NODE_ENV
- Add serverExternalPackages for @elysiajs/static and elysia to prevent
  webpack from bundling dynamic imports that cause Html prerender error
- Use output: standalone for proper Docker deployment
- Comment out NODE_ENV=development in .env.example to avoid conflict
  with next build which requires NODE_ENV=production
- Set NODE_ENV=production before build step in Dockerfile
- Update runtime stage to use standalone output structure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:48:05 +08:00
bipproduction
a4db3a149d Fix build error: move ViewTransitions inside body to fix 404 prerendering
ViewTransitions was wrapping the html element, which violates Next.js App
Router requirement that html and body be returned directly from root layout.
This caused prerendering of /404 to fail with Html import error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:29:29 +08:00
bipproduction
fece983ac5 Fix Dockerfile: remove non-existent gen:api script and fix build output paths
- Remove bun run gen:api which does not exist in package.json
- Change dist to .next for correct Next.js build output
- Replace non-existent generated/ with public/ for static assets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:12:33 +08:00
8b7eef5fee New bunlock.b 2026-03-10 11:04:44 +08:00
8b22d01e0d FIx Docker File 2026-03-10 10:56:15 +08:00
dc13e37a02 Add .env.example and fix .gitignore to allow it
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-10 10:50:52 +08:00
2d2cbef29b Tambah File Docker 2026-03-10 10:42:51 +08:00
8c8a96b830 First Stg 2026-03-10 10:07:14 +08:00
dc3eccacbf First Stg 2026-03-10 10:03:33 +08:00
ffe94992e5 StaggingWeb 2026-03-09 16:44:42 +08:00
github-actions[bot]
f5566bca2c chore: sync workflows from base-template 2026-03-09 07:53:36 +00:00
github-actions[bot]
ba964df32c chore: sync workflows from base-template 2026-03-09 07:45:35 +00:00
github-actions[bot]
df3f382a97 chore: sync workflows from base-template 2026-03-09 07:05:55 +00:00
4fb522f88f Fix Eror Grafik Realisasi-3 2026-03-06 12:03:22 +08:00
85332a8225 Merge pull request 'Fix Eror Grafik Realisasi-2' (#78) from nico/6-mar-26/fix-container-portainer-1 into staggingweb
Reviewed-on: #78
2026-03-06 11:24:46 +08:00
3fe2a5ccab Fix Eror Grafik Realisasi-2 2026-03-06 11:19:45 +08:00
363bfa65fb Merge pull request 'Fix Eror Grafik Realisasi' (#77) from nico/6-mar-26/fix-container-portainer-1 into staggingweb
Reviewed-on: #77
2026-03-06 10:53:19 +08:00
dccf590cbf Fix Eror Grafik Realisasi 2026-03-06 10:52:10 +08:00
f076b81d14 Merge pull request 'Fix Prisma 1' (#76) from nico/6-mar-26/fix-container-portainer-1 into staggingweb
Reviewed-on: #76
2026-03-06 10:35:54 +08:00
b5ea3216e0 Fix Prisma 1 2026-03-06 10:31:19 +08:00
64b116588b Merge pull request 'nico/5-mar-26/fix-musik-fix-apbdes' (#75) from nico/5-mar-26/fix-musik-fix-apbdes into staggingweb
Reviewed-on: #75
2026-03-05 16:38:07 +08:00
63161e1a39 Fix tombolreplay, posisi tombol, posisi icon music. Fix create & edit apbdes upload image dan file optional 2026-03-05 16:36:12 +08:00
8b8c65dd1e fix(apbdes-edit): clear imageId/fileId when user removes preview
Problem:
- Saat user klik X button untuk hapus preview image/file
- Form state masih menyimpan imageId/fileId lama
- Saat submit, data lama tetap terkirim
- User tidak bisa benar-benar menghapus image/file

Solution:
- Clear apbdesState.edit.form.imageId saat hapus preview gambar
- Clear apbdesState.edit.form.fileId saat hapus preview dokumen
- Now user can truly make image/file empty

Files changed:
- src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 16:26:34 +08:00
159fb3cec6 feat(apbdes): make image and file optional for edit page too
Changes:

Backend (updt.ts, index.ts):
- Update FormUpdateBody: imageId?: string | null
- Update Elysia schema: t.Optional(t.String())
- Handle null/undefined values when updating

UI (edit/page.tsx):
- Remove mandatory validation for imageId and fileId
- Update labels to show '(Opsional)'
- Simplify handleSubmit logic (no validation check)
- Keep existing file IDs if no new upload

User Flow:
Before: Edit required imageId and fileId to be present
After: Can update APBDes without files, preserve existing or set to null

Files changed:
- src/app/api/[[...slugs]]/_lib/landing_page/apbdes/updt.ts
- src/app/api/[[...slugs]]/_lib/landing_page/apbdes/index.ts
- src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 15:53:26 +08:00
4821934224 fix(music-player): fix floating icon position shift on hover
Problem:
- Icon bergeser ke bawah saat hover
- transform: 'scale(1.1)' mengganti transform: 'translateY(-80%)'
- CSS transform property di-replace, bukan di-mix

Solution:
- Gabungkan kedua transform dalam satu string
- Hover: 'translateY(-80%) scale(1.1)' - maintain posisi + scale
- Leave: 'translateY(-80%)' - kembali ke posisi semula

Changes:
- onMouseEnter: transform = 'translateY(-80%) scale(1.1)'
- onMouseLeave: transform = 'translateY(-80%)'
- Added 'ease' timing function for smoother transition

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 14:53:38 +08:00
ee39b88b00 feat(music-player): add minimize feature with floating icon and centered controls
Layout Changes:
- Center all control buttons (shuffle, prev, play/pause, next, repeat)
- Center progress bar alongside controls
- Keep volume control + close button on the right
- Song info remains on the left

New Feature - Minimize Player:
- Add isMinimized state to track player visibility
- Replace close button with minimize functionality
- Show floating music icon when minimized (bottom-right corner)
- Click floating icon to restore player bar
- Floating icon has hover scale animation for better UX

UI/UX Improvements:
- Better visual hierarchy with centered controls
- Floating icon uses blue bg with white music icon
- Smooth transitions between states
- Icon scales on hover for interactive feedback
- Persistent player state (song continues playing when minimized)

Files changed:
- src/app/darmasaba/_com/FixedPlayerBar.tsx: Complete redesign

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 14:14:46 +08:00
ce46d3b5f7 fix(music-player): fix repeat button not working due to stale closure
Problem:
- Tombol repeat tidak berfungsi saat lagu selesai
- Event listener 'ended' menggunakan variabel state 'isRepeat' dari closure yang lama
- Meskipun state sudah di-toggle, event listener masih menggunakan nilai lama

Solution:
- Tambahkan isRepeatRef untuk menyimpan nilai terbaru dari isRepeat
- Sync ref dengan state menggunakan useEffect
- Gunakan isRepeatRef.current di event listener 'ended'
- Remove isRepeat dari dependency array useEffect

Files changed:
- src/app/context/MusicContext.tsx: Add isRepeatRef and sync with state

This ensures the repeat functionality works correctly when the song ends.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 13:54:15 +08:00
144ac37e12 fix(public-apbdes): fix realisasi display on public APBDes page
- Fix types.ts transformAPBDesData to map totalRealisasi → realisasi
  - Backend returns totalRealisasi, frontend expects realisasi
  - Add fallback to use item.realisasi if totalRealisasi not available

- Fix grafikRealisasi.tsx to use realisasi field
  - Update Summary component to use i.realisasi || i.totalRealisasi
  - Update total calculation to use realisasi field

- Fix apbDesaTable.tsx to use realisasi field
  - Update total calculation to use item.realisasi

- Fix apbDesaProgress.tsx to use realisasi field
  - Update calcTotal to use item.realisasi with fallback

Root cause: Backend Prisma schema uses 'totalRealisasi' field, but public
page components were expecting 'realisasi' field.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 11:27:28 +08:00
f90477ed63 fix(apbdes-edit): preserve realisasi data when editing APBDes
- Fix backend updt.ts to preserve realisasiItems from old items
  - Load existing items with realisasiItems before delete
  - Re-create realisasiItems for new items based on kode match
  - Recalculate totalRealisasi, selisih, persentase after restore

- Update frontend state to handle realisasi fields
  - Add realisasi, selisih, persentase to ApbdesItemSchema
  - Fix edit.load() to map totalRealisasi → realisasi
  - Fix edit.update() to omit calculated fields when sending to backend

- Update edit page.tsx to display realisasi data
  - Fix load data to use item.totalRealisasi (not item.realisasi)
  - Add Realisasi, Selisih, % columns to items table
  - Update handleAddItem and handleReset to preserve realisasi fields

Root cause: Backend was resetting totalRealisasi=0 for all items on update,
and frontend was accessing wrong field name (realisasi vs totalRealisasi)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-05 11:20:45 +08:00
4a7811e06f Merge pull request 'Fix Tabel Apbdes, & fix muciplayer in background' (#74) from nico/4-mar-26/fix-musik-play-bg into staggingweb
Reviewed-on: #74
2026-03-04 16:41:25 +08:00
f63aaf916d Fix Tabel Apbdes, & fix muciplayer in background 2026-03-04 16:28:06 +08:00
3803c79c95 Merge pull request 'nico/4-mar-26/realiasasi-apbdes' (#73) from nico/4-mar-26/realiasasi-apbdes into staggingweb
Reviewed-on: #73
2026-03-04 12:03:46 +08:00
2d901912ea fix(realisasi): add kode field to RealisasiItem and simplify table display
- Add kode field to RealisasiItem model in Prisma schema
- Update API endpoints (create, update) to accept kode parameter
- Update state management with proper type definitions
- Add kode input field in RealisasiManager component
- Simplify realisasiTable to show flat list (Kode, Uraian, Realisasi, %)
- Remove section grouping and expandable details
- Fix race condition in findUnique.load() with loading guard
- Fix linting errors across multiple files

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-04 11:51:58 +08:00
a791efe76c fix(grafik): sync grafikRealisasi with totalRealisasi field
Fix:
- Change i.realisasi to i.totalRealisasi in Summary component
- Change i.realisasi to i.totalRealisasi in total calculation
- Add fragment wrapper <> to fix Box children type error
- Reorder Total Keseluruhan section to top (before category breakdown)

Now grafik shows correct data from multiple realisasi items.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 16:47:26 +08:00
e9f7bc2043 feat(ui landing): add expandable realisasi detail in RealisasiTable
Features:
- Add expandable rows for each APBDes item
- Show detailed realisasi breakdown per item
- Each realisasi shows:
  * Keterangan/Uraian
  * Jumlah (formatted in Rupiah)
  * Tanggal (formatted date)
- Chevron icon indicator (right/down)
- Click row to expand/collapse
- Hover effect on clickable rows
- Info text: "Klik pada item untuk melihat detail realisasi"

UI Components:
- RealisasiDetail: Component to display list of realisasi
- ItemRow: Expandable row with click handler
- Updated Section: Manage expanded state per item

Styling:
- Gray background for detail section
- Blue color for amount
- Dimmed color for date
- Responsive layout with wrap="nowrap"
- Proper spacing between items

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 16:38:15 +08:00
6712da9ac2 fix(ui): replace Title with Text in Modal title to avoid h5 in h2 nesting
Fix:
- Change Modal title from <Title order={5}> to <Text fz='lg' fw={600}>
- Avoids invalid HTML nesting (<h5> cannot be child of <h2>)
- Maintains same visual appearance

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 16:21:27 +08:00
ac11a9367c fix(api): correct selisih calculation formula
Bug Fix:
- Change selisih formula from: totalRealisasi - anggaran
- To: anggaran - totalRealisasi

Reason:
- Selisih positif = Sisa anggaran (belum digunakan)
- Selisih negatif = Over budget (melebihi anggaran)

Example:
- Anggaran: Rp 30.000.000
- Realisasi: Rp 5.000.000
- Selisih (OLD): 5jt - 30jt = -25jt  Wrong
- Selisih (NEW): 30jt - 5jt = 25jt  Correct (sisa anggaran)

Files Updated:
- create.ts: Fix initial item creation
- updt.ts: Fix item update
- realisasi/create.ts: Fix after adding realisasi
- realisasi/update.ts: Fix after updating realisasi
- realisasi/delete.ts: Fix after deleting realisasi

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 16:16:38 +08:00
67e5ceb254 feat(ui): add Realisasi Manager component for multiple realisasi CRUD
New Component - RealisasiManager:
- Add modal form for create/edit realisasi
- Input fields: Jumlah (Rp), Tanggal, Keterangan/Uraian
- Display list of existing realisasi with edit/delete actions
- Summary cards showing: Anggaran, Total Realisasi, Sisa Anggaran, Persentase
- Color-coded percentage badges (teal ≥100%, blue ≥80%, yellow ≥60%, red <60%)
- Auto-reload data after create/update/delete operations

Features:
- Multiple realisasi per APBDes item
- Each realisasi has its own description (uraian)
- Date picker for realisasi tanggal
- Format currency in IDR (Rupiah)
- Responsive table layout
- Empty state when no realisasi exists

Integration:
- Integrated with existing state.realisasi CRUD functions
- Auto-calculate totalRealisasi and persentase (handled by backend)
- Display realisasi items from API response
- Works with existing APBDes detail page

UI/UX:
- Clean modal design with form validation
- Summary cards with color-coded backgrounds
- Icon indicators for date and currency
- Confirmation dialog before delete
- Loading states during async operations

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 15:41:43 +08:00
65942ac9d2 refactor(create): remove realisasiAwal, simplify to anggaran-only input
Refactoring:
- Remove realisasiAwal field from ItemForm type
- Remove NumberInput for realisasi awal from UI
- Remove realisasiAwal column from preview table
- Simplify state management (no realisasiAwal mapping)
- API create: Create items with totalRealisasi=0 (no auto-create realisasi)

Rationale:
- Cleaner separation: Anggaran dan Realisasi adalah entitas terpisah
- User create item untuk ANGGARAN dulu
- Setelah item dibuat, user bisa add MULTIPLE REALISASI dengan:
  * Uraian yang jelas untuk setiap realisasi
  * Tanggal yang spesifik
  * Keterangan detail
  * Bukti file attachment
- Follows the original schema design more closely

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 15:30:33 +08:00
e0436cc384 feat(create): add realisasi awal input di create page
Features:
- Add realisasiAwal field to ItemForm type
- Add NumberInput for realisasi awal (optional)
- Update table preview to show realisasi awal
- Update state to send realisasiAwal to API
- Update API create to handle realisasiAwal:
  * Create APBDesItem with totalRealisasi = realisasiAwal
  * Auto-create first RealisasiItem if realisasiAwal > 0
  * Auto-calculate selisih and persentase

UX Improvements:
- User can input initial realization during create
- Optional field with clear label and description
- Auto-calculation of percentages on backend
- Single transaction for item + first realisasi

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 15:13:58 +08:00
63682e47b6 feat(state): update APBDes state management for multiple realisasi
State Changes:
- Update ApbdesItemSchema: Remove realisasi, selisih, persentase fields
- Add RealisasiItemSchema for realisasi CRUD operations
- Update normalizeItem: Remove manual calculations (backend handles it)
- Update edit.load: Map items without realisasi fields
- Add realisasi state: create, update, delete functions

UI Changes:
- Update create/page.tsx: Remove realisasi input field and column
- Update edit/page.tsx: Remove realisasi input field and column
- Update ItemForm type: Remove realisasi property
- Simplify forms to only input anggaran, realisasi added separately

Features:
- Support for multiple realisasi per item
- Realisasi CRUD via dedicated state functions
- Auto-reload findUnique after realisasi operations
- Backend auto-calculates totalRealisasi, selisih, persentase

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 14:55:06 +08:00
f4705690a9 feat(api): implement multiple realisasi for APBDes
Schema Changes:
- Add RealisasiItem model for multiple realizations per APBDesItem
- Replace realisasi field with totalRealisasi (auto-calculated sum)
- Add selisih and persentase as auto-calculated fields
- Cascade delete realisasi when item is deleted

API Changes:
- Update index.ts: Add realisasi CRUD endpoints
  - POST /:itemId/realisasi - Create realisasi
  - PUT /realisasi/:realisasiId - Update realisasi
  - DELETE /realisasi/:realisasiId - Delete realisasi
- Update create.ts: Auto-calculate totalRealisasi=0, selisih, persentase
- Update updt.ts: Reset calculations when items updated
- Update findUnique.ts: Include realisasiItems in response
- Update findMany.ts: Include realisasiItems in response
- Remove realisasi field from ApbdesItemSchema

New Files:
- realisasi/create.ts - Create realisasi with auto-calculation
- realisasi/update.ts - Update realisasi with recalculation
- realisasi/delete.ts - Soft delete with recalculation

Features:
- Auto-calculate totalRealisasi from sum of all realisasiItems
- Auto-calculate selisih = totalRealisasi - anggaran
- Auto-calculate persentase = (totalRealisasi / anggaran) * 100
- Support for bukti file attachment
- Support for keterangan (notes) per realisasi
- Soft delete support for audit trail

UI Updates:
- Update admin detail page to use totalRealisasi instead of realisasi
- Update landing page realisasiTable to use totalRealisasi

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 14:45:21 +08:00
239771a714 fix(apbdes): improve UI components and styling
- Update Apbdes component with better conditional rendering
- Enhance grafikRealisasi with improved percentage display
- Refine color coding and feedback messages
- Optimize layout and spacing for better UX

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 14:15:27 +08:00
03451195c8 feat(apbdes) grafik: add detailed percentage comparison
- Display percentage value prominently next to each category title
- Add formatted currency (Rupiah) for better readability
- Color-coded progress bars based on achievement level:
  * Teal: ≥100% (target tercapai)
  * Blue: ≥80% (baik)
  * Yellow: ≥60% (cukup)
  * Red: <60% (perlu perhatian)
- Add contextual feedback messages based on percentage:
  * ✓ Achievement message for 100%
  *  Positive message for 80-99%
  * ⚠️ Warning messages for <80%
- Add TOTAL KESELURUHAN summary section at the top
- Add emoji icons for better visual distinction (💰 💸 📊)
- Animated progress bars for <100% achievement

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 13:58:52 +08:00
597af7e716 fix(apbdes) landing page: fix APBDes component not displaying on darmasaba page
- Restore Apbdes component with full functionality (fetch data, year selector, tables, charts)
- Fix realisasiTable.tsx: add missing items variable
- Fix grafikRealisasi.tsx: dynamic year title instead of hardcoded 2026
- Add eslint-disable comments for TypeScript any types
- Remove unused imports in paguTable.tsx
- Integrate PaguTable, RealisasiTable, GrafikRealisasi into main Apbdes component
- Component now fetches data from Valtio state and displays 3 tables + charts

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 12:51:53 +08:00
0a8a026b94 fix(apbdes): integrate new APBDes API with admin UI
- Update API schema to support name, deskripsi, and jumlah fields
- Enhance state management with additional form fields
- Add input fields for name, description, and total amount in create/edit pages
- Display description and total amount in detail page
- Fix APBDes component order in landing page
- Update TypeScript types and Prisma schema integration

API Changes:
- POST /api/landingpage/apbdes/create: Added optional fields (name, deskripsi, jumlah)
- PUT /api/landingpage/apbdes/🆔 Added optional fields (name, deskripsi, jumlah)

Admin UI Changes:
- create/page.tsx: Add TextInput for name, deskripsi, and jumlah
- edit/page.tsx: Add TextInput for name, deskripsi, and jumlah; improve reset functionality
- [id]/page.tsx: Display deskripsi and jumlah if available
- page.tsx: Minor formatting fix
- _state/apbdes.ts: Update Zod schema and default form with new fields

Landing Page:
- Move Apbdes component to top of stack for better visibility

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-03 10:56:30 +08:00
a5bd91b580 feat(berita): add multiple images gallery and YouTube video support
- Update schema: add images relation list and linkVideo field
- API: support multiple image upload and YouTube link in create/update
- Admin create page: add gallery upload (max 10) and YouTube embed preview
- Admin edit page: manage existing/new gallery images and YouTube link
- Admin detail page: display gallery grid and YouTube video embed
- Public detail page: show gallery images and YouTube video with responsive layout
- State: add imageIds[] and linkVideo fields with proper type handling
- Music player: fix seek functionality and ESLint warnings

Breaking changes:
- Prisma schema updated - requires migration
- API create/update endpoints now expect imageIds array and linkVideo string

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 16:06:53 +08:00
bipproduction
7368a367f4 chore: tambah publish.yml workflow_dispatch ke main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 15:38:49 +08:00
bipproduction
ed664d5b10 deploy docker ghcr 2026-03-02 15:09:46 +08:00
ae3187804e Notes slider musik belum berfungsi 2026-03-02 14:28:20 +08:00
bipproduction
0ba30aa5b2 tambahan 2026-03-02 13:20:05 +08:00
bipproduction
790d6535e5 fix: perbaiki Dockerfile - lockfile, bunfig.toml, dan path build output
- Ganti bun.lock → bun.lockb (nama file yang benar)
- Hapus bunfig.toml dari COPY (file tidak ada)
- Ganti /app/dist → /app/.next (output Next.js build)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 13:15:54 +08:00
bipproduction
46ce16ae97 tambahanya 2026-03-02 12:58:42 +08:00
91e32f3f1c fix(musik): fix seek slider reset ke 0 - root cause: useEffect dependency
ROOT CAUSE:
- filteredMusik di-calculate ulang setiap render (.filter() tanpa memoization)
- currentSong = filteredMusik[currentSongIndex] → object reference baru setiap render
- useEffect dependency [currentSong, currentSongIndex] trigger setiap render
- useEffect reset setCurrentTime(0) → slider kembali ke awal

FIX:
1. useMemo untuk filteredMusik - mencegah re-calculate setiap render
2. useEffect dependency [currentSong?.id, currentSongIndex] - hanya trigger saat lagu benar-benar berubah
3. Hapus semua debug console.log yang tidak diperlukan
4. Simplifikasi seekTo function

File Changed:
- src/app/darmasaba/(pages)/musik/musik-desa/page.tsx
- src/app/darmasaba/(pages)/musik/lib/seek.ts

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 12:03:26 +08:00
4d03908f23 feat(musik): tambahkan extensive debug logging untuk tracking seek issue
- Tambah key stabil pada audio element untuk mencegah remount
- Log di seekTo: before/after currentTime
- Log di onTimeUpdate: currentTime, rounded, isSeeking
- Log di onChangeEnd slider: value, seekTime
- Log di useEffect song change: currentSong, currentSongIndex
- Log di skipBack/skipForward: index perubahan lagu

Purpose: Track urutan eksekusi dan identifikasi race condition atau re-render yang tidak diinginkan

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 11:58:40 +08:00
0563f9664f fix(musik): perbaiki timing dan rounding pada seek slider
- Gunakan durasi dari database sebagai acuan utama (bukan dari audio metadata)
- Ganti Math.floor dengan Math.round untuk smoothing currentTime
- Tambahkan validasi seek time: Math.min(Math.max(0, v), duration)
- Tambahkan debug logging untuk tracking seek behavior
- Hapus override duration di onLoadedMetadata untuk menghindari konflik

Root cause:
- Duration dari database (string 'MM:SS' → seconds) berbeda dengan audio.duration (float)
- Math.floor menyebabkan lompatan kasar dan kehilangan presisi
- onLoadedMetadata override duration dengan audio.duration yang tidak exact

Fix:
- Database duration = source of truth
- Math.round untuk smoothing tanpa kehilangan presisi
- Validasi bounds untuk mencegah seek negatif atau melebihi durasi

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 11:53:36 +08:00
961cc32057 fix(musik): slider seek sekarang berfungsi dengan benar
- Fix slider seek reset ke detik awal saat digeser
- Tambahkan isSeeking state untuk mencegah onTimeUpdate mereset posisi slider
- Implementasi pattern preview/commit untuk seek:
  - onChange: update UI state saja (preview)
  - onChangeEnd: commit ke audio player (commit)
- Update seekTo function untuk support optional setCurrentTime callback
- Terapkan fix ke kedua slider (Sedang Diputar dan bottom player)

Bug: Slider seek langsung kembali ke posisi awal saat digeser karena:
1. onTimeUpdate terus menerus update currentTime state
2. seekTo tidak update React state setelah set audio.currentTime
3. Tidak ada isSeeking flag untuk block onTimeUpdate saat user sedang seek

Fix:
1. Set isSeeking=true saat onChange, false saat onChangeEnd
2. onTimeUpdate check isSeeking sebelum update state
3. seekTo sekarang juga update state via callback optional

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 11:47:05 +08:00
fe7672e09f refactor(musik): integrate music player library functions and fix build errors
- Integrate togglePlayPause, getNextIndex, getPrevIndex, handleRepeatOrNext, seekTo, toggleShuffle, setAudioVolume, toggleMute library functions
- Fix ESLint warnings: remove unused eslint-disable, add missing useEffect dependencies
- Fix ESLint error in useMusicPlayer.ts togglePlayPause function
- Add force-dynamic export to root layout to prevent prerendering errors
- Improve seek slider with preview/commit functionality
- Add isSeeking state to prevent UI flickering during seek

Fixes: Build PageNotFoundError for admin/darmasaba pages

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 11:41:14 +08:00
341ff5779f Fix Durasi Musik Di Tampilan User 2026-02-27 11:52:18 +08:00
69f7b4c162 feat: integrate musik desa page with API and improve audio player
- Fetch musik data from /api/desa/musik/find-many endpoint
- Filter only active musik (isActive: true)
- Add search functionality by title, artist, and genre
- Implement real audio playback with HTML5 audio element
- Add play/pause, next/previous, shuffle, repeat controls
- Add progress bar with seek functionality
- Add volume control with mute toggle
- Auto-play next song when current song ends
- Add loading and empty states
- Use cover image and audio file from database
- Fix skip back/forward button handlers

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-26 22:24:25 +08:00
409ad4f1a2 Fix Login KodeOtp WA 2026-02-26 22:10:28 +08:00
55ea3c473a add menu musik 2026-02-26 21:32:33 +08:00
0160fa636d Merge pull request 'Fix Login KodeOtp WA' (#72) from nico/26-feb-26/fix-auth-wa-login into staggingweb
Reviewed-on: #72
2026-02-26 14:14:29 +08:00
a152eaf984 Fix Login KodeOtp WA 2026-02-26 14:12:54 +08:00
3684e83187 Merge pull request 'Fix CORS config for staging environment' (#71) from nico/25-feb-26 into staggingweb
Reviewed-on: #71
2026-02-25 23:15:18 +08:00
223b85a714 Fix CORS config for staging environment
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 22:55:36 +08:00
77c54b5c8a Merge pull request 'nico/25-feb-26' (#70) from nico/25-feb-26 into staggingweb
Reviewed-on: #70
2026-02-25 21:34:28 +08:00
f1729151b3 Fix themeTokens light mode status colors
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 21:24:39 +08:00
8e8c133eea Fix eror build 2026-02-25 21:19:56 +08:00
1e7acac193 Fix eror build 2026-02-25 21:18:26 +08:00
bb80b0ecc1 Merge pull request 'fix/admin/menu-desa/berita' (#69) from fix/admin/menu-desa/berita into staggingweb
Reviewed-on: #69
2026-02-25 16:27:35 +08:00
42dcbcfb22 fix-admin-menu-desa-berita 2026-02-25 16:25:59 +08:00
22de1aa1f3 fix-admin-menu-desa-potensi 2026-02-25 15:41:01 +08:00
b1d28a8322 fix-admin-menu-desa-profile 2026-02-25 15:25:51 +08:00
b86a3a85c3 fix: force default light mode for public pages and admin
- Set defaultColorScheme='light' in root MantineProvider
- Change darkModeStore default from system preference to false (light)
- Add MantineProvider with light theme to darmasaba/layout.tsx
- Remove dark mode dependency from ModuleView component
- Prevent system color scheme from affecting initial page load

This ensures consistent light mode on first visit for both
public pages and admin panel, regardless of OS settings.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 10:45:27 +08:00
fd63bb0fd4 feat: implement dark mode support & fix Prisma schema validation
- Add dark mode toggle component in admin header
- Integrate dark mode store across admin layout and components
- Add unified typography and surface components for consistent theming
- Implement smooth transitions for dark/light mode switching
- Fix Prisma schema: remove @default(null) from DateTime? fields
- Update form validation for inovasi, lingkungan, and pendidikan modules
- Add form validation and improve UX across multiple admin pages

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-25 10:41:48 +08:00
f2c9a922a6 fix(profil-module): QC improvements based on QC-PROFIL-MODULE.md
- Fix fetch method inconsistency (convert to ApiFetch)
  - programInovasi: findUnique, delete, update methods
  - mediaSosial: findUnique, delete, update methods
- Add loading state to findUnique operations
- Fix iconUrl validation (make optional instead of required)
- Add DOMPurify for HTML sanitization (XSS protection)
  - program-inovasi page.tsx (list & detail)
- Remove console.log in production (use dev-only logging)
- Install dompurify and @types/dompurify

Security: Prevent XSS attacks by sanitizing HTML content
Consistency: Use ApiFetch for all API operations
UX: Proper loading states for better user feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 15:11:00 +08:00
92b24440fe fix: Quality Control improvements & bug fixes
- APBDes: Fix edit form original data tracking (imageId, fileId)
- APBDes: Update formula consistency in state
- PPID modules: Various UI improvements and bug fixes
- PPID Profil: Preview and edit page improvements
- PPID Dasar Hukum: Page structure improvements
- PPID Visi Misi: Page structure improvements
- PPID Struktur: Posisi organisasi page improvements
- PPID Daftar Informasi: Edit page improvements
- Auth login: Route improvements
- Update dependencies (package.json, bun.lockb)
- Update seed data
- Update .gitignore

QC Reports added:
- QC-APBDES-MODULE.md
- QC-PROFIL-MODULE.md
- QC-SDGS-DESA.md
- QC-DESA-ANTI-KORUPSI.md
- QC-PRESTASI-DESA-MODULE.md
- QC-PPID-PROFIL-MODULE.md
- QC-STRUKTUR-PPID-MODULE.md
- QC-VISI-MISI-PPID-MODULE.md
- QC-DASAR-HUKUM-PPID-MODULE.md
- QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md
- QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md
- QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md
- QC-IKM-MODULE.md

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 14:38:28 +08:00
f0558aa0d0 feat: implement dark mode support for admin layout and components
- Add dark mode toggle component in admin header
- Integrate dark mode store across admin layout and child components
- Update header, judulList, and judulListTab components with theme tokens
- Add unified typography components for consistent theming
- Implement smooth transitions for dark/light mode switching
- Add mounted state to prevent hydration mismatches
- Style navbar with dark mode aware colors and hover states
- Update button styles with gradient effects for both themes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 10:48:00 +08:00
8132609ccb feat: add form validation for inovasi, lingkungan, and pendidikan modules
- Added isFormValid() and isHtmlEmpty() helper functions for form validation
- Disabled submit buttons when required fields are empty across multiple admin and public pages
- Applied consistent validation pattern for creating and editing records
- Commented out WhatsApp OTP sending in login route for debugging/testing
- Fixed path in NavbarMainMenu tooltip action
2026-02-20 15:08:41 +08:00
1ddc1d7eac feat: add form validation for ekonomi module admin pages
- Added isFormValid() and isHtmlEmpty() helper functions
- Disabled submit buttons when required fields are empty
- Applied consistent validation pattern across all create/edit pages
- Validated fields: nama, deskripsi, tahun, jumlah, value, icon, statistik, and more
- Edit pages allow existing data, create pages require all fields

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-18 16:13:20 +08:00
aa354992e7 feat: add form validation for keamanan module admin pages
- Added isFormValid() and isHtmlEmpty() helper functions
- Disabled submit buttons when required fields are empty
- Applied consistent validation pattern across all create/edit pages
- Validated fields: judul, deskripsi, lokasi, tanggal, status, kronologi, penanganan, link video, and image uploads
- Edit pages allow existing images, create pages require new uploads

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-18 11:22:30 +08:00
d43b07c2ef feat: add form validation for kesehatan module admin pages
- Added isFormValid() and isHtmlEmpty() helper functions
- Disabled submit buttons when required fields are empty
- Applied consistent validation pattern across all create/edit pages
- Validated fields: name, address, dates, descriptions, and image uploads
- Edit pages allow existing images, create pages require new uploads

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-18 10:51:10 +08:00
9678e6979b feat: implement form validation for empty fields across multiple admin pages
- Added validation to disable submit buttons when required fields are empty
- Implemented consistent validation patterns across various admin pages
- Applied validation to create and edit forms for berita, gallery, layanan, penghargaan, pengumuman, potensi, profil-desa, and ppid sections
- Used helper functions to check for empty HTML content in editor fields
- Ensured submit buttons are disabled until all required fields are filled

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-14 14:17:17 +08:00
b35874b120 feat: add form validation and disable submit buttons when fields are empty
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-11 17:04:55 +08:00
1b59d6bf09 Merge pull request 'Fix Coba lagi image stagging' (#66) from nico/5-feb-26-2 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/66
2026-02-05 14:31:53 +08:00
b69df2454e Fix Coba lagi image stagging 2026-02-05 14:29:44 +08:00
eb1ad54db6 Merge pull request 'Seed create' (#65) from nico/5-feb-26-1 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/65
2026-02-05 14:04:09 +08:00
df198c320a Seed create 2026-02-05 14:03:40 +08:00
21ec3ad1c1 Merge pull request 'nico / 5-feb-26 (3)' (#64) from nico/5-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/64
2026-02-05 12:35:21 +08:00
f550e29a75 Fix ke 3 Env dan seafile 2026-02-05 12:34:31 +08:00
3a115908c4 Merge pull request 'nico / 5-feb-26(2)' (#63) from nico/5-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/63
2026-02-05 12:07:17 +08:00
bb7384f1e5 Fix Seed Profile PPID
Fix Seed Visi Misi Desa Profile Desa
2026-02-05 12:06:20 +08:00
5ff791642c Merge pull request 'nico / 5-feb-26' (#62) from nico/5-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/62
2026-02-05 11:14:42 +08:00
df154806f7 Fix image di seafile sudah tidak pakai token tapi by folder di seafile
Kasih console di page profil ppid & visi misi di Profile Desa
2026-02-05 11:10:30 +08:00
b803c7a90c Merge pull request 'nico / 4-feb-26' (#61) from nico/4-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/61
2026-02-04 17:10:27 +08:00
25000d0b0f PPID > Profile PPID
Desa >  Profile Visi Misi Desa
Keamanan >  Pencegahan Kriminalitas  Grid Kiri  Datanya  Mepet
Keamanan >  Laporan  Publik
Ekonomi  >  Sektor Unggulan Desa Coba tampilin 3  Aja
2026-02-04 16:59:49 +08:00
fb2fe67c23 Merge pull request 'Nico / 4-Feb-2026' (#60) from nico/4-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/60
2026-02-04 11:49:22 +08:00
bbd52fb6f5 Fix Jam Operasional Kantor Desa
Fix Agar Token Seafile ga expired cuma 1 hari
2026-02-04 11:47:56 +08:00
51460558d4 Merge pull request 'Seeder Menu Lingkungan dan Pendidikan' (#59) from nico/3-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/59
2026-02-03 17:12:03 +08:00
358ff14efe Seeder Menu Lingkungan dan Pendidikan
Fix Jam Operasional Kantor Desa Darmasaba
2026-02-03 16:53:15 +08:00
d105ceeb6b Merge pull request 'nico/2-feb-26' (#58) from nico/2-feb-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/58
2026-02-02 17:32:10 +08:00
6c36a15290 Seed Menu Ekonomi
Seed MEnu Inovasi
Sisa 2 Menu
2026-02-02 17:31:27 +08:00
da585dde99 seed kesheatan
seed keamanan
2026-02-02 15:05:53 +08:00
c865aee766 Merge pull request 'Fix Eror Code get_images.ts (1)' (#57) from nico/30-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/57
2026-01-30 17:16:42 +08:00
8afbaabd91 Fix Eror Code get_images.ts (1) 2026-01-30 17:15:39 +08:00
273dfdfd09 Merge pull request 'Fix Seeder Image, Menu Landing Page - Desa' (#56) from nico/30-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/56
2026-01-30 15:57:16 +08:00
f0425cfc47 Fix Seeder Image, Menu Landing Page - Desa 2026-01-30 15:55:05 +08:00
1d1d8e50dc Merge pull request 'Fix Seed Image 27 Jan' (#55) from nico/27-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/55
2026-01-27 10:51:22 +08:00
c2ad515366 Fix Seed Image 27 Jan 2026-01-27 10:50:33 +08:00
092afe67d2 Merge pull request 'Seed Pendidikan' (#54) from nico/23-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/54
2026-01-23 16:52:25 +08:00
d9ce4aac6d Seed Pendidikan 2026-01-23 16:51:35 +08:00
2d9170705d Merge pull request 'Fix seeder statistik kemiskinan' (#53) from nico/21-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/53
2026-01-21 14:27:32 +08:00
3fcfec22fb Fix seeder statistik kemiskinan 2026-01-21 14:25:20 +08:00
fdf9a951a4 Merge pull request 'Fix uploads -1' (#52) from nico/21-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/52
2026-01-21 14:10:42 +08:00
6ca1e032a6 Fix uploads -1 2026-01-21 14:09:27 +08:00
ca74029688 Merge pull request 'nico/21-jan-26' (#51) from nico/21-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/51
2026-01-21 12:10:18 +08:00
78c55a8a71 Seed data menu ekonomi - lingkungan
fix iconmap
2026-01-21 12:07:52 +08:00
1a8fc1a670 Merge pull request 'nico/17-jan-26' (#49) from nico/17-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/49
> Add Layanan Polsek submenu polsek terdekat
> Seeder menu keamanan -> menu ekonomi submenu : demografi pekerjaan, junlah pengangguran, lowongan kerja lokal, pasar desa, program kemiskinan, sektor unggulan, struktur organisasi
2026-01-17 10:36:11 +08:00
17b20e0d40 Add Layanan Polsek submenu polsek terdekat
Seeder menu keamanan -> menu ekonomi submenu : demografi pekerjaan, junlah pengangguran, lowongan kerja lokal, pasar desa, program kemiskinan, sektor unggulan, struktur organisasi
2026-01-17 10:32:48 +08:00
184854d273 Fix Table Admin Preview Desktop
Seeder Menu Kesehatan
2026-01-13 11:45:55 +08:00
903dc74cca Seeder data Landing Page - Desa 2026-01-12 14:03:44 +08:00
503da91ce6 Tambah seeder di bagian landing page 2026-01-06 17:54:21 +08:00
19235f0791 Merge pull request 'Fix All Search Admin' (#48) from nico/5-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/48
2026-01-05 17:12:29 +08:00
daaed8089b Fix All Search Admin 2026-01-05 17:11:30 +08:00
61de7d8d33 Merge pull request 'Fix QC Kak Inno Mobile Done' (#47) from nico/1-jan-26 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/47
2026-01-02 16:42:08 +08:00
f436aa2ef0 Fix QC Kak Inno Mobile Done
FIx QC Kak Ayu Mobile Admin Done
Fix Tampilan Admin Mobile Device All Menu Done
2026-01-02 16:33:15 +08:00
8fb85ce56c Merge pull request 'Fix QC Kak Inno 23 Des' (#46) from nico/24-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/46
2025-12-24 14:38:12 +08:00
50bc54ceca Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Menu Inovasi
2025-12-24 14:36:51 +08:00
1f98b6993d Merge pull request 'nico/23-des-25' (#45) from nico/23-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/45
2025-12-23 17:19:57 +08:00
f0f201c853 Fix QC Kak Inno 22 Des
Fix QC Kak Ayu 22 Des
Fix Tampilan Admin Mobile Device Menu Ekonomi
Fix Search -> useDebounced Menu Ekonomi
2025-12-23 17:18:36 +08:00
29065cb3e2 Fix QC Kak Inno 19 Des
Fix QC Kak Ayu 19 Des
Fix Tampilan Admin Mobile Menu Keamanan
Fix Search Debounce Menu Keamanan
2025-12-22 15:10:25 +08:00
f3a10d63d1 Merge pull request 'nico/19-des-25' (#44) from nico/19-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/44
2025-12-19 15:44:53 +08:00
bf20cd55e8 Fix QC Kak Inno 18 Des
Fix UI Admin Menu Kesehatan
Fix Search : Sudah diberi useDebounced menu Kesehatan
2025-12-19 15:43:55 +08:00
af60bcd6fc Fix QC Kak Inno Tgl 17
Fix QC Kak Ayu Tgl 17
Fix UI Admin Mobile Menu PPID
Search Admin Menu Landing Page & Menu PPID
2025-12-18 17:25:22 +08:00
7a42bec63b Merge pull request 'nico/17-des-25' (#43) from nico/17-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/43
2025-12-17 17:39:29 +08:00
dc8793e3ae Fix QC Kak Inno 16 Des
Fix QC Kak Ayu 16 Des
FIx UI Admin Mobile Menu PPID
Fix Search Admin Menu Landing Page & Menu PPID
2025-12-17 17:37:58 +08:00
44c421129e Merge pull request 'nico/16-des-25' (#42) from nico/16-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/42
2025-12-16 16:38:42 +08:00
c8484357cb Fix QC Kak Ayu 15 Des
Fix QC Kak Inno 15 Des
Fix UI User Font Size, Font Weight, Line Height
Fix UI Admin Font Size, Font Weight, Line Height & UI Mobile
2025-12-16 16:37:17 +08:00
342e9bbc65 Fix QC Kak Ayu Tgl 12
Fix QC Kak Ino Tgl 12
Fix UI Mobile Menu Keamanan
Fix UI Mobile Admin Menu Landing Page
2025-12-16 10:19:15 +08:00
ddff427926 Merge pull request 'nico/12-des-25' (#41) from nico/12-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/41
2025-12-12 17:07:31 +08:00
f6f77d9e35 Fix QC Kak Inno Tgl 11 Des
Fix QC Kak Ayu Tgl 11 Des
Fix font style {font size, color, line height} menu kesehatan
2025-12-12 17:06:33 +08:00
a00481152c Fix Konsisten teks di tampilan mobile dan desktop
Fix QC Kak Inno tgl 10 Des
Fix QC Kak Ayu tgl 10 Des
2025-12-11 17:58:03 +08:00
00c8caade4 Merge pull request 'nico/10-des-25' (#40) from nico/10-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/40
2025-12-10 17:45:16 +08:00
242ea86f77 Fix konsisten font, menu landing page & PPID 2025-12-10 17:44:31 +08:00
99c2c9c6d7 Fix semua tulisan profile jadi profil, mulai dari navbar, dan route 2025-12-10 14:16:15 +08:00
0209f49449 Merge pull request 'nico/9-des-25' (#39) from nico/9-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/39
2025-12-09 17:40:16 +08:00
ac2fc1a705 Fix QC Kak Inno 8 Des
Fix QC Kak Ayu 8 Des
Fix QC Pak Jun 8 Des
2025-12-09 17:27:23 +08:00
9dbe172165 Fix QC Kak Inno Tgl 4 & 5 Desember
Fix QC Kak Ayu Tgl 4 & 5 Desember
Fix QC Pak Jun Tgl 5 Desember
2025-12-09 12:00:27 +08:00
cc318d4d54 Fix QC Kak Inno Tgl 4 & 5 Desember
Fix QC Kak Ayu Tgl 4 & 5 Desember
Fix QC Pak Jun Tgl 5 Desember
2025-12-09 10:28:17 +08:00
dcb8017594 Fix undefined ke detail berita terbaru 2025-12-05 17:42:04 +08:00
344c6ada6d Merge pull request 'nico/5-des-25' (#38) from nico/5-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/38
2025-12-05 14:32:12 +08:00
ec3ad12531 Fix Notifikasi saat ada berita atau pengumuman baru, notifikasi baru muncul. Ga setiap masuk landing page ada notifikasi 2025-12-05 14:30:53 +08:00
dad44c0537 Fix Menu Gallery : Gallery Foto
Fix detail berita
2025-12-05 10:56:03 +08:00
11acd04419 Merge pull request 'Fix Error Build Staging' (#37) from nico/3-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/37
2025-12-04 11:59:43 +08:00
867dce42f0 Fix Error Build Staging 2025-12-04 11:58:47 +08:00
8d49213b68 Merge pull request 'Menambahkan menu dokter dan tenaga medis, admin bisa create, edit, delet dokter' (#36) from nico/3-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/36
2025-12-03 17:57:33 +08:00
7bb17ddf22 Menambahkan menu dokter dan tenaga medis, admin bisa create, edit, delet dokter
Menambahkan menu tarif dan layanan, admin bisa create, edit, delete tarif dan layanan
Dibagian fasilitas kesehatan admin bisa multiselect bagian dokter dan tarif layanan
Di tampilan user juga sudah disesuaikan dengan datanya bisa muncul lebih dari 1 dokter dan 1 tarif layanan
2025-12-03 17:24:03 +08:00
96911e3cf1 Merge pull request 'Fix UI Sosial Media Landing Page in User' (#35) from nico/2-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/35
2025-12-02 16:46:49 +08:00
a4069d3cba Fix UI Sosial Media Landing Page in User 2025-12-02 16:45:55 +08:00
9950c28b9b Merge pull request 'Fix menu admin landing page, submenu sosial media' (#34) from nico/2-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/34
2025-12-02 16:09:19 +08:00
ffe5e6dd9f Fix menu admin landing page, submenu sosial media 2025-12-02 16:06:14 +08:00
fa0f3538d1 Merge pull request 'Tambahan filter data sesuai tahun, di landing page apbdes' (#33) from nico/1-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/33
2025-12-01 17:12:34 +08:00
dcf195f54f Tambahan filter data sesuai tahun, di landing page apbdes 2025-12-01 17:11:24 +08:00
2778f53aff Merge pull request 'Tambah Term of Service di Registrasi' (#32) from nico/1-des-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/32
2025-12-01 14:02:11 +08:00
c03a6b3aed Tambah Term of Service di Registrasi 2025-12-01 14:01:03 +08:00
37ac91d4f4 Push Conflict 2025-12-01 13:58:27 +08:00
217f4a9a3b Tambah Term of Service di Registrasi 2025-12-01 13:53:39 +08:00
5d6a7437ed Merge branch 'nico/1-des-25' into staggingweb 2025-12-01 13:52:11 +08:00
1bb9f239db Tambah Term of Service di Registrasi 2025-12-01 13:50:25 +08:00
a213ff7d37 Tambah Term of Service di Registrasi 2025-12-01 12:10:22 +08:00
752a6cabee Merge pull request 'Meta-data' (#29) from nico/1-des-25-1 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/29
2025-12-01 10:22:10 +08:00
134ddc6154 Meta-data 2025-12-01 10:21:00 +08:00
28979c6b49 Merge pull request 'Test Google Insight' (#28) from nico/28-nov-25-1 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/28
2025-11-28 19:24:18 +08:00
b2066caa13 Test Google Insight 2025-11-28 17:54:18 +08:00
023c77d636 Merge pull request 'Add <meta charSet=utf-8 />' (#27) from nico/28-nov-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/27
2025-11-28 17:05:14 +08:00
9bf3ec72cf Add <meta charSet=utf-8 /> 2025-11-28 17:04:35 +08:00
f359f5b1ce Merge pull request 'nico/28-nov-25' (#26) from nico/28-nov-25 into staggingweb
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/26
2025-11-28 15:39:08 +08:00
1c1e8fb190 fix ganti role, user menu access ikut ke create fix 2025-11-28 15:38:07 +08:00
54f83da3b8 fix ganti role, user menu access ikut ke create 2025-11-28 15:35:21 +08:00
f8985c550f Merge branch 'nico/28-nov-25' of https://wibugit.wibudev.com/wibu/desa-darmasaba into nico/28-nov-25 2025-11-28 15:32:19 +08:00
e3d909e760 fix ganti role, user menu access ikut ke create 2025-11-28 15:31:10 +08:00
16a8df50c1 Merge pull request 'staggingweb' (#25) from staggingweb into nico/28-nov-25
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/25
2025-11-28 15:04:30 +08:00
0018bdc251 Fix Ganti Role, ganti role menunya sudah menyesuaikan 2025-11-28 15:03:18 +08:00
83fb39a957 Fix Ganti Role, ganti role menunya sudah menyesuaikan 2025-11-28 15:00:09 +08:00
7238692dd0 Push WebDesaDarmasabaSatging 2025-11-28 13:56:40 +08:00
8b50139d79 Push Staging 2025-11-28 12:03:07 +08:00
066180fc0e Fix registrasi, waitong-room, & tampilan layout sesuai id 2025-11-28 11:13:20 +08:00
67f29aabef Balik ke awal 2025-11-27 18:53:33 +08:00
dbf7c34228 Fix eror registrasi 2 2025-11-27 17:08:17 +08:00
036fc86fed Fix eror registrasi 1 2025-11-27 16:45:47 +08:00
2cecec733e Tambah cookies di bagian verifikasi, agar kedeteksi user sudah regis apa belom 2025-11-27 14:46:49 +08:00
c64a2e5457 Fix Seeder User, dan role 2025-11-27 12:18:15 +08:00
757911d7dd Fix Seeder 2025-11-26 15:32:49 +08:00
54232e4465 Menambahkan seed user
Fix Infinite reload di page ikm dan landing page
2025-11-26 15:01:34 +08:00
29a9a59bca saat tampilan user sudah diubah dan login ulan sudah menyesuaikan untuk menunya 2025-11-26 11:01:23 +08:00
2fb3666e57 User yang sudah registrasi sudah langsung diarahkan ke layout sesuai dengan roleIdnya
Superadmin sudah bisa menambah atau mengurangkan menu pad user yang diinginkan
Next-------------------------------
Ada bug saat tampilan menu sudah di edit superamin berhasil namun saat user logout tampilan menunya balik ke sebelumnya
2025-11-26 10:14:05 +08:00
e30b27f7a4 Fix Search 2025-11-25 17:30:41 +08:00
e941ed3893 Sudah fix menunya, superadmin bisa memilihkan menu untuk user 2025-11-25 16:21:15 +08:00
ace5aff1b6 Fix Kondisi Verify Otp Registrasi dan Login
Next mau fix eror saat user sudah terdaftar tetapi di redirect ke login, seharusnya redirect sesuai roleIdnya
2025-11-25 15:03:27 +08:00
716db0adca Fix Middleware
Fix Layout sesuai role, dan superadmin bisa menambahkan menu ke user jika diperlukan
Penambahan menu di user & role : menu access
2025-11-24 16:02:13 +08:00
a291bdfb51 Tampilan Layout sudah sesuai dengan roleIdnya
Sudah sessionnya
Sudah disesuaikan juga semisal superadmin ngubah role admin, maka admin tersebut akan logOut dan diarahkan ke halama login
sudah bisa logOut
2025-11-21 17:26:38 +08:00
0dff8f3254 Nico 20 Nov 25
Dibagian layout admin sudah disesuaikan dengan rolenya : supadmin, admin desa, admin kesehatan, admin pendidikan
Fix API User & Role Admin
2025-11-20 16:42:36 +08:00
78b8aa74cd Saat user baru registrasi maka akan diarahkan ke page waiting-room dan menunggu validasi admin 2025-11-20 14:07:26 +08:00
a0537810e8 Login, Register, Verifkasi Code Admin V1 2025-11-20 02:42:39 +08:00
b3c169a2d4 Fix create admin & progress bar persentase 2025-11-18 17:23:38 +08:00
2608a5ffdd Fix Edit di Admin APbdes, dan fix data real di apbdes user 2025-11-18 16:26:09 +08:00
6c32f3ebdb Fix Route APBdes 2025-11-18 14:27:53 +08:00
0feeb4de93 Fix SDGs Desa Barchart sudah responsive, tabel dan bar progress di menu apbdes sudah sesuai dengan data 2025-11-18 11:56:16 +08:00
9622eb5a9a Fix QC Kak Inno Admin, Fix QC Keano UI User, Fix QC Pak jun tabel apbdes 2025-11-12 17:42:31 +08:00
417a8937f5 Semua tooltips di admin sudah dihilangkan 2025-11-07 14:38:32 +08:00
db8909b9ed Fix Text to Speech Menu Landing Page && Add barchart Landing Page APBDes 2025-11-06 11:35:04 +08:00
f66a46f645 QC ToolTip Admin Keano Masih di Menu Landing Page - Keamanan, QC Dari Darmasaba Pop Up Notifikasi 2025-11-05 14:32:38 +08:00
fb57698dc9 Add Menu Musik
Add News Reader for Difable
Add Running text news / announcement
2025-11-04 15:08:48 +08:00
d128313e71 Fix QC Keano FrontEnd
Fix QC Kak Ayu Admin 29 Okt
2025-11-03 17:36:00 +08:00
7b4bb1e58e QC Kak Inno FrontEnd Done
QC Kak Ayu FrontEnd Done
QC Keano 31 Okt
2025-11-03 10:28:03 +08:00
0befe6a3f2 QC Kak Inno 28 Okt
QC Kak Ayu 28 Okt
QC Keano 28 Okt
2025-10-30 15:51:12 +08:00
a6663bbcee QC Kak Inno 27 Oct
QC Kak Ayu 27 Oct
QC Keano 27 Oct
QC Pak Jun 27 Oct
2025-10-28 17:34:38 +08:00
d66a952d4c Merge pull request 'nico/27-okt-25' (#1) from nico/27-okt-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/1
2025-10-27 22:17:59 +08:00
ed371bd0d9 Fix QC Kak Inno 24 Okt 25
Fix QC Kak Ayu 24 Okt 25
Fix QC Keano 24 Okt 25
Fix Detail Lowongan Kerja
2025-10-27 22:15:55 +08:00
f82c7b86e0 27 Oct 2025-10-27 10:54:50 +08:00
b5d6585cd5 27 Oct 2025-10-27 10:54:01 +08:00
aa98359ef7 Fix Revisi Kak Inno 22 Oktober && Fix Revisi Kak Ayu 22 Oktober 2025-10-23 17:45:45 +08:00
0ff0d5234a Fix QC Kak Inno 21 Oktober, QC Kak Ayu 21 Oktober, QC Keano, && QC Pak Jun 21 Oktober 2025-10-22 17:00:12 +08:00
827c1c191a Revisi QC Kak Inno tanggal 20 2025-10-22 09:58:16 +08:00
fb596f9033 Fix QC Kak Inno 17 Okt 25, Fix QC Kak Ayu 17 Okt 25, & Fix Qc Pak Jun 17 Okt 25 2025-10-21 12:17:30 +08:00
9055b40769 Fix navbar mobile add active page 2025-10-19 18:08:49 +08:00
bbf13c1cf7 Mengerjakan QC Kak Inno & Kak Ayu Tanggal 16 Oktober
Fix Search
2025-10-17 17:45:56 +08:00
75bf0652b1 Fix QC Kak Inno & Kak Ayu Tanggal 15 Oct 2025-10-17 10:03:03 +08:00
0b574406e2 Fix QC Kak Inno : tanggal 14 Oktober
Fitur Search bisa digunakan di 6 Menu, sisa 3 Menu Lagi
2025-10-15 17:29:57 +08:00
ccf39bc778 Penambahan fungsi search disetiap menu & submenu,
Menu Landing Page
Menu PPID
Menu Desa
2025-10-15 10:13:02 +08:00
3c21f7742c Yang sudh dikerjakan:
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya ( status buku bisa engga otomatis dipinjemnya), kalau dikembalikan statusnya otomatis
)
Yang Mau Dikerjakan:
Cek fungsi menu yang kompleks
2025-10-14 10:38:55 +08:00
a158241c0b - QC User & Admin Menu Pendidikan V
- Fix SubMenu :
- Beasiswa Desa ( Baca Selengkapnya terdapatkan konten ) V
- Info Sekolah ( Kategori Menyesuaikan Dengan Datanya ) V
- Perpustakaan Digital (  V
- Kategori Menyesuaikan Dengan Datanya V
- Saat Mau minjam muncul modal data diri peminjam buku V
- Ada Status Peminjamannya V
)
2025-10-13 11:20:38 +08:00
80c5dc6361 - QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
2025-10-08 17:06:21 +08:00
8ad38fc907 - QC User & Admin Menu Lingkungan
- Fix SubMenu : Edukasi Lingkungan & Konservasi Adat Bali dibagian User
- Fix SUbMenu : Gotong Royong User ( Tabs kategori menyesuaikan dengan data kategori kegiatan )
2025-10-08 14:02:11 +08:00
d601b2fee3 Fix Bug SubMenu Struktur PPID, SubMenu Struktur Organisasi BumDes 2025-10-07 14:38:20 +08:00
cee0957e07 QC - User & Admin Menu Ekonomi SubMenu Pasar Desa
Fix bug kategori produk
2025-10-06 10:26:59 +08:00
5c66eccf23 Fix Menu Ekonomi :
Pasar Desa : Kategorinya ga tampil,
Bug inputan edit di submenu : Demografi pekerjaa
2025-10-04 21:34:31 +08:00
f7fd9be255 QC User & Admin Responsive : Menu Kesehatan - Ekonomi 2025-10-03 10:17:06 +08:00
8a6d8ed8db QC User & Admin Responsive : Menu Landing Page - Desa 2025-10-02 00:10:33 +08:00
63054cedf0 fix inputan edit menu: desa, ekonomi, inovasi, keamanan, kesehatan, landing-page, & lingkungan 2025-09-30 21:41:26 +08:00
c2f1ab8179 Fix Menu Desa Admin & User 2025-09-30 17:13:06 +08:00
295d6f7d63 Fix tampilan admin pertama kali 2025-09-29 14:33:25 +08:00
dbd56a1493 Fix All Text Input User & Admin, fix deskripsi detail break word 2025-09-29 14:06:04 +08:00
2a26db6e17 Tambahan Fix Form Di Menu Landing Page & PPID IKM 2025-09-25 16:21:44 +08:00
33fc472472 Fix Tampilan Mobile Penghargaan Landing Page 2025-09-25 11:41:43 +08:00
d8fa56d923 Tambahan fix menu prestasi desa 2025-09-25 11:14:38 +08:00
cac146471a Fix UI Mobile User & Admin Menu Kesehatan, QC Menu Kesehatan 2025-09-25 10:40:47 +08:00
3e4a7a1c0a Fix Ui Admin & User to Mobile && QC Menu Landing Page, PPID, Desa 2025-09-24 14:50:53 +08:00
b5c044df6e Fix Tampilan User & Admin Menu Inovasi & Lingkungan 2025-09-22 17:15:11 +08:00
0fc47c28ff fix tampilan admin menu inovasi, sisa menu lingkungan 2025-09-22 10:53:48 +08:00
8e25c91e85 Fix Tampilab DesaAntiKorupsi Landing Page Mobile 2025-09-20 03:49:20 +08:00
068d8b1077 Fix All Image Add Lazy Loading 2025-09-19 10:41:18 +08:00
9f72e94557 Fix Admin - User Menu Keamanan, Submenu Pencegahan Kriminalitas 2025-09-17 17:54:03 +08:00
79ad39fc55 Fix Admin - User Menu Keamanan, Submenu Laporan Kontak Darurat, Laporan Publik 2025-09-17 14:59:46 +08:00
39e1e7b575 QC Tampilan Admin & User, Api berfungsi 2025-09-16 16:47:12 +08:00
4ceea5203f QC Admin - User Menu Ekonomi : Jumlah Pengangguran 2025-09-16 10:11:54 +08:00
a5d841bb6b Fix Compres Gambar && seed gambar profile - landing page 2025-09-12 11:55:40 +08:00
6a7bd386ae Add Layout Kontak Darurat - Admin Menu Keamanan 2025-09-11 12:15:40 +08:00
a9d98895bb Fix Admin Menu SDGs Desa & APBdes Desa, Fix UI IMage Layanan Landing Page & Layanan Desa 2025-09-09 17:14:28 +08:00
75475dc62e Fix Package.json Bun 2025-09-08 21:52:17 +08:00
b39800a475 Fix UI Admin Keamanan Lingkungan 2025-09-08 15:45:56 +08:00
797713ef49 Fix UI Admin Menu Kesehatan, Login Admin, OTP 2025-09-08 14:02:21 +08:00
8817b937b1 API Auth 2025-09-04 11:46:08 +08:00
2adf60f9eb Fix UI Admin menu desa 2025-09-03 15:30:02 +08:00
fa9601e126 Fix UI Admin Menu Pendidikam, Add Menu User & Role 2025-09-02 18:08:53 +08:00
7ae83788b4 Fix UI Admin Menu Landing Page & PPID 2025-09-01 16:14:28 +08:00
22ec8d942d Sinkronisasi UI & Admin - Submenu Perpustakaan Digital 2025-08-30 12:22:32 +08:00
9f9a0fb451 Sinkronisasi UI & API Admin - User Submenu Info Sekolah 2025-08-29 15:20:46 +08:00
b6d6583e77 Sinkroniasasi Admin - User, Submenu Info Sekolah Paud 2025-08-29 01:31:05 +08:00
a8fd715822 Fix UI User Menu Ekonomi & Fix UI Submenu Profile, Desa Anti Korupsi 2025-08-28 11:44:03 +08:00
f9530c32eb Fix UI User Menu PPID & Kesehatan 2025-08-27 15:39:13 +08:00
f15ef5a275 Sinkronisasi UI & API Admin - User Submenu Gotong Royong, Menu Lingkungan 2025-08-27 11:57:02 +08:00
3a726a3334 Fix Menu Lingkungan Darmasaba User 2025-08-26 17:49:33 +08:00
b21e1f0c2e Add Debounched Search Menu Ekonomi, Inovasi, Keamanan 2025-08-25 21:47:45 +08:00
f63249327d Sinkronisasi UI & API Admin - User Menu Inovasi 2025-08-25 16:40:03 +08:00
bb8dab05ba Fix API Jumlah Penganggguran 2025-08-25 11:07:21 +08:00
3081e426bd Fix Seed Jumlah Pengangguran 2025-08-23 11:39:16 +08:00
8a275c2a32 Fix Tampilan Data Kesehatan Warga User 2025-08-22 16:30:35 +08:00
8469ebd2e1 Sinkronisasi UI & API Admin - User Submenu Demografi Pekerjaan, Menu Ekonomi 2025-08-22 01:10:18 +08:00
760ba4b6d2 Sinkronisasi Sinkronisasi UI & API Admin - User Submenu Program Kemiskinan 2025-08-22 00:26:58 +08:00
20d4c90e60 Sinkronisasi UI & API Admin - User Submenu Jumlah Pengangguran - Jumlah Penduduk Miskin 2025-08-21 17:15:06 +08:00
fafbb12a08 Sinkronisasi UI & API Admin - User Submenu Pendapatan Asli Desa 2025-08-21 14:50:23 +08:00
01aa0da5cc Fix admin menu Landing page 2025-08-21 10:16:05 +08:00
b580978f8e Fix eror edit admin lowongan kerja 2025-08-20 17:52:10 +08:00
1c01397c0d Sinkronisasi UI & API Admin - User Submenu lowongan kerja lokal, Menu Ekonomi 2025-08-20 17:17:04 +08:00
90a6605efd Sinkronisasi UI & API Admin - User Submenu Pasar Desa, Menu Ekonomi 2025-08-20 17:01:20 +08:00
c22d865283 Sinkronisasi UI & API Admin - User Submenu Tips Keamanan, Menu Keamanan 2025-08-20 14:35:08 +08:00
49067f0218 Add Detail Semua Polsek, Submenu Polsek Terdekat, Menu Keamanan 2025-08-19 17:40:59 +08:00
d79425d529 Sinkronisasi UI & API Menu Keamanan, Admin - User Submenu Keamanan Lingkungan & Polse Terdekat 2025-08-19 11:12:39 +08:00
4491d23bea Sinkronisasi UI & API Admin - User Submenu Penanganan Darurat, User Submenu Kontak Darurat & User Submenu Info Wabah / Penyakit 2025-08-18 20:56:18 +08:00
1e154ced86 Sinkronisasi UI & API Admin - User Submenu Program Kesehatan 2025-08-18 17:14:33 +08:00
bcc51aec12 Sinkronisasi UI & API Admin - User Submenu Data Kesehatan Warga 2025-08-18 15:01:39 +08:00
8d15563f15 Sinkronisasi UI & API Admin - User Submenu Data Kesehatan Warga
-Dibagian Tanggal Gak Auto Ngambil Tanggal Yang Udah Dipakai
-Dibagian fasilitas kesehatan : data dokter dan tarif rencananya mau pakai select
2025-08-15 14:07:56 +08:00
d7a592c635 Fix UI & API Admin Menu Kesehatan, Submenu Data Kesehatan Warga Bagian ChartBar 2025-08-14 20:47:07 +08:00
5e137ba658 Fix Admin Submenu Posyandu, Menu Kesehatan, dan Sinkronisasi UI & API Admin - User Submenu Posyandu 2025-08-14 11:48:57 +08:00
c99416c7f8 Fix FileInput dengan Dropzone 2025-08-14 10:24:03 +08:00
212e2db1fb Test 2025-08-13 15:24:47 +08:00
b8a45bc451 Sinkronisasi UI & API Admin - User Submenu Profile, Menu Desa 2025-08-13 14:53:48 +08:00
0777b00a7d Sinkronisasi UI & API Admin - User Menu Landing Page Submenu Indeks Kepuasan Masyarakat 2025-08-13 10:51:17 +08:00
a035039b2c Admin Fix Chart Bar PerMonth Submenu IKM, Menu PPID 2025-08-13 09:47:21 +08:00
a6832cad40 Admin: Ubah Pie Chart Submenu IKM, Menu PPID 2025-08-13 00:34:44 +08:00
a1d55e2b0a Fix Admin Menu PPID, Submenu IKM 2025-08-13 00:07:57 +08:00
c1583c21b1 Sinkronisasi UI & API Admin - User Menu Desa, Submenu Gallery 2025-08-12 11:45:39 +08:00
2fe8b8ce1a Sinkronisasi UI & API Admin - User Submenu Pengumuman 2025-08-11 14:35:47 +08:00
5cbf7810bc API & UI Admin Menu Desa, Submenu Pengumuman 2025-08-11 10:39:06 +08:00
b3bf6b0327 API & UI Admin Menu Desa, Submenu Pengumuman 2025-08-08 16:59:51 +08:00
a65529cb23 Sinkronisasi UI & API Admin - User Submenu Berita 2025-08-08 14:31:28 +08:00
afc7bced44 Sinkronisasi UI & API Admin - User Submenu Berita 2025-08-08 12:07:44 +08:00
0ac9fa1f53 Sinkronisasi UI & API Admin - User Submenu Berita 2025-08-07 10:53:56 +08:00
d4af56b508 Sinkronisasi UI & API Admin - User Submenu Penghargaan 2025-08-06 11:56:21 +08:00
b62c4be30a Sinkronisasi API & UI Admin & User, Menu Landing Page, Submenu Potensi 2025-08-05 22:08:25 +08:00
ab887c30e6 Sinkronisasi Admin dan User, Menu Landing Page, Submenu Potensi 2025-08-05 17:43:04 +08:00
8e76a83d14 Sinkronisasi ADMIN & USER Menu Landing Page, Submenu Layanan 2025-08-05 14:59:15 +08:00
a2b68ec78b FIX UI & API Menu Desa, Submenu Pelayanan Surat Keterangan 2025-08-04 17:46:51 +08:00
0e55462adc Sinkronisasi UI Admin & User Menu Landing Page, submenu Prestasi Desa 2025-08-04 16:28:53 +08:00
73ae198158 Menu Landing Page User
Submenu APBDes
2025-08-04 14:46:51 +08:00
9d14bb0c56 Menu Landing Page User
Submenu Desa Anti Korupsi
2025-08-04 14:09:03 +08:00
1cdff53c56 Sinkronisasi UI Admin & User Menu Landing Page, Submenu Profile, SDGSDesa 2025-08-04 10:29:13 +08:00
54312e9486 Fix UI Menu Landing Page, Submenu Profile & Desa Anti Korupsi 2025-08-01 15:21:01 +08:00
024d5517fa UI & API Menu PPID, Submenu Struktur PPID 2025-07-31 16:22:02 +08:00
4e61695649 Test AUTH - 30 Jul 2025-07-30 12:11:17 +08:00
c11cc421a4 UI & API Menu Pendidikan, Submenu Perpustakaan Digital 2025-07-29 17:58:17 +08:00
0109886e00 UI & API Menu Pendidikan, Submenu Beasiswa tab beasiswa pendaftar 2025-07-29 12:29:25 +08:00
50e8999205 UI & API Menu Pendidikan, Submenu Data Pendidikan 2025-07-29 11:05:01 +08:00
e2e1672c80 UI & API Pendidikan Non Formal 2025-07-28 17:51:42 +08:00
ac0eb926eb UI & API Menu Pendidikan & Submenu Bimbingan Belajar Desa 2025-07-28 16:45:36 +08:00
b24bcd8019 UI & API Menu Pendidikan, Submenu Info Sekolah 2025-07-28 14:17:42 +08:00
5601e59922 UI & API Menu Landing Page 2025-07-28 10:18:24 +08:00
a25cfe8b8a API & UI Menu Landing Page, Submenu Prestasi Desa 2025-07-24 14:49:53 +08:00
b745bd4623 API & UI Menu Landing Page, Submenu SDGs Desa & APBDes 2025-07-24 11:53:58 +08:00
bdf751ec3d UI & API Menu Landing Page, Submenu Desa Anti Korupsi 2025-07-23 17:12:33 +08:00
1bc6dd8dbf UI & API Menu LandingPage, Submenu Profile 2025-07-23 12:19:10 +08:00
88a10538a7 API & UI Menu Landing Page, Submenu Profile, Tabs 1 & 2 sisa tabs 3 2025-07-23 10:08:02 +08:00
d4efcacf1b API & UI Menu Landing Page, Submenu Profile 2025-07-22 17:13:56 +08:00
9b2201ea57 FIX UI & API Menu Lingkungan Submenu Gotong Royong 2025-07-22 11:53:15 +08:00
80a7df663e API & UI Menu Lingkungan, Submenu Gotong Royong 2025-07-22 11:24:19 +08:00
9dfcda7687 Pertama 2025-07-21 15:23:50 +08:00
e2f75ff3ad API & UI Menu Lingkungan, Submenu Edukas Lingkungan 2025-07-18 21:24:51 +08:00
f05a096633 pertama 2025-07-18 17:42:10 +08:00
9c55869aa6 pertama 2025-07-18 17:40:57 +08:00
6e5d45fa20 pertama 2025-07-18 17:31:31 +08:00
e5373b4823 pertama 2025-07-18 17:28:55 +08:00
928cd048c0 pertama 2025-07-18 17:27:54 +08:00
41f54772e9 API & UI Menu Lingkungan, Submenu Data Lingkungan Desa 2025-07-18 17:11:13 +08:00
cd343badb2 API & UI Menu Lingkungan, Submenu Program Penghijauan 2025-07-18 16:27:38 +08:00
4025771a4d API & UI Menu Lingkungan Submenu Pengelolaan Sampah 2025-07-18 15:01:43 +08:00
7439eb7687 API & UI Menu Lingkungan, Submenu Penglolaan Sampah 2025-07-17 17:09:28 +08:00
49a1084099 API & UI Menu Inovasi, SubMenu Layanan Online Desa 2025-07-17 15:31:10 +08:00
cde6c91cd4 API & UI Menu Inovasi, Submenu Layanan Online Desa: Tab 1-4 2025-07-17 11:44:53 +08:00
55433128a9 API & UI Menu Inovasi & Submenu Layan Online Desa 2 tabs 2025-07-16 00:23:15 +08:00
e8ad74d118 Merge branch 'nico/push-stg' into nico/15-jul-25 2025-07-15 11:51:13 +08:00
99c1fd1004 UI & API Menu Inovasi, SubMenu : Kolaborasi Inovasi & Info Teknologi 2025-07-15 11:48:56 +08:00
03c0523194 UI & API Menu Inovasi, SubMenu Program Kreatif Desa 2025-07-14 17:35:38 +08:00
ae328f40a0 UI & API Menu Inovasi, SubMenu Program Kreatif Desa 2025-07-14 17:14:02 +08:00
6e109ffe00 FIX UI Admin Menu PPID 2025-07-14 14:33:08 +08:00
c4aea568e9 UI & API Smart DIgital Village 2025-07-14 10:24:59 +08:00
1c8104ee69 API & UI Admin Menu Ekonomi, Submenu PADesa 2025-07-11 18:50:20 +08:00
4baffe95f3 UI & API Menu Ekonomi, SubMenu PADesa : Tabs Pendapatan, Pembiayaan, dan Belanja 2025-07-11 17:51:07 +08:00
cb52701f47 API & UI Tabs SubMenu Pendapatan, Menu PADesa 2025-07-10 16:52:26 +08:00
2bc9b2f3c6 Menerapkan pagination di submenu pegawai & berita 2025-07-10 10:46:58 +08:00
7b2b306849 API & UI Menu Ekonomi, Submenu Jumlah Pengangguran 2025-07-10 00:21:33 +08:00
d328f64d86 UI & API Tabs Detail Data Pengangguran 2025-07-09 15:25:48 +08:00
119275b95c API & State Jumlah Pengangguran 2025-07-09 12:00:37 +08:00
124dfb8160 UI & API Menu Ekonomi, Submenu Demografi 2025-07-08 23:30:48 +08:00
46c79b8ded UI & API Submenu Sektor Unggulan, Menu Ekonomi 2025-07-08 17:16:13 +08:00
d105293149 UI & API Menu Ekonomi, Submenu Jumlah Penduduk Miskin 2025-07-08 16:30:33 +08:00
adcbe3aa3d UI & API Menu Ekonomi, Submenu Jumlah Penduduk usia yang menganggur 2025-07-08 15:20:26 +08:00
bffe648802 Fix Tabs Pegawai, Submenu Struktur & Organisasi, Menu Ekonomi 2025-07-08 12:05:10 +08:00
7e95d5fbb4 Add Fitur Search Di setiap menu 2025-07-08 00:30:55 +08:00
2725c2c064 UI & API Menu Struktur Organisasi Tabs Hubungan Organisasi 2025-07-07 22:24:46 +08:00
be189df37c UI & API Struktur organisasi sudah bisa aktif & tidak aktif 2025-07-07 21:45:04 +08:00
c0b941395d UI & API Struktur organisasi sudah bisa aktif & tidak aktif 2025-07-07 21:41:27 +08:00
a2e25a3e3a API & UI Menu Struktur bagian pegawai 2025-07-07 17:14:44 +08:00
d86824a943 API & UI Struktur Organisasi Posisi Organisasi 2025-07-07 11:33:05 +08:00
c823462a47 Merge pull request #39 from bipproduction/nico/4-jul-25
Nico/4 jul 25:
Fix UI & API Admin Menu Ekonomi Pasar Desa
2025-07-04 11:20:15 +08:00
4f97c01501 Perbaikan UI & API Menu Ekonomi Pasar Desa 2025-07-04 11:09:06 +08:00
0fd47e3e94 API & UI Pasar Desa Menu Ekonomi 2025-07-04 00:11:55 +08:00
b92a974dcd API & UI Admin Menu Keamanan Done 2025-07-03 16:09:39 +08:00
10361770b4 API & UI Program Kemiskinan Menu Ekonomi 2025-07-03 12:21:08 +08:00
aec2f5094a Push 1 Program Kemiskinan 2025-07-03 11:24:54 +08:00
72d39b020a API/UI Admin Ekonomi Lowongan kerja 2025-07-02 17:03:20 +08:00
51d67736ef API & UI Admin Keamanan Laporan Publik 2025-07-02 15:35:10 +08:00
406c6f3c9f UI & API Menu Keamanan, Kontak Darurat 2025-07-02 14:10:22 +08:00
1c5e4410c4 Save 2025-07-01 20:57:32 +08:00
4724b7473d Try Fix UI & API Menu Ekonomi Sub Menu Pasar Desa 2025-07-01 16:48:44 +08:00
32a75bcb01 Merge pull request #38 from bipproduction/nico/1-jul-25
UI & API Menu Keamanan baru 3 Menu : Keamanan Lingkungan, Polsek Terd
2025-07-01 11:19:03 +08:00
c5fc4f4cea UI & API Menu Keamanan baru 3 Menu : Keamanan Lingkungan, Polsek Terdekat, & Tips Keamanan 2025-07-01 11:16:53 +08:00
9f39eb41ab Merge pull request #36 from bipproduction/nico/30-jun-2025
Nico/30 jun 2025
2025-06-30 11:15:28 +08:00
81ea18cb07 Merge branch 'nico/push-stg' into nico/30-jun-2025 2025-06-30 11:15:06 +08:00
dd7ce6943d Keperluan Deploy 2025-06-30 11:04:20 +08:00
ee10f339e9 API Admin Menu Keamanan Done 2025-06-30 10:56:46 +08:00
02462b2c19 API 2 Menu Keamanan 2025-06-29 02:46:17 +08:00
41181d4cb3 Fix Dibagian Data Kesehatan Warga 2025-06-28 00:10:45 +08:00
6d5b8dcf64 UI & API Admin Menu Kesehatan Done 2025-06-27 23:52:00 +08:00
924be5b11b Sisa 1 Tabs aja yang data kesehatan warga 2025-06-26 17:24:57 +08:00
21085ce342 Merge branch 'staging' into nico/push-stg 2025-06-26 11:19:54 +08:00
88784f00f6 Merge pull request #34 from bipproduction/nico/25-jun-25
Nico/25 jun 25
2025-06-26 11:01:15 +08:00
4f6cc66b7c Kebutuhan Deploy 2025-06-26 11:00:15 +08:00
456342851b Merge pull request #33 from bipproduction/nico/25-jun-25
Nico/25 jun 25
staging versi 0.1.3
2025-06-25 16:32:28 +08:00
4683034cd7 UI & API Admin Kesehatan Menu Data Kesehatan Warga Sisa 2 Tabs 2025-06-25 15:50:24 +08:00
37de71a75a UI & API Admin Kesehatan Menu Data Kesehatan Warga Sisa 2 Tabs 2025-06-25 15:47:05 +08:00
27fa7ac0fc UI & API Data Kesehatan warga sisa 3 tabs 2025-06-24 17:25:43 +08:00
fc08b2e790 UI & API Admin Kesehatan Done 2025-06-20 08:11:31 +08:00
4a5524ce88 API & UI Kesehatan Sudah Sampai Di Penanganan Darurat 2025-06-20 00:08:13 +08:00
899883ca2a Fix UI & API Admin Kesehatan Puskesmas 2025-06-19 15:40:27 +08:00
10ecc13ad7 API All Kesehatan 2025-06-19 14:12:57 +08:00
58f538425c UI & API Admin Menu Kesehatan 2025-06-19 10:24:50 +08:00
fa922c7127 Merge pull request #31 from bipproduction/nico/push-stg
Nico/push stg
2025-06-18 16:38:45 +08:00
45acdba93f Merge pull request #30 from bipproduction/nico/18-jun-25
Nico/18 jun 25
2025-06-18 16:38:10 +08:00
d2f53ff69b Keperluan Deploy 2025-06-18 16:37:08 +08:00
40f0294595 Tambahan buat deploy 2025-06-18 16:32:19 +08:00
cb8d561467 Merge pull request #29 from bipproduction/nico/push-stg
Nico/push stg 18 Juni 2025:
UI & API Menu PPID & Desa Clear
2025-06-18 15:35:49 +08:00
85a0cb6d56 Merge pull request #28 from bipproduction/nico/18-jun-25
Nico/18 jun 25:
UI & API Menu PPID & Desa Clear
2025-06-18 15:34:27 +08:00
6ed0246cea API Profile Desa Udah Clear, API Menu desa udah clear
API & UI Profile Desa Clear
2025-06-18 15:32:06 +08:00
af726043bd API Profile Desa aman, tinggal Profil Perbekel
UI Profile Desa aman, tinggal Profil Perbekel
2025-06-18 14:08:02 +08:00
f4888b53ab API Profile Desa Menu Desa
Fix Eror gallery bagian tabs video
Next UI Profile Desa
2025-06-17 17:30:47 +08:00
f7437708c0 Menu Desa, Sub Menu Yang Tersisa Tinga Tinggal Profile Aja 2025-06-16 17:46:25 +08:00
7bf5ee69d5 Api Admin Menu Desa Sub Menu Layanan Tabs 3 Pelayanan Done 2025-06-16 15:01:15 +08:00
e03b071b00 UI Admin Dashboard Layanan Desa 1 tabs
API Admin Dashboard Layanan Desa 1 Tabs
2025-06-16 03:08:34 +08:00
8ded234991 UI Desa Gallery Done
API Desa Gallery Done
2025-06-13 16:32:33 +08:00
1462e1d256 Tampilan UI Admin PPID IKM & API 2025-06-12 17:40:37 +08:00
a1c2821153 API Struktur Admin PPID 2025-06-11 11:19:30 +08:00
9f66b037f9 Add Fitur Status Operasional & Jadwal Kerja 2025-06-10 14:22:51 +08:00
9e725e2eea Fix UI Lingkungan, UI Ekonomi Progress 2025-06-10 11:53:10 +08:00
e4b7418ed3 Tambahan Fix UI PPID 2025-06-10 11:01:13 +08:00
6d312b7a28 Fix UI Admin PPID
Profile PPID clear
2025-06-10 00:54:29 +08:00
41ae92774d Fix UI PPID:
Visi Misi PPID, Dasar Hukum, Daftar Informasi Publik
2025-06-09 12:09:00 +08:00
46748205fd API Menu Kesehatan, Sub Menu Posyandu 2025-06-05 16:10:43 +08:00
5e74447056 Tambahan UI di menu ligkungan 2025-06-05 12:23:02 +08:00
c9d0ea2a97 Tambahan Tampilan Admin Menu Inovasi 2025-06-05 11:00:51 +08:00
7d58513e33 UI admin bagian pengumuman dan backendnya 2025-06-04 17:24:32 +08:00
f56c5b3532 Tambahan UI Admin : Dibagian Desa Sisa Profile, Potensi, Penghargaan 2025-06-04 15:08:12 +08:00
06622c49e8 Tampilan UI Admin Ekonomi & Inovasi Progress 2025-06-04 14:02:39 +08:00
a1e7fddbed UI Admin Revisi Baru Di Menu Ekonomi 2025-06-03 17:42:34 +08:00
423ad0e2ba Tambahan 2025-06-03 12:10:00 +08:00
084435500f Lanjutan UI 2025-06-03 10:21:06 +08:00
4f2c565b2e Merge pull request #27 from bipproduction/nico/push-stg
Nico/push stg

Senin, 2 Juni 2025 : 
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu desa
Yang Lagi Dikerjakan:
* Tampilan UI Admin di menu kesehatan

Yang Akan Dikerjakan:
* API Create, edit dan delete pengumuman
* Tampilan UI Admin di menu keamanan
2025-06-02 22:30:15 +08:00
c4adc9bb22 Merge pull request #26 from bipproduction/nico/1-jun-25
Senin, 2 Juni 2025 : 
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu desa
Yang Lagi Dikerjakan:
* Tampilan UI Admin di menu kesehatan

Yang Akan Dikerjakan:
* API Create, edit dan delete pengumuman
* Tampilan UI Admin di menu keamanan
2025-06-02 22:28:58 +08:00
5037009c40 Kesehatan : udah fix semua, cuma dibagian kayak detail sama edit uinya belum 2025-06-02 22:23:13 +08:00
9d572f82c3 Merge pull request #25 from bipproduction/nico/push-stg
Jum'at, 30 May 2025 : 
Yang Sudah Di Kerjakan
- Tampilan UI Admin di menu inovasi
- API Create, edit dan delete potensi
- Tampilan UI Landing Page sudah sesuai di mobile

Yang Lagi Dikerjakan:
- Progress Tampilan UI Admin Di Menu lingkungan
- Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
- API Create, edit dan delete pengumuman
- Tampilan UI Admin Di Menu Pendidikan
2025-05-30 21:16:15 +08:00
452692f314 Merge pull request #24 from bipproduction/nico/30-may-25
Jum'at, 30 May 2025 : 
Yang Sudah Di Kerjakan
- Tampilan UI Admin di menu inovasi
- API Create, edit dan delete potensi
- Tampilan UI Landing Page sudah sesuai di mobile

Yang Lagi Dikerjakan:
- Progress Tampilan UI Admin Di Menu lingkungan
- Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
- API Create, edit dan delete pengumuman
- Tampilan UI Admin Di Menu Pendidikan
2025-05-30 21:15:27 +08:00
8f2b9665a9 Jum'at, 30 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu inovasi
* API Create, edit dan delete potensi
* Tampilan UI Landing Page sudah sesuai di mobile

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu lingkungan
* Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
* API Create, edit dan delete pengumuman
* Tampilan UI Admin Di Menu Pendidikan
2025-05-30 21:13:55 +08:00
77f99a7c8f Jum'at, 30 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu inovasi
* API Create, edit dan delete potensi

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu lingkungan
* Progress API Create, edit dan delete potensi

Yang Akan Dikerjakan:
* API Create, edit dan delete pengumuman
* Tampilan UI Admin Di Menu Pendidikan
2025-05-30 16:57:41 +08:00
d88f168258 Jumat, 30 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu inovasi
* API Create, edit dan delete potensi

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu Lingkungan

Yang Akan Dikerjakan:
* API Menu Lain
* Tampilan UI Admin Di Menu Pendidikan
2025-05-30 11:22:31 +08:00
f9bd2cea11 tambahan 2025-05-27 16:18:23 +08:00
5010677bc8 Merge pull request #23 from bipproduction/nico/27-may-25-01
Selasa, 27 May 2025:
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu Inovasi
* Progress API ProfilePPID

Yang Akan Dikerjakan:
* API Menu Lain
* Tampilan UI Admin Di Menu Lingkungan
* Tampilan UI Admin Di Menu Pendidikan
2025-05-27 11:24:24 +08:00
5734e5d9a7 Selasa, 27 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Lagi Dikerjakan:
* Progress Tampilan UI Admin Di Menu Inovasi
* Progress API ProfilePPID

Yang Akan Dikerjakan:
* API Menu Lain
* Tampilan UI Admin Di Menu Lingkungan
* Tampilan UI Admin Di Menu Pendidikan
2025-05-27 11:23:20 +08:00
7af3fbff2d Merge pull request #22 from bipproduction/nico/26-may-25
Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Akan Dikerjakan:
* API ProfilePPID
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 17:16:07 +08:00
3654629bde Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, edit dan delete berita

Yang Akan Dikerjakan:
* API ProfilePPID
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 17:15:07 +08:00
34ca736dda Merge pull request #21 from bipproduction/nico/26-may-25
Nico/26 may 25
Senin, 26 May 2025 : 
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, dan delete berita

Yang Akan Dikerjakan:
* API Di Menu Desa : Edit Berita
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 14:30:07 +08:00
02738104b5 Senin, 26 May 2025 :
Yang Sudah Di Kerjakan
* Tampilan UI Admin di menu ekonomi
* API Create, dan delete berita

Yang Akan Dikerjakan:
* API Di Menu Desa : Edit Berita
* Tampilan UI Admin Di Menu Inovasi
2025-05-26 14:26:10 +08:00
92de697ae0 Nico-25 Mei 2025:
Membuat create berita dan gambar
Membuat fungsi tombol di mana bisa menghapus konten sesuai idnya
2025-05-25 11:33:50 +08:00
cf6a5422ec Bagian Berita Di Admin Sudah Bisa Upload Di Bun Dev atau Bun Start 2025-05-23 16:30:46 +08:00
ee9368e911 UI Admin Keamanan 2025-05-23 11:33:59 +08:00
cab86eb02f Nico 21 Mei :
Yang udah dikerjakan:
Tampilan UI Menu DesaTampilan UI Menu Kesehatan

Yang Akan dikerjakan:
Tampilan UI Keamanan, dan Menu Selanjutnya
Memperbaiki eror di bagian inputan dimana saat nginput data outputnya ikut ke ganti seharusnya di submit dulu
2025-05-21 11:28:27 +08:00
d1e39ae7f9 Senin, 20 May 2025 : 2025-05-20 12:07:00 +08:00
3b61f54509 Merge pull request #20 from bipproduction/nico/8-may-25
Nico/8 may 25
2025-05-09 10:36:45 +08:00
fbe0c19d22 Merge pull request #19 from bipproduction/nico/2-may-25
Fix Eror Admin Persentase & Grafik
2025-05-02 16:11:38 +08:00
f8914ab78f Merge pull request #18 from bipproduction/nico/30-apr-25
Admin Dashboard Bagian Data Kesehatan
2025-04-30 16:42:45 +08:00
8f3ee2f831 Push STG 2025-04-30 16:32:29 +08:00
ddf0ca62c4 Merge pull request #17 from bipproduction/nico/28-apr-25
Dashboard Admin
2025-04-30 16:16:48 +08:00
f8cdd3abdd Merge pull request #13 from bipproduction/nico/24-mar-25
tamabah versi
2025-03-24 15:35:55 +08:00
64bc739496 Merge pull request #12 from bipproduction/nico/24-mar-25
Nico/24 mar 25
2025-03-24 15:33:27 +08:00
2268 changed files with 231151 additions and 22377 deletions

47
.dockerignore Normal file
View File

@@ -0,0 +1,47 @@
node_modules
.next
.git
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
bun-debug.log*
# Docker files
Dockerfile
.dockerignore
# OS files
.DS_Store
Thumbs.db
# Markdown/Documentation
README.md
GEMINI.md
AGENTS.md
AUDIT_REPORT.md
QWEN.md
NOTE.md
task-project-apbdes.md
MUSIK_CREATE_ANALYSIS.md
darkMode.md
/test-results
/playwright-report
/tmp_assets
/foldergambar
/googleapi
/xx
/xx.ts
/xx.txt
/test.txt
/x.json
/x.sh
/xcoba.ts
/xcoba2.ts
/gambar.ttx
/test-berita-state.ts

44
.env.example Normal file
View File

@@ -0,0 +1,44 @@
# Database Configuration
DATABASE_URL="postgresql://username:password@localhost:5432/desa-darmasaba?schema=public"
# Seafile Configuration (File Storage)
SEAFILE_TOKEN=your_seafile_token
SEAFILE_REPO_ID=your_seafile_repo_id
SEAFILE_URL=https://your-seafile-instance.com
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
# Upload Configuration
WIBU_UPLOAD_DIR=uploads
WIBU_DOWNLOAD_DIR=./download
# WhatsApp Server Configuration
WA_SERVER_TOKEN=your_whatsapp_server_token
# Application Configuration
# IMPORTANT: For staging/production, set this to your actual domain
# Local development: NEXT_PUBLIC_BASE_URL=http://localhost:3000
# Staging: NEXT_PUBLIC_BASE_URL=https://desa-darmasaba-stg.wibudev.com
# Production: NEXT_PUBLIC_BASE_URL=https://your-production-domain.com
# Or use relative URL '/' for automatic protocol/domain detection (recommended)
NEXT_PUBLIC_BASE_URL=/
# Email Configuration (for notifications/subscriptions)
EMAIL_USER=your_email@gmail.com
EMAIL_PASS=your_email_app_password
# Session Configuration
BASE_SESSION_KEY=your_session_key_generate_secure_random_string
BASE_TOKEN_KEY=your_jwt_secret_key_generate_secure_random_string
# Telegram Bot Configuration (for notifications)
BOT_TOKEN=your_telegram_bot_token
CHAT_ID=your_telegram_chat_id
# Session Password (for iron-session)
SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
# ElevenLabs API Key (for TTS features - optional)
ELEVENLABS_API_KEY=your_elevenlabs_api_key
# Environment (optional, defaults to development)
# NODE_ENV=development

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bun
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
// Function to manually load .env from project root if process.env is missing keys
function loadEnv() {
const envPath = join(process.cwd(), ".env");
if (existsSync(envPath)) {
const envContent = readFileSync(envPath, "utf-8");
const lines = envContent.split("\n");
for (const line of lines) {
if (line && !line.startsWith("#")) {
const [key, ...valueParts] = line.split("=");
if (key && valueParts.length > 0) {
const value = valueParts.join("=").trim().replace(/^["']|["']$/g, "");
process.env[key.trim()] = value;
}
}
}
}
}
async function run() {
try {
// Ensure environment variables are loaded
loadEnv();
const inputRaw = readFileSync(0, "utf-8");
if (!inputRaw) return;
let finalText = "";
let sessionId = "web-desa-darmasaba";
try {
// Try parsing as JSON first
const input = JSON.parse(inputRaw);
sessionId = input.session_id || "web-desa-darmasaba";
finalText = typeof input === "string" ? input : (input.response || input.text || JSON.stringify(input));
} catch {
// If not JSON, use raw text
finalText = inputRaw;
}
const BOT_TOKEN = process.env.BOT_TOKEN;
const CHAT_ID = process.env.CHAT_ID;
if (!BOT_TOKEN || !CHAT_ID) {
console.error("Missing BOT_TOKEN or CHAT_ID in environment variables");
return;
}
const message =
`✅ *Gemini Task Selesai*\n\n` +
`🆔 Session: \`${sessionId}\` \n\n` +
`🧠 Output:\n${finalText.substring(0, 3500)}`;
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: CHAT_ID,
text: message,
parse_mode: "Markdown",
}),
});
if (!res.ok) {
const errorData = await res.json();
console.error("Telegram API Error:", errorData);
} else {
console.log("Notification sent successfully!");
}
process.stdout.write(JSON.stringify({ status: "continue" }));
} catch (err) {
console.error("Hook Error:", err);
process.stdout.write(JSON.stringify({ status: "continue" }));
}
}
run();

17
.gemini/settings.json Normal file
View File

@@ -0,0 +1,17 @@
{
"hooks": {
"AfterAgent": [
{
"matcher": "*",
"hooks": [
{
"name": "telegram-notify",
"type": "command",
"command": "bun $GEMINI_PROJECT_DIR/.gemini/hooks/telegram-notify.ts",
"timeout": 10000
}
]
}
]
}
}

View File

@@ -1,219 +0,0 @@
name: Build And Save Log
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment (e.g., staging, production)"
required: true
default: "staging"
version:
description: "Version to deploy"
required: false
default: "latest"
env:
APP_NAME: desa-darmasaba-action
WA_PHONE: "6289697338821,6289697338822"
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
# Checkout kode sumber
- name: Checkout code
uses: actions/checkout@v3
# Setup Bun
- name: Setup Bun
uses: oven-sh/setup-bun@v2
# Cache dependencies
- name: Cache dependencies
uses: actions/cache@v3
with:
path: .bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
# Step 1: Set BRANCH_NAME based on event type
- name: Set BRANCH_NAME
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
else
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
fi
# Step 2: Generate APP_VERSION dynamically
- name: Set APP_VERSION
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
# Step 3: Kirim notifikasi ke API build Start
- name: Notify start build
run: |
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
for PHONE in "${PHONES[@]}"; do
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
done
# Install dependencies
- name: Install dependencies
run: bun install
# Konfigurasi environment variable untuk PostgreSQL dan variabel tambahan
- name: Set up environment variables
run: |
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
echo "PORT=3000" >> .env
echo "NEXT_PUBLIC_WIBU_URL=localhost:3000" >> .env
echo "WIBU_UPLOAD_DIR=/uploads" >> .env
# Create log file
- name: Create log file
run: touch build.txt
# Migrasi database menggunakan Prisma
- name: Apply Prisma schema to database
run: bun prisma db push >> build.txt 2>&1
# Seed database (opsional)
- name: Seed database
run: |
bun prisma db seed >> build.txt 2>&1 || echo "Seed failed or no seed data found. Continuing without seed." >> build.txt
# Build project
- name: Build project
run: bun run build >> build.txt 2>&1
# Ensure project directory exists
- name: Ensure /var/www/projects/${{ env.APP_NAME }} exists
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
mkdir -p /var/www/projects/${{ env.APP_NAME }}
# Deploy to a new version directory
- name: Deploy to VPS (New Version)
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
source: "."
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}"
# Set up environment variables
- name: Set up environment variables
run: |
rm -r .env
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5433/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
echo "NEXT_PUBLIC_WIBU_URL=${{ env.APP_NAME }}" >> .env
echo "WIBU_UPLOAD_DIR=/var/www/projects/${{ env.APP_NAME }}/uploads" >> .env
# Kirim file .env ke server
- name: Upload .env to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
source: ".env"
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}/"
# manage deployment
- name: manage deployment
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
# Source ~/.bashrc
source ~/.bashrc
# Find an available port
PORT=$(curl -s -X GET https://wibu-bot.wibudev.com/api/find-port | jq -r '.[0]')
if [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
echo "Invalid or missing port from API."
exit 1
fi
# manage deployment
cd /var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}
# Create uploads directory
mkdir -p /var/www/projects/${{ env.APP_NAME }}/uploads
# Install dependencies
bun install --production
# Apply database schema
if ! bun prisma db push; then
echo "Database migration failed."
exit 1
fi
# Seed database (optional)
bun prisma db seed || echo "tidak membutuhkan seed"
# Restart the application
pm2 reload ${{ env.APP_NAME }} || pm2 start "bun run start --port $PORT" --name "${{ env.APP_NAME }}-$PORT" --namespace "${{ env.APP_NAME }}"
# Step 4: Set BUILD_STATUS based on success or failure
- name: Set BUILD_STATUS
if: success()
run: echo "BUILD_STATUS=success" >> $GITHUB_ENV
- name: Set BUILD_STATUS on failure
if: failure()
run: echo "BUILD_STATUS=failed" >> $GITHUB_ENV
# Update status log
- name: Update status log
if: always()
run: |
echo "=====================" >> build.txt
echo "BUILD_STATUS=${{ env.BUILD_STATUS }}" >> build.txt
echo "APP_NAME=${{ env.APP_NAME }}" >> build.txt
echo "APP_VERSION=${{ env.APP_VERSION }}" >> build.txt
echo "=====================" >> build.txt
# Upload log to 0x0.st
- name: Upload log to 0x0.st
id: upload_log
if: always()
run: |
LOG_URL=$(curl -F "file=@build.txt" https://wibu-bot.wibudev.com/api/file )
echo "LOG_URL=$LOG_URL" >> $GITHUB_ENV
# Kirim notifikasi ke API
- name: Notify build success via API
if: always()
run: |
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
for PHONE in "${PHONES[@]}"; do
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:${{ env.BUILD_STATUS }}\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}\nLog:${{ env.LOG_URL }}'))")
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
done

56
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Publish Docker to GHCR
on:
push:
tags:
- "v*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract tag name
id: meta
run: echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

106
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Publish Docker to GHCR
on:
workflow_dispatch:
inputs:
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- prod
- stg
tag:
description: "Image tag (e.g. 1.0.0)"
required: true
default: "1.0.0"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout branch ${{ github.event.inputs.stack_env }}
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.stack_env }}
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
path: .ci
sparse-checkout: .github/workflows/script
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
type=raw,value=${{ github.event.inputs.stack_env }}-latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true
- name: Notify success
if: success()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
- name: Notify failure
if: failure()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"

60
.github/workflows/re-pull.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Re-Pull Docker
on:
workflow_dispatch:
inputs:
stack_name:
description: "stack name"
required: true
type: string
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- stg
- prod
jobs:
publish:
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
sparse-checkout: .github/workflows/script
- name: Deploy ke Portainer
run: bash ./.github/workflows/script/re-pull.sh
env:
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
- name: Notify success
if: success()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
- name: Notify failure
if: failure()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"

26
.github/workflows/script/notify.sh vendored Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
if [ "$NOTIFY_STATUS" = "success" ]; then
ICON="✅"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
else
ICON="❌"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
fi
if [ -n "$NOTIFY_DETAIL" ]; then
TEXT="${TEXT}
${NOTIFY_DETAIL}"
fi
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg chat_id "$TELEGRAM_CHAT_ID" \
--arg text "$TEXT" \
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"

120
.github/workflows/script/re-pull.sh vendored Normal file
View File

@@ -0,0 +1,120 @@
#!/bin/bash
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
: "${STACK_NAME:?STACK_NAME tidak di-set}"
# Timeout total: MAX_RETRY * SLEEP_INTERVAL detik
MAX_RETRY=60 # 60 × 10s = 10 menit
SLEEP_INTERVAL=10
echo "🔐 Autentikasi ke Portainer..."
TOKEN=$(curl -s -X POST "https://${PORTAINER_URL}/api/auth" \
-H "Content-Type: application/json" \
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
| jq -r .jwt)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
exit 1
fi
echo "🔍 Mencari stack: $STACK_NAME..."
STACK=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks" \
-H "Authorization: Bearer ${TOKEN}" \
| jq ".[] | select(.Name == \"$STACK_NAME\")")
if [ -z "$STACK" ]; then
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
exit 1
fi
STACK_ID=$(echo "$STACK" | jq -r .Id)
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
ENV=$(echo "$STACK" | jq '.Env // []')
# ── Catat container ID lama sebelum redeploy ──────────────────────────────────
echo "📸 Mencatat container aktif sebelum redeploy..."
CONTAINERS_BEFORE=$(curl -s -X GET \
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
-H "Authorization: Bearer ${TOKEN}")
OLD_IDS=$(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Id] | join(",")')
echo " Container lama: $(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Names[0]] | join(", ")')"
# ── Ambil compose file lalu trigger redeploy ─────────────────────────────────
echo "📄 Mengambil compose file..."
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
-H "Authorization: Bearer ${TOKEN}" \
| jq -r .StackFileContent)
PAYLOAD=$(jq -n \
--arg content "$STACK_FILE" \
--argjson env "$ENV" \
'{stackFileContent: $content, env: $env, pullImage: true}')
echo "🚀 Triggering redeploy $STACK_NAME (pull latest image)..."
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
if [ "$HTTP_STATUS" != "200" ]; then
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
cat /tmp/portainer_response.json | jq .
exit 1
fi
echo "⏳ Menunggu image selesai di-pull dan container baru running..."
echo " (Timeout: $((MAX_RETRY * SLEEP_INTERVAL)) detik)"
COUNT=0
while [ $COUNT -lt $MAX_RETRY ]; do
sleep $SLEEP_INTERVAL
COUNT=$((COUNT + 1))
CONTAINERS=$(curl -s -X GET \
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
-H "Authorization: Bearer ${TOKEN}")
# Container baru = ID tidak ada di daftar container lama
NEW_RUNNING=$(echo "$CONTAINERS" | jq \
--arg old "$OLD_IDS" \
'[.[] | select(.State == "running" and ((.Id) as $id | ($old | split(",") | index($id)) == null))] | length')
FAILED=$(echo "$CONTAINERS" | jq \
'[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not))] | length')
echo "🔄 [$((COUNT * SLEEP_INTERVAL))s / $((MAX_RETRY * SLEEP_INTERVAL))s] Container baru running: ${NEW_RUNNING} | Gagal: ${FAILED}"
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status) | id: \(.Id[:12])"'
if [ "$FAILED" -gt "0" ]; then
echo ""
echo "❌ Ada container yang crash!"
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not)) | " → \(.Names[0]) | \(.Status)"'
exit 1
fi
if [ "$NEW_RUNNING" -gt "0" ]; then
# Cleanup dangling images setelah redeploy sukses
echo "🧹 Membersihkan dangling images..."
curl -s -X POST "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/images/prune" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filters":{"dangling":["true"]}}' | jq -r '" Reclaimed: \(.SpaceReclaimed // 0 | . / 1073741824 | tostring | .[0:5]) GB"'
echo "✅ Cleanup selesai!"
echo ""
echo "✅ Stack $STACK_NAME berhasil di-redeploy dengan image baru dan running!"
exit 0
fi
done
echo ""
echo "❌ Timeout $((MAX_RETRY * SLEEP_INTERVAL))s! Container baru tidak kunjung running."
echo " Kemungkinan image masih dalam proses pull atau ada error di server."
exit 1

View File

@@ -1,55 +0,0 @@
name: test workflows
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment (e.g., staging, production)"
required: true
default: "staging"
version:
description: "Version to deploy"
required: false
default: "latest"
env:
APP_NAME: desa-darmasaba-action
WA_PHONE: "6289697338821,6289697338822"
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checkout kode sumber
- name: Checkout code
uses: actions/checkout@v3
# Setup Bun
- name: Setup Bun
uses: oven-sh/setup-bun@v2
# Create log file
- name: Create log file
run: touch build.txt
# Step 1: Set BRANCH_NAME based on event type
- name: Set BRANCH_NAME
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
else
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
fi
# Step 2: Generate APP_VERSION dynamically
- name: Set APP_VERSION
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
# Step 3: Kirim notifikasi ke API build Start
- name: Notify start build
run: |
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
for PHONE in "${PHONES[@]}"; do
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nenv:${{ inputs.environment }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
done

11
.gitignore vendored
View File

@@ -29,7 +29,12 @@ yarn-error.log*
.pnpm-debug.log*
# env
# env local files (keep .env.example)
.env*
!.env.example
# QC
QC
# vercel
.vercel
@@ -41,10 +46,12 @@ next-env.d.ts
# uploads
/uploads
# download
/download
# cache
/cache
.github/
.env.*
*.tar.gz

13
.qwen/settings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"mcpServers": {
"playwright-mcp": {
"command": "npx",
"args": [
"-y",
"playwright-mcp@latest"
],
"timeout": 60000
}
},
"$version": 3
}

9
.qwen/settings.json.orig Normal file
View File

@@ -0,0 +1,9 @@
{
"mcpServers": {
"playwright-mcp": {
"command": "npx",
"args": ["-y", "playwright-mcp@latest"],
"timeout": 60000
}
}
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": []
}

167
AGENTS.md Normal file
View File

@@ -0,0 +1,167 @@
# AGENTS.md
This file contains essential information for agentic coding agents working in the desa-darmasaba repository.
## Project Overview
Desa Darmasaba is a Next.js 15 application for village management services in Badung, Bali. It uses:
- **Framework**: Next.js 15 with App Router
- **Language**: TypeScript with strict mode
- **Styling**: Mantine UI components with custom CSS
- **Backend**: Elysia.js API server integrated with Next.js
- **Database**: PostgreSQL with Prisma ORM
- **State Management**: Jotai for global state
- **Authentication**: JWT with iron-session
## Build Commands
```bash
# Development
npm run dev
# Production build
npm run build
# Start production server
npm start
# Database seeding
bun run prisma/seed.ts
# Linting (ESLint)
npx eslint .
# Type checking
npx tsc --noEmit
# Prisma operations
npx prisma generate
npx prisma db push
npx prisma studio
```
## Running Tests
Currently no test framework is configured. When adding tests:
- Set up test scripts in package.json
- Consider Jest or Vitest for unit testing
- Use Playwright for E2E testing
- Update this section with specific test commands
## Code Style Guidelines
### Imports
- Use absolute imports with `@/` alias (configured in tsconfig.json)
- Group imports: external libraries first, then internal modules
- Keep import statements organized and remove unused imports
```typescript
// External libraries
import { useState } from 'react'
import { Button, Stack } from '@mantine/core'
// Internal modules
import ApiFetch from '@/lib/api-fetch'
import { MyComponent } from '@/components/my-component'
```
### TypeScript Configuration
- Strict mode enabled (`"strict": true`)
- Target: ES2017
- Module resolution: bundler
- Path alias: `@/*` maps to `./src/*`
### Naming Conventions
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
- **Variables/Functions**: camelCase
- **Constants**: UPPER_SNAKE_CASE
- **Database Models**: PascalCase (Prisma convention)
### Error Handling
- Use try-catch blocks for async operations
- Implement proper error boundaries in React components
- Log errors appropriately without exposing sensitive data
- Use Zod for runtime validation and type safety
### API Structure
- Backend uses Elysia.js with TypeScript
- API routes are in `src/app/api/[[...slugs]]/` directory
- Use treaty client for type-safe API calls
- Follow RESTful conventions for endpoints
- Include proper HTTP status codes and error responses
### Database Operations
- Use Prisma client from `@/lib/prisma.ts`
- Database connection includes graceful shutdown handling
- Use transactions for complex operations
- Implement proper error handling for database queries
### Component Guidelines
- Use functional components with hooks
- Implement proper prop types with TypeScript interfaces
- Use Mantine components for UI consistency
- Follow atomic design principles when possible
- Add loading states and error states for async operations
### State Management
- Use Jotai atoms for global state
- Keep local state in components when possible
- Use React Query (SWR) for server state caching
- Implement optimistic updates for better UX
### Styling
- Primary: Mantine UI components
- Use Mantine theme system for customization
- Custom CSS should be minimal and scoped
- Follow responsive design principles
- Use semantic HTML5 elements
### Environment Variables
- Use `.env.local` for development
- Prefix public variables with `NEXT_PUBLIC_`
- Never commit environment files to version control
- Use proper typing for environment variables
### File Organization
```
src/
├── app/ # Next.js app router pages
├── components/ # Reusable React components
├── lib/ # Utility functions and configurations
├── state/ # Jotai atoms and state management
├── types/ # TypeScript type definitions
└── con/ # Constants and static data
```
### Security Practices
- Validate all user inputs with Zod schemas
- Use JWT tokens for authentication
- Implement proper CORS configuration
- Never expose database credentials or API keys
- Use HTTPS in production
- Implement rate limiting for sensitive endpoints
### Performance Considerations
- Use Next.js Image optimization
- Implement proper caching strategies
- Use React.memo for expensive components
- Optimize bundle size with dynamic imports
- Use Prisma query optimization
## Development Workflow
1. Always run type checking before committing: `npx tsc --noEmit`
2. Run linting to catch style issues: `npx eslint .`
3. Test database changes with `npx prisma db push`
4. Use the integrated Swagger docs at `/api/docs` for API testing
5. Check environment variables are properly configured
6. Verify responsive design on different screen sizes
## Important Notes
- The application uses a custom Elysia.js server integrated with Next.js API routes
- Image uploads are handled through `/api/upl-img-single` endpoint
- Database seeding is done with Bun runtime
- The app supports Indonesian locale (id_ID) for SEO and content
- CORS is configured to allow cross-origin requests during development

73
AUDIT_REPORT.md Normal file
View File

@@ -0,0 +1,73 @@
# Engineering Audit Report: Desa Darmasaba
**Status:** Production Readiness Review (Critical)
**Auditor:** Staff Technical Architect
---
## 📊 Executive Summary & Scores
| Category | Score | Status |
| :--- | :---: | :--- |
| **Project Architecture** | 3/10 | 🔴 Critical Failure |
| **Code Quality** | 4/10 | 🟠 Poor |
| **Performance** | 5/10 | 🟡 Mediocre |
| **Security** | 5/10 | 🟠 Risk Detected |
| **Production Readiness** | 2/10 | 🔴 Not Ready |
---
## 🏗️ 1. Project Architecture
The project suffers from a **"Frankenstein Architecture"**. It attempts to run a full Elysia.js instance inside a Next.js Catch-All route.
- **Fractured Backend:** Logic is split between standard Next.js routes (`/api/auth`) and embedded Elysia modules.
- **Stateful Dependency:** Reliance on local filesystem (`WIBU_UPLOAD_DIR`) makes the application impossible to deploy on modern serverless platforms like Vercel.
- **Polluted Namespace:** Routing tree contains "test/coba" folders (`src/app/coba`, `src/app/percobaan`) that would be accessible in production.
## ⚛️ 2. Frontend Engineering (React / Next.js)
- **State Management Chaos:** Simultaneous use of `Valtio`, `Jotai`, `React Context`, and `localStorage`.
- **Tight Coupling:** Public pages (`/darmasaba`) import state directly from Admin internal states (`/admin/(dashboard)/_state`).
- **Heavy Client-Side Logic:** Logic that belongs in Server Actions or Hooks is embedded in presentational components (e.g., `Footer.tsx`).
## 📡 3. Backend / API Design
- **Framework Overhead:** Running Elysia inside Next.js adds unnecessary cold-boot overhead and complexity.
- **Weak Validation:** Widespread use of `as Type` casting in API handlers instead of runtime validation (Zod/Schema).
- **Service Integration:** OTP codes are sent via external `GET` requests with sensitive data in the query string—a major logging risk.
## 🗄️ 4. Database & Data Modeling (Prisma)
- **Schema Over-Normalization:** ~2000 lines of schema. Every minor content type (e.g., `LambangDesa`) is a separate table instead of a unified CMS model.
- **Polymorphic Monolith:** `FileStorage` is a "god table" with optional relations to ~40 other tables, creating a massive bottleneck and data integrity risk.
- **Connection Mismanagement:** Manual `prisma.$disconnect()` in API routes kills connection pooling performance.
## 🚀 5. Performance Engineering
- **Bypassing Optimization:** Custom `/api/utils/img` endpoint bypasses `next/image` optimization, serving uncompressed assets.
- **Aggressive Polling:** Client-side 30s polling for notifications is battery-draining and inefficient compared to SSE or SWR.
## 🔒 6. Security Audit
- **Insecure OTP Delivery:** Credentials passed as URL parameters to the WhatsApp service.
- **File Upload Risks:** Potential for Arbitrary File Upload due to direct local filesystem writes without rigorous sanitization.
## 🧹 7. Code Quality
- **Inconsistency:** Mixed English/Indonesian naming (e.g., `nomor` vs `createdAt`).
- **Artifacts:** Root directory is littered with scratch files: `xcoba.ts`, `xx.ts`, `test.txt`.
---
## 🚩 Top 10 Critical Problems
1. **Architectural Fracture:** Embedding Elysia inside Next.js creates a "split-brain" system.
2. **Serverless Incompatibility:** Dependency on local disk storage for uploads.
3. **Database Bloat:** Over-complicated schema with a fragile `FileStorage` monolith.
4. **State Fragmentation:** Mixed usage of Jotai and Valtio without a clear standard.
5. **Credential Leakage:** OTP codes sent via GET query parameters.
6. **Poor Cleanup:** Trial/Test folders and files committed to the production source.
7. **Asset Performance:** Bypassing Next.js image optimization.
8. **Coupling:** High dependency between public UI and internal Admin state.
9. **Type Safety:** Manual casting in APIs instead of runtime validation.
10. **Connection Pooling:** Inefficient Prisma connection management.
---
## 🛠️ Tech Lead Refactoring Priorities
1. **Unify the API:** Decommission the Elysia wrapper. Port all logic to standard Next.js Route Handlers with Zod validation.
2. **Stateless Storage:** Implement an S3-compatible adapter for all file uploads. Remove `fs` usage.
3. **Schema Consolidation:** Refactor the schema to use generic content models where possible.
4. **Standardize State:** Choose one global state manager and migrate all components.
5. **Project Sanitization:** Delete all `coba`, `percobaan`, and scratch files (`xcoba.ts`, etc.).

191
DEV-INSPECTOR-ANALYSIS.md Normal file
View File

@@ -0,0 +1,191 @@
# Dev Inspector - Analisis & Rekomendasi untuk Project Desa Darmasaba
## 📋 Ringkasan Analisis
Dokumen `dev-inspector-click-to-source.md` **TIDAK dapat diterapkan langsung** ke project ini karena perbedaan arsitektur fundamental.
## 🔍 Perbedaan Arsitektur
| Syarat di Dokumen | Project Desa Darmasaba | Status |
|-------------------|------------------------|--------|
| **Vite sebagai bundler** | Next.js 15 (Webpack/Turbopack) | ❌ Tidak kompatibel |
| **Elysia + Vite middlewareMode** | Next.js App Router + Elysia sebagai API handler | ❌ Berbeda |
| **React** | ✅ React 19 | ✅ Kompatibel |
| **Bun runtime** | ✅ Bun | ✅ Kompatibel |
## ✅ Solusi: Next.js Sudah Punya Built-in Click-to-Source
Next.js memiliki fitur **click-to-source bawaan** yang bekerja tanpa setup tambahan:
### Cara Menggunakan
1. **Pastikan dalam development mode:**
```bash
bun run dev
```
2. **Klik elemen dengan modifier key:**
- **macOS**: `Option` + `Click` (atau `` + `Click`)
- **Windows/Linux**: `Alt` + `Click`
3. **File akan terbuka di editor** pada baris dan kolom yang tepat
### Syarat Agar Berfungsi
1. **Editor harus ada di PATH**
VS Code biasanya sudah terdaftar. Jika menggunakan editor lain, set:
```bash
# Untuk Cursor
export EDITOR=cursor
# Untuk Windsurf
export EDITOR=windsurf
# Untuk Sublime Text
export EDITOR=subl
```
2. **Hanya berfungsi di development mode**
- Fitur ini otomatis tree-shaken di production
- Zero overhead di production build
3. **Browser DevTools harus terbuka** (beberapa browser memerlukan ini)
## 🎯 Rekomendasi untuk Project Ini
### Opsi 1: Gunakan Built-in Next.js (DIREKOMENDASIKAN)
**Kelebihan:**
- ✅ Zero setup
- ✅ Maintain oleh Vercel
- ✅ Otomatis compatible dengan Next.js updates
- ✅ Zero production overhead
**Kekurangan:**
- ⚠️ Hotkey berbeda (`Option+Click` vs `Ctrl+Shift+Cmd+C`)
- ⚠️ Tidak ada visual overlay/tooltip seperti di dokumen
**Cara:**
Tidak perlu melakukan apapun - fitur sudah aktif saat `bun run dev`.
### Opsi 2: Custom Implementation (JIKA DIPERLUKAN)
Jika ingin visual overlay dan tooltip seperti di dokumen, bisa dibuat custom component dengan pendekatan berbeda:
#### Arsitektur Alternatif untuk Next.js
```
BUILD TIME (Next.js/Webpack):
.tsx/.jsx file
→ [Custom Webpack Loader] inject data-inspector-* attributes
→ [Next.js internal transform] JSX to React.createElement
→ Browser menerima elemen dengan attributes
RUNTIME (Browser):
[SAMA seperti dokumen - DevInspector component]
BACKEND (Next.js API Route):
/__open-in-editor → Bun.spawn([editor, '--goto', 'file:line:col'])
```
#### Komponen yang Dibutuhkan:
1. **Custom Webpack Loader** (bukan Vite Plugin)
- Inject attributes via webpack transform
- Taruh di `next.config.ts` webpack config
2. **DevInspector Component** (sama seperti dokumen)
- Browser runtime untuk handle hotkey & klik
3. **API Route `/__open-in-editor`**
- Buat sebagai Next.js API route: `src/app/api/__open-in-editor/route.ts`
- HARUS bypass auth middleware
4. **Conditional Import** (sama seperti dokumen)
```tsx
const InspectorWrapper = process.env.NODE_ENV === 'development'
? (await import('./DevInspector')).DevInspector
: ({ children }) => <>{children}</>
```
#### Implementasi Steps:
Jika Anda ingin melanjutkan dengan custom implementation, berikut steps:
1. ✅ Buat `src/components/DevInspector.tsx` (copy dari dokumen)
2. ⚠️ Buat webpack loader untuk inject attributes (perlu research)
3. ✅ Buat API route `src/app/api/__open-in-editor/route.ts`
4. ✅ Wrap root layout dengan DevInspector
5. ✅ Set `REACT_EDITOR` di `.env`
**Peringatan:**
- Webpack loader lebih kompleks daripada Vite plugin
- Mungkin ada edge cases dengan Next.js internals
- Perlu maintenance ekstra saat Next.js update
## 📊 Perbandingan
| Fitur | Built-in Next.js | Custom Implementation |
|-------|------------------|----------------------|
| Setup | ✅ Zero | ⚠️ Medium |
| Visual Overlay | ❌ Tidak ada | ✅ Ada |
| Tooltip | ❌ Tidak ada | ✅ Ada |
| Hotkey | `Option+Click` | Custom (bisa disesuaikan) |
| Maintenance | ✅ Vercel | ⚠️ Manual |
| Compatibility | ✅ Guaranteed | ⚠️ Perlu testing |
| Production Impact | ✅ Zero | ✅ Zero (dengan conditional import) |
## 🎯 Kesimpulan
**Rekomendasi: Gunakan Built-in Next.js**
Alasan:
1. ✅ Sudah tersedia - tidak perlu setup
2. ✅ Lebih stabil - maintain oleh Vercel
3. ✅ Lebih simple - tidak ada custom code
4. ✅ Future-proof - otomatis update dengan Next.js
**Custom implementation hanya diperlukan jika:**
- Anda sangat membutuhkan visual overlay & tooltip
- Anda ingin hotkey yang sama persis (`Ctrl+Shift+Cmd+C`)
- Anda punya waktu untuk maintenance
## 🚀 Quick Start - Built-in Feature
Untuk menggunakan click-to-source bawaan Next.js:
1. Jalankan development server:
```bash
bun run dev
```
2. Buka browser ke `http://localhost:3000`
3. Tahan `Option` (macOS) atau `Alt` (Windows/Linux)
4. Cursor akan berubah menjadi crosshair
5. Klik elemen mana pun - file akan terbuka di editor
6. **Opsional**: Set editor di `.env`:
```env
# .env.local
EDITOR=code # atau cursor, windsurf, subl
```
## 📝 Notes
- Fitur ini hanya aktif di development mode (`NODE_ENV=development`)
- Production build (`bun run build`) otomatis menghilangkan fitur ini
- Next.js menggunakan mekanisme yang mirip (source mapping) untuk menentukan lokasi component
- Jika editor tidak terbuka, pastikan:
- Editor sudah terinstall dan ada di PATH
- Browser DevTools terbuka (beberapa browser require ini)
- Anda menggunakan development server, bukan production
## 🔗 Referensi
- [Next.js Documentation - Launching Editor](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactStrictMode)
- [React DevTools - Component Inspection](https://react.dev/learn/react-developer-tools)
- [Original Dev Inspector Document](./dev-inspector-click-to-source.md)

67
Dockerfile Normal file
View File

@@ -0,0 +1,67 @@
# ==============================
# Stage 1: Builder
# ==============================
FROM oven/bun:1-debian AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libc6 \
git \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY package.json bun.lockb* ./
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN bun install --frozen-lockfile
COPY . .
RUN cp .env.example .env || true
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
RUN bunx prisma generate
# Generate API types (opsional)
RUN bun run gen:api || echo "tidak ada gen api"
RUN bun run build
# ==============================
# Stage 2: Runner (Production)
# ==============================
FROM oven/bun:1-debian AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextjs:nodejs /app/next.config.* ./
USER nextjs
EXPOSE 3000
CMD ["bun", "start"]

62
GEMINI.md Normal file
View File

@@ -0,0 +1,62 @@
# Project: Desa Darmasaba
## Project Overview
The `desa-darmasaba` project is a Next.js (version 15+) application developed with TypeScript. It serves as an official platform for Desa Darmasaba (a village in Badung, Bali), offering various public services, news, and detailed village profiles.
**Key Technologies:**
* **Frontend Framework:** Next.js (v15+) with React (v19+)
* **Language:** TypeScript
* **UI Library:** Mantine UI
* **Database ORM:** Prisma (v6+)
* **Database:** PostgreSQL (as configured in `prisma/schema.prisma`)
* **API Framework:** Elysia (used for API routes, as seen in dependencies)
* **State Management:** Potentially Jotai and Valtio (listed in dependencies)
* **Image Processing:** Sharp
* **Package Manager:** Likely Bun, given `bun.lockb` and the `prisma:seed` script.
The application architecture follows the Next.js App Router structure, with comprehensive data models defined in `prisma/schema.prisma` covering various domains like public information, health, security, economy, innovation, environment, and education. It also includes configurations for image handling and caching.
## Building and Running
This project uses `bun` as the package manager. Ensure Bun is installed to run these commands.
* **Install Dependencies:**
```bash
bun install
```
* **Development Server:**
Runs the Next.js development server.
```bash
bun run dev
```
* **Build for Production:**
Builds the Next.js application for production deployment.
```bash
bun run build
```
* **Start Production Server:**
Starts the Next.js application in production mode.
```bash
bun run start
```
* **Database Seeding:**
Executes the Prisma seeding script to populate the database.
```bash
bun run prisma:seed
```
## Development Conventions
* **Coding Language:** TypeScript is strictly enforced.
* **Frontend Framework:** Next.js App Router for page and component structuring.
* **UI/UX:** Adherence to Mantine UI component library for consistent styling and user experience.
* **Database Interaction:** Prisma ORM is used for all database operations, with a PostgreSQL database.
* **Linting:** ESLint is configured with `next/core-web-vitals` and `next/typescript` to maintain code quality and adherence to Next.js and TypeScript best practices.
* **Styling:** PostCSS is used, with `postcss-preset-mantine` and `postcss-simple-vars` defining Mantine-specific breakpoints and other CSS variables.
* **Imports:** Absolute imports are configured using `@/*` which resolves to the `src/` directory.

173
MUSIK_CREATE_ANALYSIS.md Normal file
View File

@@ -0,0 +1,173 @@
# Musik Desa - Create Feature Analysis
## Error Summary
**Error**: `ERR_BLOCKED_BY_CLIENT` saat create musik di staging environment
## Root Cause Analysis
### 1. **CORS Configuration Issue** (Primary)
File: `src/app/api/[[...slugs]]/route.ts`
The CORS configuration has specific origins listed:
```typescript
const corsConfig = {
origin: [
"http://localhost:3000",
"http://localhost:3001",
"https://cld-dkr-desa-darmasaba-stg.wibudev.com",
"https://cld-dkr-staging-desa-darmasaba.wibudev.com",
"*",
],
// ...
}
```
**Problem**: The wildcard `*` is at the end, but some browsers don't respect it when `credentials: true` is set.
### 2. **API Fetch Base URL** (Secondary)
File: `src/lib/api-fetch.ts`
```typescript
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
```
**Problem**:
- In staging, this might still default to `http://localhost:3000`
- Mixed content (HTTPS frontend → HTTP API) gets blocked by browsers
- The `NEXT_PUBLIC_BASE_URL` environment variable might not be set in staging
### 3. **File Storage Upload Path** (Tertiary)
File: `src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts`
```typescript
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
```
**Problem**: If `WIBU_UPLOAD_DIR` is not set or points to a non-writable location, uploads will fail silently.
## Solution
### Fix 1: Update CORS Configuration
**File**: `src/app/api/[[...slugs]]/route.ts`
```typescript
// Move wildcard to first position and ensure it works with credentials
const corsConfig = {
origin: [
"*", // Allow all origins (for staging flexibility)
"http://localhost:3000",
"http://localhost:3001",
"https://cld-dkr-desa-darmasaba-stg.wibudev.com",
"https://cld-dkr-staging-desa-darmasaba.wibudev.com",
"https://desa-darmasaba-stg.wibudev.com"
],
methods: ["GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"] as HTTPMethod[],
allowedHeaders: ["Content-Type", "Authorization", "Accept"],
exposedHeaders: ["Content-Range", "X-Content-Range"],
maxAge: 86400, // 24 hours
credentials: true,
};
```
### Fix 2: Add Environment Variable Validation
**File**: `.env.example` (update)
```bash
# Application Configuration
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# For staging/production, set this to your actual domain
# NEXT_PUBLIC_BASE_URL=https://cld-dkr-desa-darmasaba-stg.wibudev.com
```
### Fix 3: Update API Fetch to Handle Relative URLs
**File**: `src/lib/api-fetch.ts`
```typescript
import { AppServer } from '@/app/api/[[...slugs]]/route'
import { treaty } from '@elysiajs/eden'
// Use relative URL for better deployment flexibility
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '/'
const ApiFetch = treaty<AppServer>(BASE_URL)
export default ApiFetch
```
### Fix 4: Add Error Handling in Create Page
**File**: `src/app/admin/(dashboard)/musik/create/page.tsx`
Add better error logging to diagnose issues:
```typescript
const handleSubmit = async () => {
// ... validation ...
try {
setIsSubmitting(true);
// Upload cover image
const coverRes = await ApiFetch.api.fileStorage.create.post({
file: coverFile,
name: coverFile.name,
});
if (!coverRes.data?.data?.id) {
console.error('Cover upload failed:', coverRes);
return toast.error('Gagal mengunggah cover, silakan coba lagi');
}
// ... rest of the code ...
} catch (error) {
console.error('Error creating musik:', {
error,
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
});
toast.error('Terjadi kesalahan saat membuat musik');
} finally {
setIsSubmitting(false);
}
};
```
## Testing Checklist
### Local Development
- [ ] Test create musik with cover image and audio file
- [ ] Verify CORS headers in browser DevTools Network tab
- [ ] Check that file uploads are saved to correct directory
### Staging Environment
- [ ] Set `NEXT_PUBLIC_BASE_URL` to staging domain
- [ ] Verify HTTPS is used for all API calls
- [ ] Check browser console for mixed content warnings
- [ ] Verify `WIBU_UPLOAD_DIR` is set and writable
- [ ] Test create musik end-to-end
## Additional Notes
### ERR_BLOCKED_BY_CLIENT Common Causes:
1. **CORS policy blocking** - Most likely cause
2. **Ad blockers** - Can block certain API endpoints
3. **Mixed content** - HTTPS page making HTTP requests
4. **Content Security Policy (CSP)** - Restrictive CSP headers
5. **Browser extensions** - Privacy/security extensions blocking requests
### Debugging Steps:
1. Open browser DevTools → Network tab
2. Try to create musik
3. Look for failed requests (red status)
4. Check the "Headers" tab for:
- Request URL (should be correct domain)
- Response headers (should have `Access-Control-Allow-Origin`)
- Status code (4xx/5xx indicates server-side issue)
5. Check browser console for CORS errors
## Recommended Next Steps
1. **Immediate**: Update CORS configuration to allow staging domain
2. **Short-term**: Add proper environment variable validation
3. **Long-term**: Implement proper error boundaries and logging

232
QWEN.md Normal file
View File

@@ -0,0 +1,232 @@
# Desa Darmasaba - Village Management System
## Project Overview
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
### Key Technologies
- **Framework**: Next.js 15 with App Router
- **Language**: TypeScript with strict mode
- **Styling**: Mantine UI components with custom CSS
- **Backend**: Elysia.js API server integrated with Next.js
- **Database**: PostgreSQL with Prisma ORM
- **State Management**: Valtio for global state
- **Authentication**: JWT with iron-session
### Architecture
The application follows a modular architecture with:
- A main frontend built with Next.js and Mantine UI
- An integrated Elysia.js API server for backend operations
- Prisma ORM for database interactions
- File storage integration with Seafile
- Multiple domain-specific modules (PPID, health, security, education, etc.)
## Building and Running
### Prerequisites
- Node.js (with Bun runtime)
- PostgreSQL database
- Seafile server for file storage
### Setup Instructions
1. Install dependencies:
```bash
bun install
```
2. Set up environment variables in `.env.local`:
```
DATABASE_URL=your_postgresql_connection_string
SEAFILE_TOKEN=your_seafile_token
SEAFILE_REPO_ID=your_seafile_repo_id
SEAFILE_BASE_URL=your_seafile_base_url
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
SEAFILE_URL=your_seafile_api_url
WIBU_UPLOAD_DIR=your_upload_directory
```
3. Generate Prisma client:
```bash
bunx prisma generate
```
4. Push database schema:
```bash
bunx prisma db push
```
5. Seed the database:
```bash
bun run prisma/seed.ts
```
6. Run the development server:
```bash
bun run dev
```
### Available Scripts
- `bun run dev` - Start development server
- `bun run build` - Build for production
- `bun run start` - Start production server
- `bun run prisma/seed.ts` - Run database seeding
- `bunx prisma generate` - Generate Prisma client
- `bunx prisma db push` - Push schema changes to database
- `bunx prisma studio` - Open Prisma Studio GUI
## Development Conventions
### Code Structure
```
src/
├── app/ # Next.js app router pages
│ ├── admin/ # Admin dashboard pages
│ ├── api/ # API routes with Elysia.js
│ ├── darmasaba/ # Public-facing village pages
│ └── ...
├── con/ # Constants and configuration
├── hooks/ # React hooks
├── lib/ # Utility functions and configurations
├── middlewares/ # Next.js middleware
├── state/ # Global state management
├── store/ # Additional state management
├── types/ # TypeScript type definitions
└── utils/ # Utility functions
```
### Import Conventions
- Use absolute imports with `@/` alias (configured in tsconfig.json)
- Group imports: external libraries first, then internal modules
- Keep import statements organized and remove unused imports
```typescript
// External libraries
import { useState } from 'react'
import { Button, Stack } from '@mantine/core'
// Internal modules
import ApiFetch from '@/lib/api-fetch'
import { MyComponent } from '@/components/my-component'
```
### TypeScript Configuration
- Strict mode enabled (`"strict": true`)
- Target: ES2017
- Module resolution: bundler
- Path alias: `@/*` maps to `./src/*`
### Naming Conventions
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
- **Variables/Functions**: camelCase
- **Constants**: UPPER_SNAKE_CASE
- **Database Models**: PascalCase (Prisma convention)
### Error Handling
- Use try-catch blocks for async operations
- Implement proper error boundaries in React components
- Log errors appropriately without exposing sensitive data
- Use Zod for runtime validation and type safety
### API Structure
- Backend uses Elysia.js with TypeScript
- API routes are in `src/app/api/[[...slugs]]/` directory
- Use treaty client for type-safe API calls
- Follow RESTful conventions for endpoints
- Include proper HTTP status codes and error responses
### Database Operations
- Use Prisma client from `@/lib/prisma.ts`
- Database connection includes graceful shutdown handling
- Use transactions for complex operations
- Implement proper error handling for database queries
### Component Guidelines
- Use functional components with hooks
- Implement proper prop types with TypeScript interfaces
- Use Mantine components for UI consistency
- Follow atomic design principles when possible
- Add loading states and error states for async operations
### State Management
- Use Valtio proxies for global state
- Keep local state in components when possible
- Use SWR for server state caching
- Implement optimistic updates for better UX
### Styling
- Primary: Mantine UI components
- Use Mantine theme system for customization
- Custom CSS should be minimal and scoped
- Follow responsive design principles
- Use semantic HTML5 elements
### Security Practices
- Validate all user inputs with Zod schemas
- Use JWT tokens for authentication
- Implement proper CORS configuration
- Never expose database credentials or API keys
- Use HTTPS in production
- Implement rate limiting for sensitive endpoints
### Performance Considerations
- Use Next.js Image optimization
- Implement proper caching strategies
- Use React.memo for expensive components
- Optimize bundle size with dynamic imports
- Use Prisma query optimization
## Domain Modules
The application is organized into several domain modules:
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
2. **Health**: Health facilities, programs, emergency response, disease information
3. **Security**: Community security, emergency contacts, crime prevention
4. **Education**: Schools, scholarships, educational programs
5. **Economy**: Local markets, BUMDes, employment data
6. **Environment**: Environmental data, conservation, waste management
7. **Innovation**: Digital services, innovation programs
8. **Culture**: Village traditions, music, cultural preservation
Each module has its own section in both the admin panel and public-facing areas.
## File Storage Integration
The application integrates with Seafile for file storage, with specific handling for:
- Images and documents
- Public sharing capabilities
- CDN URL generation
- Batch processing of assets
## Testing
Currently no formal test framework is configured. When adding tests:
- Consider Jest or Vitest for unit testing
- Use Playwright for E2E testing
- Update this section with specific test commands
## Deployment
The application includes deployment scripts in the `NOTE.md` file that outline:
- Automated deployment with GitHub API integration
- Environment-specific configurations
- PM2 process management
- Release management with versioning
## Troubleshooting
Common issues and solutions:
- **API endpoints returning 404**: Check that environment variables are properly configured
- **Database connection errors**: Verify DATABASE_URL in environment variables
- **File upload issues**: Ensure Seafile integration is properly configured
- **Build failures**: Run `bunx prisma generate` before building
## Development Workflow
1. Always run type checking before committing: `bunx tsc --noEmit`
2. Run linting to catch style issues: `bun run eslint .`
3. Test database changes with `bunx prisma db push`
4. Use the integrated Swagger docs at `/api/docs` for API testing
5. Check environment variables are properly configured
6. Verify responsive design on different screen sizes

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import ApiFetch from '@/lib/api-fetch';
describe('FileStorage API', () => {
it('should fetch a list of files from /api/fileStorage/findMany', async () => {
const response = await ApiFetch.api.fileStorage.findMany.get();
expect(response.status).toBe(200);
const responseBody = response.data as any;
expect(responseBody.data).toBeInstanceOf(Array);
expect(responseBody.data.length).toBe(2);
expect(responseBody.data[0].name).toBe('file1.jpg');
});
it('should create a file using /api/fileStorage/create', async () => {
const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
const response = await ApiFetch.api.fileStorage.create.post({
file: mockFile,
name: 'hello.png',
});
expect(response.status).toBe(200);
const responseBody = response.data as any;
expect(responseBody.data.realName).toBe('hello.png');
expect(responseBody.data.id).toBe('3');
});
});

View File

@@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';
test('homepage has correct title and content', async ({ page }) => {
await page.goto('/');
// Wait for the redirect to /darmasaba
await page.waitForURL('/darmasaba');
// Check for the main heading
await expect(page.getByText('DARMASABA', { exact: true })).toBeVisible();
});

View File

@@ -0,0 +1,43 @@
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('http://localhost:3000/api/fileStorage/findMany', () => {
return HttpResponse.json({
data: [
{ id: '1', name: 'file1.jpg', url: '/uploads/file1.jpg' },
{ id: '2', name: 'file2.png', url: '/uploads/file2.png' },
],
meta: {
page: 1,
limit: 10,
total: 2,
totalPages: 1,
},
});
}),
http.post('http://localhost:3000/api/fileStorage/create', async ({ request }) => {
const data = await request.formData();
const file = data.get('file') as File;
const name = data.get('name') as string;
if (!file) {
return new HttpResponse(null, { status: 400 });
}
return HttpResponse.json({
data: {
id: '3',
name: 'generated-nanoid',
path: `/uploads/generated-nanoid`,
link: `/uploads/generated-nanoid`,
realName: name,
mimeType: file.type,
category: "uncategorized",
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
}
});
}),
];

View File

@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);

7
__tests__/setup.ts Normal file
View File

@@ -0,0 +1,7 @@
import '@testing-library/jest-dom';
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

49
biome.json Normal file
View File

@@ -0,0 +1,49 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"experimentalScannerIgnores": [
"node_modules",
".next",
"out",
"public"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "warn",
"noUnusedImports": "warn"
},
"suspicious": {
"noExplicitAny": "warn"
},
"style": {
"noNonNullAssertion": "warn"
},
"complexity": {
"noForEach": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
}
}

2302
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

169
darkMode.md Normal file
View File

@@ -0,0 +1,169 @@
# 🌙 Dark Mode Design Specification
## Admin Darmasaba Dashboard & CMS
Dokumen ini mendefinisikan standar **Dark Mode UI** agar:
- nyaman di mata
- konsisten
- tidak flat
- tetap profesional untuk aplikasi pemerintahan
---
## 🎨 Color Palette (Dark Mode)
### Background Layers
| Layer | Token | Warna | Fungsi |
|------|------|------|------|
| Base | `--bg-base` | `#0B1220` | Background utama aplikasi |
| App | `--bg-app` | `#0F172A` | Area kerja utama |
| Card | `--bg-card` | `#162235` | Card / container |
| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input |
---
### Border & Divider
| Token | Warna | Catatan |
|-----|------|--------|
| `--border-default` | `#2A3A52` | Border utama |
| `--border-soft` | `#22314A` | Divider halus |
> ❗ Hindari border terlalu tipis (`opacity < 20%`)
---
### Text Colors
| Jenis | Token | Warna |
|-----|------|------|
| Primary | `--text-primary` | `#E5E7EB` |
| Secondary | `--text-secondary` | `#9CA3AF` |
| Muted | `--text-muted` | `#6B7280` |
| Inverse | `--text-inverse` | `#020617` |
---
### Accent & Action
| Fungsi | Warna |
|------|------|
| Primary Action | `#3B82F6` |
| Hover | `#2563EB` |
| Active | `#1D4ED8` |
| Link | `#60A5FA` |
---
### Status Colors
| Status | Warna |
|------|------|
| Success | `#22C55E` |
| Warning | `#FACC15` |
| Error | `#EF4444` |
| Info | `#38BDF8` |
---
## 🧱 Layout Rules
### Sidebar
- Background: `--bg-app`
- Active menu:
- Background: `rgba(59,130,246,0.15)`
- Text: Primary
- Indicator: kiri (23px accent bar)
- Hover:
- Background: `rgba(255,255,255,0.04)`
---
### Header / Topbar
- Background: `linear-gradient(#0F172A → #0B1220)`
- Border bawah wajib (`--border-soft`)
- Icon:
- Default: muted
- Hover: primary
---
## 📦 Card & Section
### Card
- Background: `--bg-card`
- Border: `--border-default`
- Radius: 1216px
- Jangan pakai shadow hitam
### Section Header
- Font weight lebih besar
- Text: primary
- Spacing jelas dari konten
---
## 📊 Table (Dark Mode Friendly)
### Table Header
- Background: `--bg-surface`
- Text: secondary
- Font weight: medium
### Table Row
- Default: transparent
- Hover:
- Background: `rgba(255,255,255,0.03)`
- Divider antar row wajib terlihat
### Link di Table
- Warna link **lebih terang dari text**
- Hover underline
---
## 🔘 Button Rules
### Primary Button
- Background: Primary Action
- Text: Inverse
- Hover: darker shade
### Secondary Button
- Background: transparent
- Border: `--border-default`
- Text: primary
### Icon Button
- Default: muted
- Hover: primary + bg soft
---
## 🧭 Tab Navigation
- Inactive:
- Text: muted
- Active:
- Background: `rgba(59,130,246,0.15)`
- Text: primary
- Icon ikut berubah
---
## 🌗 Dark vs Light Mode Rule
- Layout, spacing, typography **HARUS SAMA**
- Yang boleh beda:
- warna
- border intensity
- background layer
> ❌ Jangan ganti struktur UI antara dark & light
---
## ✅ Dark Mode Checklist
- [ ] Kontras teks terbaca
- [ ] Active state jelas
- [ ] Hover terasa hidup
- [ ] Tidak flat
- [ ] Tidak silau
---
Dokumen ini adalah **single source of truth** untuk Dark Mode.

View File

@@ -0,0 +1,553 @@
# Skill: Dev Inspector — Click-to-Source untuk Bun + Elysia + Vite + React
## Ringkasan
Fitur development: klik elemen UI di browser → langsung buka source code di editor (VS Code, Cursor, dll) pada baris dan kolom yang tepat. Zero overhead di production.
**Hotkey**: `Ctrl+Shift+Cmd+C` (macOS) / `Ctrl+Shift+Alt+C` → aktifkan mode inspect → klik elemen → file terbuka.
## Kenapa Tidak Pakai Library
`react-dev-inspector` crash di React 19 karena:
- `fiber.return.child.sibling` bisa null di React 19
- `_debugSource` dihapus dari React 19
- Walking fiber tree tidak stabil antar versi React
Solusi ini **regex-based + multi-fallback**, tidak bergantung pada React internals.
## Syarat Arsitektur
Fitur ini bekerja karena 4 syarat struktural terpenuhi. Jika salah satu tidak ada, fitur tidak bisa diimplementasi atau perlu adaptasi signifikan.
### 1. Vite sebagai Bundler (Wajib)
Seluruh mekanisme bergantung pada **Vite plugin transform pipeline**:
- `inspectorPlugin()` inject attributes ke JSX saat build/HMR
- `enforce: 'pre'` memastikan plugin jalan sebelum OXC/Babel transform JSX
- `import.meta.env?.DEV` sebagai compile-time constant untuk tree-shaking
**Tidak bisa diganti dengan**: esbuild standalone, webpack (perlu loader berbeda), SWC standalone.
**Bisa diganti dengan**: framework yang pakai Vite di dalamnya (Remix Vite, TanStack Start, Astro).
### 2. Server dan Frontend dalam Satu Proses (Wajib)
Endpoint `/__open-in-editor` harus **satu proses dengan dev server** yang melayani frontend:
- Browser POST ke origin yang sama (no CORS)
- Server punya akses ke filesystem lokal untuk `Bun.spawn(editor)`
- Endpoint harus bisa ditangani **sebelum routing & middleware** (auth, tenant, dll)
**Pola yang memenuhi syarat:**
- Elysia + Vite middlewareMode (project ini) — `onRequest` intercept sebelum route matching
- Express/Fastify + Vite middlewareMode — middleware biasa sebelum auth
- Vite dev server standalone (`vite dev`) — pakai `configureServer` hook
**Tidak memenuhi syarat:**
- Frontend dan backend di proses/port terpisah (misal: CRA + separate API server) — perlu proxy atau CORS config tambahan
- Serverless/edge deployment — tidak bisa `spawn` editor
### 3. React sebagai UI Framework (Wajib untuk Multi-Fallback)
Strategi extraction source info bergantung pada React internals:
1. `__reactProps$*` — React menyimpan props di DOM element
2. `__reactFiber$*` — React fiber tree untuk walk-up
3. DOM attribute — fallback universal
**Jika pakai framework lain** (Vue, Svelte, Solid):
- Hanya strategi 3 (DOM attribute) yang berfungsi — tetap cukup
- Hapus strategi 1 & 2 dari `getCodeInfoFromElement()`
- Inject attributes tetap via Vite plugin (framework-agnostic)
### 4. Bun sebagai Runtime (Direkomendasikan, Bukan Wajib)
Bun memberikan API yang lebih clean:
- `Bun.spawn()` — fire-and-forget tanpa import
- `Bun.which()` — cek executable ada di PATH (mencegah uncatchable error)
**Jika pakai Node.js:**
- `Bun.spawn()``child_process.spawn(editor, args, { detached: true, stdio: 'ignore' }).unref()`
- `Bun.which()``const which = require('which'); which.sync(editor, { nothrow: true })`
### Ringkasan Syarat
| Syarat | Wajib? | Alternatif |
|-------------------------------|----------|------------------------------------------------------|
| Vite sebagai bundler | Ya | Framework berbasis Vite (Remix, Astro, dll) |
| Server + frontend satu proses | Ya | Bisa diakali dengan proxy, tapi tambah kompleksitas |
| React | Sebagian | Framework lain bisa, hanya fallback ke DOM attribute |
| Bun runtime | Tidak | Node.js dengan `child_process` + `which` package |
## Arsitektur
```
BUILD TIME (Vite Plugin):
.tsx/.jsx file
→ [inspectorPlugin enforce:'pre'] inject data-inspector-* attributes ke JSX
→ [react() OXC] transform JSX ke createElement
→ Browser menerima elemen dengan attributes
RUNTIME (Browser):
Hotkey → aktifkan mode → hover elemen → baca attributes → klik
→ POST /__open-in-editor {relativePath, line, column}
BACKEND (Elysia onRequest):
/__open-in-editor → Bun.spawn([editor, '--goto', 'file:line:col'])
→ Editor terbuka di lokasi tepat
```
## Komponen yang Dibutuhkan
### 1. Vite Plugin — `inspectorPlugin()` (enforce: 'pre')
Inject `data-inspector-*` ke setiap JSX opening tag via regex.
**HARUS `enforce: 'pre'`** — kalau tidak, OXC transform JSX duluan dan regex tidak bisa menemukan `<Component`.
```typescript
// Taruh di file vite config (misal: src/vite.ts atau vite.config.ts)
import path from 'node:path'
import type { Plugin } from 'vite'
function inspectorPlugin(): Plugin {
const rootDir = process.cwd()
return {
name: 'inspector-inject',
enforce: 'pre',
transform(code, id) {
// Hanya .tsx/.jsx, 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 atau <div
// Skip TypeScript generics (Record<string>) via charBefore check
const jsxPattern = /(<(?:[A-Z][a-zA-Z0-9.]*|[a-z][a-zA-Z0-9-]*))\b/g
let match: RegExpExecArray | null = null
while ((match = jsxPattern.exec(line)) !== null) {
// Skip jika karakter sebelum `<` adalah identifier char (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')
},
}
}
```
**Mengapa regex, bukan Babel?**
- `@vitejs/plugin-react` v6+ pakai OXC (Rust), bukan Babel
- Config `babel: { plugins: [...] }` di plugin-react **DIABAIKAN**
- Regex jalan sebelum OXC via `enforce: 'pre'`
**Gotcha: TypeScript generics**
- `Record<string>` → karakter sebelum `<` adalah `d` (identifier) → SKIP
- `<Button` → karakter sebelum `<` adalah space/newline → MATCH
### 2. Vite Plugin Order (KRITIS)
```typescript
plugins: [
// 1. Route generation (jika pakai TanStack Router)
TanStackRouterVite({ ... }),
// 2. Inspector inject — HARUS sebelum react()
inspectorPlugin(),
// 3. React OXC transform
react(),
// 4. (Opsional) Dedupe React Refresh untuk middlewareMode
dedupeRefreshPlugin(),
]
```
**Jika urutan salah (inspectorPlugin setelah react):**
- OXC transform `<Button>``React.createElement(Button, ...)`
- Regex tidak menemukan `<Button` → attributes TIDAK ter-inject
- Fitur tidak berfungsi, tanpa error
### 3. DevInspector Component (Browser Runtime)
Komponen React yang handle hotkey, overlay, dan klik.
```tsx
// src/frontend/DevInspector.tsx
import { useCallback, useEffect, useRef, useState } from 'react'
interface CodeInfo {
relativePath: string
line: string
column: string
}
/** Baca data-inspector-* dari fiber props atau DOM attributes */
function getCodeInfoFromElement(element: HTMLElement): CodeInfo | null {
// Strategi 1: React internal props __reactProps$ (paling akurat)
for (const key of Object.keys(element)) {
if (key.startsWith('__reactProps$')) {
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',
}
}
}
// Strategi 2: Walk fiber tree __reactFiber$
if (key.startsWith('__reactFiber$')) {
const fiber = (element as any)[key]
let f = fiber
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
}
}
}
// Strategi 3: Fallback DOM attribute langsung
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
}
/** Walk up DOM tree sampai ketemu elemen yang punya source info */
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`
}, [])
// Activate/deactivate event listeners
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)
navigator.clipboard.writeText(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',
}}
/>
</>
)
}
```
### 4. Backend Endpoint — `/__open-in-editor`
**HARUS ditangani di `onRequest` / sebelum middleware**, bukan sebagai route biasa. Kalau jadi route, akan kena auth middleware dan gagal.
```typescript
// Di entry point server (src/index.tsx), dalam onRequest handler:
if (!isProduction && pathname === '/__open-in-editor' && request.method === 'POST') {
const { relativePath, lineNumber, columnNumber } = (await request.json()) as {
relativePath: string
lineNumber: string
columnNumber: string
}
const file = `${process.cwd()}/${relativePath}`
const editor = process.env.REACT_EDITOR || 'code'
const loc = `${file}:${lineNumber}:${columnNumber}`
const args = editor === 'subl' ? [loc] : ['--goto', loc]
const editorPath = Bun.which(editor)
console.log(`[inspector] ${editor}${editorPath ?? 'NOT FOUND'}${loc}`)
if (editorPath) {
Bun.spawn([editor, ...args], { stdio: ['ignore', 'ignore', 'ignore'] })
} else {
console.error(`[inspector] Editor "${editor}" not found in PATH. Set REACT_EDITOR in .env`)
}
return new Response('ok')
}
```
**Penting — `Bun.which()` sebelum `Bun.spawn()`:**
- `Bun.spawn()` throw native error yang TIDAK bisa di-catch jika executable tidak ada
- `Bun.which()` return null dengan aman → cek dulu sebelum spawn
**Editor yang didukung:**
| REACT_EDITOR | Editor | Args |
|------------------|--------------|--------------------------------|
| `code` (default) | VS Code | `--goto file:line:col` |
| `cursor` | Cursor | `--goto file:line:col` |
| `windsurf` | Windsurf | `--goto file:line:col` |
| `subl` | Sublime Text | `file:line:col` (tanpa --goto) |
### 5. Frontend Entry — Conditional Import (Zero Production Overhead)
```tsx
// src/frontend.tsx (atau entry point React)
import type { ReactNode } from 'react'
const InspectorWrapper = import.meta.env?.DEV
? (await import('./frontend/DevInspector')).DevInspector
: ({ children }: { children: ReactNode }) => <>{children}</>
const app = (
<InspectorWrapper>
<App />
</InspectorWrapper>
)
```
**Bagaimana zero overhead tercapai:**
- `import.meta.env?.DEV` adalah compile-time constant
- Production build: `false` → dynamic import TIDAK dieksekusi
- Tree-shaking menghapus seluruh `DevInspector.tsx` dari bundle
- Tidak ada runtime check, tidak ada dead code di bundle
### 6. (Opsional) Dedupe React Refresh — Workaround Vite middlewareMode
Jika pakai Vite dalam `middlewareMode` (seperti di Elysia/Express), `@vitejs/plugin-react` v6 bisa inject React Refresh footer dua kali → error "already declared".
```typescript
function dedupeRefreshPlugin(): Plugin {
return {
name: 'dedupe-react-refresh',
enforce: 'post',
transform(code, id) {
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
const marker = 'import * as RefreshRuntime from "/@react-refresh"'
const firstIdx = code.indexOf(marker)
if (firstIdx === -1) return null
const secondIdx = code.indexOf(marker, firstIdx + marker.length)
if (secondIdx === -1) return null
const sourcemapIdx = code.indexOf('\n//# sourceMappingURL=', secondIdx)
const endIdx = sourcemapIdx !== -1 ? sourcemapIdx : code.length
const cleaned = code.slice(0, secondIdx) + code.slice(endIdx)
return { code: cleaned, map: null }
},
}
}
```
## Langkah Implementasi di Project Baru
### Prasyarat
- Runtime: Bun
- Server: Elysia (atau framework lain dengan onRequest/beforeHandle)
- Frontend: React + Vite
- `@vitejs/plugin-react` (OXC)
### Step-by-step
1. **Buat `DevInspector.tsx`** — copy komponen dari Bagian 3 ke folder frontend
2. **Tambah `inspectorPlugin()`** — copy fungsi dari Bagian 1 ke file vite config
3. **Atur plugin order**`inspectorPlugin()` SEBELUM `react()` (Bagian 2)
4. **Tambah endpoint `/__open-in-editor`** — di `onRequest` handler (Bagian 4)
5. **Wrap root app** — conditional import di entry point (Bagian 5)
6. **Set env**`REACT_EDITOR=code` (atau cursor/windsurf/subl) di `.env`
7. **(Opsional)** Tambah `dedupeRefreshPlugin()` jika pakai Vite `middlewareMode`
### Checklist Verifikasi
- [ ] `inspectorPlugin` punya `enforce: 'pre'`
- [ ] Plugin order: inspector → react (bukan sebaliknya)
- [ ] Endpoint `/__open-in-editor` di LUAR middleware auth
- [ ] `Bun.which(editor)` dipanggil SEBELUM `Bun.spawn()`
- [ ] Conditional import pakai `import.meta.env?.DEV`
- [ ] `REACT_EDITOR` di `.env` sesuai editor yang dipakai
- [ ] Hotkey berfungsi: `Ctrl+Shift+Cmd+C` / `Ctrl+Shift+Alt+C`
## Gotcha & Pelajaran
| Masalah | Penyebab | Solusi |
|----------------------------------|---------------------------------------------|-----------------------------------------------|
| Attributes tidak ter-inject | Plugin order salah | `enforce: 'pre'`, taruh sebelum `react()` |
| `Record<string>` ikut ter-inject | Regex match TypeScript generics | Cek `charBefore` — skip jika identifier char |
| `Bun.spawn` crash | Editor tidak ada di PATH | Selalu `Bun.which()` dulu |
| Hotkey tidak response | `e.key` return 'C' (uppercase) karena Shift | Pakai `e.key.toLowerCase()` |
| React Refresh duplicate | Vite middlewareMode bug | `dedupeRefreshPlugin()` enforce: 'post' |
| Endpoint kena auth middleware | Didaftarkan sebagai route biasa | Tangani di `onRequest` sebelum routing |
| `_debugSource` undefined | React 19 menghapusnya | Multi-fallback: reactProps → fiber → DOM attr |
## Adaptasi untuk Framework Lain
### Express/Fastify (bukan Elysia)
- Endpoint `/__open-in-editor`: gunakan middleware biasa SEBELUM auth
- `Bun.spawn``child_process.spawn` jika pakai Node.js
- `Bun.which``which` npm package jika pakai Node.js
### Next.js
- Tidak perlu — Next.js punya built-in click-to-source
- Tapi jika ingin custom: taruh endpoint di `middleware.ts`, plugin di `next.config.js`
### Remix/Tanstack Start (SSR)
- Plugin tetap sama (Vite-based)
- Endpoint perlu di server entry, bukan di route loader

1
eror.md Normal file

File diff suppressed because one or more lines are too long

99
gambar.ttx Normal file
View File

@@ -0,0 +1,99 @@
type DirItem = {
type: "file" | "dir";
name: string;
path: string;
size?: number;
};
// type FileDownloadResponse = {
// url: string;
// };
// const TOKEN = "20a19f4a04032215d50ce53292e6abdd38b9f806";
// const REPO_ID = "8814bfe1-30d5-4e77-ab36-3122fa59a022";
// const DIR_TARGET = "image";
// const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
const TOKEN = process.env.SEAFILE_TOKEN!;
const REPO_ID = process.env.SEAFILE_REPO_ID!;
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
const DIR_TARGET = "asset-web";
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
const headers = {
Authorization: `Token ${TOKEN}`,
};
/**
* Ambil list file di directory
*/
async function getDirItems(): Promise<DirItem[]> {
const res = await fetch(
`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`,
{ headers }
);
if (!res.ok) {
throw new Error(`Failed get dir items: ${res.statusText}`);
}
return res.json();
}
/**
* Ambil download URL file
*/
async function getDownloadUrl(filePath: string): Promise<string> {
const res = await fetch(
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}`,
{ headers }
);
if (!res.ok) {
throw new Error(`Failed get file url: ${res.statusText}`);
}
const data = await res.json();
return data;
}
/**
* Ambil semua download URL dari target dir
*/
async function getAllDownloadUrls() {
const items = await getDirItems();
const files = items.filter((item) => item.type === "file");
const results = await Promise.all(
files.map(async (file) => {
const filePath = `${DIR_TARGET}/${file.name}`;
const url = await getDownloadUrl(filePath);
return {
name: file.name,
path: filePath,
downloadUrl: url,
};
})
);
return results;
}
// contoh eksekusi
(async () => {
try {
console.log("ambil gambar")
const urls = await getAllDownloadUrls();
await Bun.write("list_image2.json", JSON.stringify(urls))
console.log("selesai !")
} catch (err) {
console.error(err);
}
})();

View File

@@ -1,6 +1,12 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ['@elysiajs/static', 'elysia'],
experimental: {},
allowedDevOrigins: [
"http://192.168.1.82:3000", // buat akses dari HP/device lain
"http://localhost:3000", // akses lokal
],
async headers() {
return [
{
@@ -14,7 +20,6 @@ const nextConfig: NextConfig = {
},
];
},
};
export default nextConfig;

View File

@@ -1,33 +1,40 @@
{
"name": "desa-darmasaba",
"version": "0.1.2",
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prisma:seed": "bun run prisma/seed.ts"
"test:api": "vitest run",
"test:e2e": "playwright test",
"test": "bun run test:api && bun run test:e2e",
"gen:api": ""
},
"prisma": {
"seed": "bun run prisma/seed.ts"
},
"dependencies": {
"@cubejs-client/core": "^0.31.0",
"@elysiajs/cookie": "^0.8.0",
"@elysiajs/cors": "^1.2.0",
"@elysiajs/eden": "^1.2.0",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/jwt": "^1.3.2",
"@elysiajs/static": "^1.3.0",
"@elysiajs/stream": "^1.1.0",
"@elysiajs/swagger": "^1.2.0",
"@emotion/react": "^11.14.0",
"@mantine/carousel": "^7.16.2",
"@mantine/charts": "^7.17.1",
"@mantine/core": "^7.17.4",
"@mantine/dates": "^7.17.4",
"@mantine/dropzone": "^7.17.0",
"@mantine/dates": "^8.1.0",
"@mantine/dropzone": "^8.1.1",
"@mantine/form": "^8.1.0",
"@mantine/hooks": "^7.17.4",
"@mantine/modals": "^8.3.6",
"@mantine/tiptap": "^7.17.4",
"@paljs/types": "^8.1.0",
"@prisma/client": "^6.3.1",
"@prisma/client": "6.3.1",
"@tabler/icons-react": "^3.30.0",
"@tiptap/extension-highlight": "^2.11.7",
"@tiptap/extension-link": "^2.11.7",
@@ -38,50 +45,91 @@
"@tiptap/pm": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@types/adm-zip": "^0.5.7",
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"@types/mime-types": "^3.0.1",
"@types/nodemailer": "^7.0.2",
"add": "^2.0.6",
"adm-zip": "^0.5.16",
"animate.css": "^4.1.1",
"async-mutex": "^0.5.0",
"bcryptjs": "^3.0.2",
"bun": "^1.2.2",
"chart.js": "^4.4.8",
"classnames": "^2.5.1",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"elysia": "^1.2.12",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"dompurify": "^3.3.1",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel": "^8.6.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"extract-zip": "^2.0.1",
"form-data": "^4.0.2",
"framer-motion": "^12.4.1",
"framer-motion": "^12.38.0",
"get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
"jotai": "^2.12.3",
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
"list": "^2.0.19",
"lodash": "^4.17.21",
"mime-types": "^3.0.2",
"motion": "^12.4.1",
"nanoid": "^5.1.5",
"next": "15.1.6",
"next": "^15.5.2",
"next-view-transitions": "^0.3.4",
"node-fetch": "^3.3.2",
"nodemailer": "^7.0.10",
"p-limit": "^6.2.0",
"prisma": "^6.3.1",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
"prisma": "6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-exif-orientation-img": "^0.1.5",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
"recharts": "^2.15.3",
"recharts": "^3.8.0",
"sharp": "^0.34.3",
"swr": "^2.3.2",
"uuid": "^11.1.0",
"valtio": "^2.1.3",
"zlib": "^1.0.5",
"zod": "^3.24.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.9.1",
"@types/cli-progress": "^3.11.6",
"@types/dompurify": "^3.2.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitest/ui": "^4.0.18",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"eslint-config-next": "15.5.12",
"jsdom": "^28.0.0",
"msw": "^2.12.9",
"parcel": "^2.6.2",
"playwright-mcp": "^0.0.19",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5",
"parcel": "^2.6.2"
"vitest": "^4.0.18"
}
}

View File

@@ -0,0 +1,208 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e7]:
- button "Darmasaba Logo" [ref=e8] [cursor=pointer]:
- img "Darmasaba Logo" [ref=e10]
- button "PPID" [ref=e11] [cursor=pointer]:
- generic [ref=e13]: PPID
- button "Desa" [ref=e14] [cursor=pointer]:
- generic [ref=e16]: Desa
- button "Kesehatan" [ref=e17] [cursor=pointer]:
- generic [ref=e19]: Kesehatan
- button "Keamanan" [ref=e20] [cursor=pointer]:
- generic [ref=e22]: Keamanan
- button "Ekonomi" [ref=e23] [cursor=pointer]:
- generic [ref=e25]: Ekonomi
- button "Inovasi" [ref=e26] [cursor=pointer]:
- generic [ref=e28]: Inovasi
- button "Lingkungan" [ref=e29] [cursor=pointer]:
- generic [ref=e31]: Lingkungan
- button "Pendidikan" [ref=e32] [cursor=pointer]:
- generic [ref=e34]: Pendidikan
- button "Musik" [ref=e35] [cursor=pointer]:
- generic [ref=e37]: Musik
- button [ref=e38] [cursor=pointer]:
- img [ref=e40]
- generic [ref=e46]:
- generic [ref=e51]:
- generic [ref=e52]:
- generic [ref=e53]:
- img "Logo Darmasaba" [ref=e55]
- img "Logo Pudak" [ref=e57]
- generic [ref=e63]:
- generic [ref=e65]:
- generic [ref=e66]:
- img [ref=e67]
- paragraph [ref=e71]: Jam Operasional
- generic [ref=e72]:
- generic [ref=e74]: Buka
- paragraph [ref=e75]: 07:30 - 15:30
- generic [ref=e77]:
- generic [ref=e78]:
- img [ref=e79]
- paragraph [ref=e82]: Hari Ini
- generic [ref=e83]:
- paragraph [ref=e84]: Status Kantor
- paragraph [ref=e85]: Sedang Beroperasi
- paragraph [ref=e95]: Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
- generic [ref=e102]:
- generic [ref=e103]: Browser Anda tidak mendukung video.
- generic [ref=e106]:
- heading "Penghargaan Desa" [level=2] [ref=e107]
- paragraph [ref=e110]: Sedang memuat data penghargaan...
- button "Lihat semua penghargaan" [ref=e111] [cursor=pointer]:
- generic [ref=e112]:
- paragraph [ref=e114]: Lihat Semua Penghargaan
- img [ref=e116]
- generic [ref=e119]:
- generic [ref=e121]:
- heading "Layanan" [level=1] [ref=e122]
- paragraph [ref=e123]: Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!
- link "Detail" [ref=e125] [cursor=pointer]:
- /url: /darmasaba/desa/layanan
- generic [ref=e127]: Detail
- separator [ref=e129]
- generic [ref=e130]:
- generic [ref=e131]:
- paragraph [ref=e132]: Potensi Desa
- paragraph [ref=e133]: Jelajahi berbagai potensi dan peluang yang dimiliki desa. Fitur ini membantu warga maupun pemerintah desa dalam merencanakan dan mengembangkan program berbasis kekuatan lokal.
- paragraph [ref=e136]: Sedang memuat potensi desa...
- button "Lihat Semua Potensi" [ref=e139] [cursor=pointer]:
- generic [ref=e140]:
- generic [ref=e141]: Lihat Semua Potensi
- img [ref=e143]
- separator [ref=e146]
- generic [ref=e147]:
- generic [ref=e148]:
- paragraph [ref=e150]: Desa Anti Korupsi
- paragraph [ref=e151]: Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola secara terbuka dengan melibatkan warga dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan sesuai kebutuhan masyarakat.
- link "Selengkapnya" [ref=e153] [cursor=pointer]:
- /url: /darmasaba/desa-anti-korupsi/detail
- generic [ref=e155]: Selengkapnya
- paragraph [ref=e158]: Memuat Data...
- generic [ref=e166]:
- heading "SDGs Desa" [level=1] [ref=e168]
- paragraph [ref=e169]: SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
- generic [ref=e170]:
- generic [ref=e171]:
- img [ref=e172]
- paragraph [ref=e175]: Data SDGs Desa belum tersedia
- link "Jelajahi Semua Tujuan SDGs Desa" [ref=e177] [cursor=pointer]:
- /url: /darmasaba/sdgs-desa
- paragraph [ref=e180]: Jelajahi Semua Tujuan SDGs Desa
- generic [ref=e181]:
- generic [ref=e183]:
- heading "APBDes" [level=1] [ref=e184]
- paragraph [ref=e185]: Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.
- link "Lihat Semua Data" [ref=e187] [cursor=pointer]:
- /url: /darmasaba/apbdes
- generic [ref=e189]: Lihat Semua Data
- generic [ref=e191]:
- paragraph [ref=e193]: Pilih Tahun APBDes
- generic [ref=e194]:
- textbox "Pilih Tahun APBDes" [ref=e195]:
- /placeholder: Pilih tahun
- generic:
- img
- paragraph [ref=e197]: Tidak ada data APBDes untuk tahun yang dipilih.
- generic [ref=e202]:
- heading "Prestasi Desa" [level=1] [ref=e203]
- paragraph [ref=e204]: Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
- link "Lihat Semua Prestasi" [ref=e205] [cursor=pointer]:
- /url: /darmasaba/prestasi-desa
- generic [ref=e207]: Lihat Semua Prestasi
- button [ref=e211] [cursor=pointer]:
- img [ref=e214]
- button [ref=e219] [cursor=pointer]:
- img [ref=e221]
- generic [ref=e225]:
- contentinfo [ref=e228]:
- generic [ref=e230]:
- generic [ref=e231]:
- heading "Komitmen Layanan Kami" [level=2] [ref=e232]
- generic [ref=e233]:
- generic [ref=e234]:
- paragraph [ref=e235]: "1. Transparansi:"
- paragraph [ref=e236]: Pengelolaan dana desa dilakukan secara terbuka agar masyarakat dapat memahami dan memantau penggunaan anggaran.
- generic [ref=e237]:
- paragraph [ref=e238]: "2. Profesionalisme:"
- paragraph [ref=e239]: Layanan desa diberikan secara cepat, adil, dan profesional demi kepuasan masyarakat.
- generic [ref=e240]:
- paragraph [ref=e241]: "3. Partisipasi:"
- paragraph [ref=e242]: Masyarakat dilibatkan aktif dalam pengambilan keputusan demi pembangunan desa yang berhasil.
- generic [ref=e243]:
- paragraph [ref=e244]: "4. Inovasi:"
- paragraph [ref=e245]: Kami terus berinovasi, termasuk melalui teknologi, agar layanan semakin mudah diakses.
- generic [ref=e246]:
- paragraph [ref=e247]: "5. Keadilan:"
- paragraph [ref=e248]: Kebijakan dan program disusun untuk memberi manfaat yang merata bagi seluruh warga.
- generic [ref=e249]:
- paragraph [ref=e250]: "6. Pemberdayaan:"
- paragraph [ref=e251]: Masyarakat didukung melalui pelatihan, pendampingan, dan pengembangan usaha lokal.
- generic [ref=e252]:
- paragraph [ref=e253]: "7. Ramah Lingkungan:"
- paragraph [ref=e254]: Seluruh kegiatan pembangunan memperhatikan keberlanjutan demi menjaga alam dan kesehatan warga.
- separator [ref=e255]
- generic [ref=e256]:
- heading "Visi Kami" [level=2] [ref=e257]
- paragraph [ref=e258]: Dengan visi ini, kami berkomitmen menjadikan desa sebagai tempat yang aman, sejahtera, dan nyaman bagi seluruh warga.
- paragraph [ref=e259]: Kami percaya kemajuan dimulai dari kerja sama antara pemerintah desa dan masyarakat, didukung tata kelola yang baik demi kepentingan bersama. Saran maupun keluhan dapat disampaikan melalui kontak di bawah ini.
- generic [ref=e260]:
- paragraph [ref=e261]: "\"Desa Kuat, Warga Sejahtera!\""
- button "Logo Desa" [ref=e262] [cursor=pointer]:
- generic [ref=e263]:
- img "Logo Desa"
- generic [ref=e265]:
- generic [ref=e267]:
- paragraph [ref=e268]: Tentang Darmasaba
- paragraph [ref=e269]: Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
- generic [ref=e270]:
- link [ref=e271] [cursor=pointer]:
- /url: https://www.facebook.com/DarmasabaDesaku
- img [ref=e273]
- link [ref=e275] [cursor=pointer]:
- /url: https://www.instagram.com/ddarmasaba/
- img [ref=e277]
- link [ref=e280] [cursor=pointer]:
- /url: https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg
- img [ref=e282]
- link [ref=e285] [cursor=pointer]:
- /url: https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc
- img [ref=e287]
- generic [ref=e290]:
- paragraph [ref=e291]: Layanan Desa
- link "Administrasi Kependudukan" [ref=e292] [cursor=pointer]:
- /url: /darmasaba/desa/layanan/
- link "Layanan Sosial" [ref=e293] [cursor=pointer]:
- /url: /darmasaba/ekonomi/program-kemiskinan
- link "Pengaduan Masyarakat" [ref=e294] [cursor=pointer]:
- /url: /darmasaba/keamanan/laporan-publik
- link "Informasi Publik" [ref=e295] [cursor=pointer]:
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
- generic [ref=e297]:
- paragraph [ref=e298]: Tautan Penting
- link "Portal Badung" [ref=e299] [cursor=pointer]:
- /url: /darmasaba/desa/berita/semua
- link "E-Government" [ref=e300] [cursor=pointer]:
- /url: /darmasaba/inovasi/desa-digital-smart-village
- link "Transparansi" [ref=e301] [cursor=pointer]:
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
- generic [ref=e303]:
- paragraph [ref=e304]: Berlangganan Info
- paragraph [ref=e305]: Dapatkan kabar terbaru tentang program dan kegiatan desa langsung ke email Anda.
- generic [ref=e306]:
- generic [ref=e308]:
- textbox "Masukkan email Anda" [ref=e309]
- img [ref=e311]
- button "Daftar" [ref=e314] [cursor=pointer]:
- generic [ref=e316]: Daftar
- separator [ref=e317]
- paragraph [ref=e318]: © 2025 Desa Darmasaba. Hak cipta dilindungi.
- region "Notifications Alt+T"
- button "Open Next.js Dev Tools" [ref=e324] [cursor=pointer]:
- img [ref=e325]
- alert [ref=e328]
```

File diff suppressed because one or more lines are too long

25
playwright.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './__tests__/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'bun run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -1,14 +1,15 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
/* Mobile first */
'mantine-breakpoint-xs': '30em', // 480px → mobile kecilnormal
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
},
},
};
},
};

View File

@@ -0,0 +1,57 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const appMenuJson = loadJsonData("core/app-menu.json");
const appMenuChildJson = loadJsonData("core/app-menu-child.json");
export async function seedAppMenu() {
console.log("🔄 Seeding AppMenu...");
for (const item of appMenuJson) {
await prisma.appMenu.upsert({
where: { id: item.id },
update: {
name: item.name,
link: item.link,
isActive: item.isActive,
},
create: {
id: item.id,
name: item.name,
link: item.link,
isActive: item.isActive,
},
});
console.log(`✅ AppMenu seeded: ${item.name}`);
}
console.log("🎉 AppMenu seed selesai");
}
export async function seedAppMenuChild() {
console.log("🔄 Seeding AppMenuChild...");
for (const item of appMenuChildJson) {
await prisma.appMenuChild.upsert({
where: { id: item.id },
update: {
name: item.name,
link: item.link,
isActive: item.isActive,
appMenuId: item.appMenuId,
},
create: {
id: item.id,
name: item.name,
link: item.link,
isActive: item.isActive,
appMenuId: item.appMenuId,
},
});
console.log(`✅ AppMenuChild seeded: ${item.name}`);
}
console.log("🎉 AppMenuChild seed selesai");
}

View File

@@ -0,0 +1,69 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const layananJson = loadJsonData("core/layanan.json");
const potensiJson = loadJsonData("core/potensi.json");
const landingPageLayananJson = loadJsonData("core/landingpage-layanan.json");
export async function seedLayananCore() {
console.log("🔄 Seeding Layanan...");
for (const item of layananJson) {
await prisma.layanan.upsert({
where: { id: item.id },
update: {
name: item.name,
},
create: {
id: item.id,
name: item.name,
},
});
console.log(`✅ Layanan seeded: ${item.name}`);
}
console.log("🎉 Layanan seed selesai");
}
export async function seedPotensiCore() {
console.log("🔄 Seeding Potensi...");
for (const item of potensiJson) {
await prisma.potensi.upsert({
where: { id: item.id },
update: {
name: item.name,
},
create: {
id: item.id,
name: item.name,
},
});
console.log(`✅ Potensi seeded: ${item.name}`);
}
console.log("🎉 Potensi seed selesai");
}
export async function seedLandingPageLayanan() {
console.log("🔄 Seeding LandingPage_Layanan...");
for (const item of landingPageLayananJson) {
await prisma.landingPage_Layanan.upsert({
where: { id: item.id },
update: {
deksripsi: item.deksripsi,
},
create: {
id: item.id,
deksripsi: item.deksripsi,
},
});
console.log(`✅ LandingPage_Layanan seeded: ${item.id}`);
}
console.log("🎉 LandingPage_Layanan seed selesai");
}

View File

@@ -0,0 +1,96 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const kategoriBerita = loadJsonData("desa/berita/kategori-berita.json");
const beritaJson = loadJsonData("desa/berita/berita.json");
export async function seedBerita() {
// ================== SUBMENU BERITA ========================
console.log("🔄 Seeding Kategori Berita...");
for (const k of kategoriBerita) {
await prisma.kategoriBerita.upsert({
where: {
name: k.name, // ✅ cocok dengan @unique
},
update: {
name: k.name,
isActive: true,
},
create: {
id: k.id, // ✅ id tetap bisa disimpan
name: k.name,
isActive: true,
},
});
}
console.log("kategori berita success ...");
console.log("🔄 Seeding Berita...");
// Build a map of valid kategori IDs
const validKategoriIds = new Set<string>();
const kategoriList = await prisma.kategoriBerita.findMany({
select: { id: true, name: true },
});
kategoriList.forEach((k) => validKategoriIds.add(k.id));
console.log(`📋 Found ${validKategoriIds.size} valid kategori IDs in database`);
for (const b of beritaJson) {
// Validate kategoriBeritaId exists
if (!b.kategoriBeritaId || !validKategoriIds.has(b.kategoriBeritaId)) {
console.warn(
`⚠️ Skipping berita "${b.judul}": Invalid kategoriBeritaId "${b.kategoriBeritaId}"`,
);
continue;
}
let imageId: string | null = null;
if (b.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: b.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for berita "${b.judul}": ${b.imageName}`,
);
} else {
imageId = image.id;
}
}
try {
await prisma.berita.upsert({
where: { id: b.id },
update: {
judul: b.judul,
deskripsi: b.deskripsi,
content: b.content,
kategoriBeritaId: b.kategoriBeritaId,
imageId,
},
create: {
id: b.id,
judul: b.judul,
deskripsi: b.deskripsi,
content: b.content,
kategoriBeritaId: b.kategoriBeritaId,
imageId,
},
});
console.log(`✅ Berita seeded: ${b.judul}`);
} catch (error: any) {
console.error(
`❌ Failed to seed berita "${b.judul}": ${error.message}`,
);
}
}
console.log("🎉 Berita seed selesai");
}

View File

@@ -0,0 +1,42 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../../load-json";
const foto = loadJsonData("desa/gallery/foto/foto.json");
export async function seedFoto() {
console.log("🔄 Seeding Foto...");
for (const f of foto) {
let imagesId: string | null = null;
if (f.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: f.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for foto "${f.name}": ${f.imageName}`,
);
} else {
imagesId = image.id;
}
}
await prisma.galleryFoto.upsert({
where: { id: f.id },
update: {
name: f.name,
deskripsi: f.deskripsi,
imagesId,
},
create: {
id: f.id,
name: f.name,
deskripsi: f.deskripsi,
imagesId,
},
});
}
console.log("✅ Foto seeding completed");
}

View File

@@ -0,0 +1,27 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../../load-json";
const galleryVideo = loadJsonData("desa/gallery/video/video.json");
export async function seedVideo() {
console.log("🔄 Seeding Gallery Video...");
for (const v of galleryVideo) {
await prisma.galleryVideo.upsert({
where: {
id: v.id,
},
update: {
name: v.judul,
deskripsi: v.deskripsi,
linkVideo: v.linkVideo,
},
create: {
name: v.judul,
deskripsi: v.deskripsi,
linkVideo: v.linkVideo,
},
});
}
console.log("gallery video success ...");
}

View File

@@ -0,0 +1,130 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const pelayananSuratKeterangan = loadJsonData("desa/layanan/pelayananSuratKeterangan.json");
const pelayananTelunjukSaktiDesa = loadJsonData("desa/layanan/pelayananTelunjukSaktiDesa.json");
const pelayananPerizinanBerusaha = loadJsonData("desa/layanan/pelayananPerizinanBerusaha.json");
const pelayananPendudukNonPermanen = loadJsonData("desa/layanan/pelayananPendudukNonPermanen.json");
export async function seedLayanan() {
console.log("🔄 Seeding Pelayanan Surat Keterangan...");
for (const p of pelayananSuratKeterangan) {
const existing = await prisma.pelayananSuratKeterangan.findUnique({
where: { id: p.id },
select: { imageId: true, image2Id: true }, // 📌 tambahkan image2Id
});
// 1⃣ Handle imageId
let imageId = existing?.imageId ?? null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for pelayanan surat keterangan 1 "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
// 2⃣ Handle image2Id
let image2Id = existing?.image2Id ?? null;
if (p.image2Name) {
const image2 = await prisma.fileStorage.findUnique({
where: { name: p.image2Name },
select: { id: true },
});
if (!image2) {
console.warn(
`⚠️ Image not found for pelayanan surat keterangan 2 "${p.name}": ${p.image2Name}`,
);
} else {
image2Id = image2.id;
}
}
// 3⃣ Upsert dengan kedua image
await prisma.pelayananSuratKeterangan.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
imageId,
image2Id, // 📌 tambahkan ini
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
imageId,
image2Id, // 📌 tambahkan ini
},
});
}
console.log("✅ Pelayanan Surat Keterangan success...");
for (const p of pelayananTelunjukSaktiDesa) {
await prisma.pelayananTelunjukSaktiDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
link: p.link,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
link: p.link,
},
});
}
console.log("pelayanan telunjuk sakti desa success ...");
for (const l of pelayananPerizinanBerusaha) {
await prisma.pelayananPerizinanBerusaha.upsert({
where: {
id: l.id,
},
update: {
name: l.name,
deskripsi: l.deskripsi,
link: l.link,
},
create: {
id: l.id,
name: l.name,
deskripsi: l.deskripsi,
link: l.link,
},
});
}
console.log("pelayanan perizinan berusaha success ...");
for (const l of pelayananPendudukNonPermanen) {
await prisma.pelayananPendudukNonPermanen.upsert({
where: {
id: l.id,
},
update: {
name: l.name,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
name: l.name,
deskripsi: l.deskripsi,
},
});
}
console.log("pelayanan penduduk non permanen success ...");
}

View File

@@ -0,0 +1,57 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const musikJson = loadJsonData("desa/musik-desa/musik-desa.json");
export async function seedMusikDesa() {
console.log("Seeding Musik Desa...");
for (const item of musikJson) {
let audioFileId: string | null = null;
let coverImageId: string | null = null;
if (item.audioFileName) {
const audio = await prisma.fileStorage.findUnique({
where: { name: item.audioFileName },
select: { id: true },
});
if (audio) audioFileId = audio.id;
}
if (item.coverImageName) {
const cover = await prisma.fileStorage.findUnique({
where: { name: item.coverImageName },
select: { id: true },
});
if (cover) coverImageId = cover.id;
}
await prisma.musikDesa.upsert({
where: { id: item.id },
update: {
judul: item.judul,
artis: item.artis,
deskripsi: item.deskripsi,
durasi: item.durasi,
audioFileId,
coverImageId,
genre: item.genre,
tahunRilis: item.tahunRilis,
},
create: {
id: item.id,
judul: item.judul,
artis: item.artis,
deskripsi: item.deskripsi,
durasi: item.durasi,
audioFileId,
coverImageId,
genre: item.genre,
tahunRilis: item.tahunRilis,
},
});
console.log(` Musik: ${item.judul} - ${item.artis}`);
}
console.log("Musik Desa seed selesai");
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const penghargaan = loadJsonData("desa/penghargaan/penghargaan.json");
export async function seedPenghargaan() {
console.log("🔄 Seeding Penghargaan...");
for (const m of penghargaan) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for penghargaan "${m.name}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.penghargaan.upsert({
where: { id: m.id },
update: {
name: m.name,
juara: m.juara,
deskripsi: m.deskripsi,
imageId,
},
create: {
id: m.id,
name: m.name,
juara: m.juara,
deskripsi: m.deskripsi,
imageId,
},
});
}
console.log("penghargaan success ...");
}

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
import { safeSeedUnique } from "../../../safeseedUnique";
const kategoriPengumuman = loadJsonData("desa/pengumuman/kategori-pengumuman.json");
const pengumuman = loadJsonData("desa/pengumuman/pengumuman.json");
export async function seedPengumuman() {
console.log("🔄 Seeding Kategori Pengumuman...");
for (const c of kategoriPengumuman) {
await safeSeedUnique(
"categoryPengumuman",
{ name: c.name }, // ✅ where clause
{
id: c.id,
name: c.name,
},
);
}
console.log("kategori pengumuman success ...");
console.log("🔄 Seeding Pengumuman...");
for (const p of pengumuman) {
await prisma.pengumuman.upsert({
where: {
id: p.id,
},
update: {
judul: p.judul,
deskripsi: p.deskripsi,
content: p.content,
categoryPengumumanId: p.categoryPengumumanId,
},
create: {
judul: p.judul,
deskripsi: p.deskripsi,
content: p.content,
categoryPengumumanId: p.categoryPengumumanId,
},
});
}
console.log("pengumuman success ...");
}

View File

@@ -0,0 +1,66 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const kategoriPotensi = loadJsonData("desa/potensi/kategori-potensi.json");
const potensiDesa = loadJsonData("desa/potensi/potensi-desa.json");
export async function seedPotensi() {
console.log("🔄Seeding Kategori Potensi Desa ...");
for (const c of kategoriPotensi) {
await prisma.kategoriPotensi.upsert({
where: {
id: c.id,
},
update: {
nama: c.nama,
},
create: {
id: c.id,
nama: c.nama,
},
});
}
console.log("kategori Potensi success ...");
console.log("🔄 Seeding Potensi Desa...");
for (const m of potensiDesa) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for potensi desa "${m.name}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.potensiDesa.upsert({
where: { id: m.id },
update: {
name: m.name,
deskripsi: m.deskripsi,
content: m.content,
kategoriId: m.kategoriId,
imageId,
},
create: {
id: m.id,
name: m.name,
deskripsi: m.deskripsi,
content: m.content,
kategoriId: m.kategoriId,
imageId,
},
});
}
console.log("potensi desa success ...");
}

View File

@@ -0,0 +1,170 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const lambangDesa = loadJsonData("desa/profile/lambang_desa.json");
const maskotDesa = loadJsonData("desa/profile/maskot_desa.json");
const profilePerbekel = loadJsonData("desa/profile/profil_perbekel.json");
const profileDesaImage = loadJsonData("desa/profile/profileDesaImage.json");
const sejarahDesa = loadJsonData("desa/profile/sejarah_desa.json");
const visiMisiDesa = loadJsonData("desa/profile/visi_misi_desa.json");
export async function seedProfileDesa() {
// =========== SEJARAH DESA ===========
for (const l of sejarahDesa) {
await prisma.sejarahDesa.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
judul: l.judul,
deskripsi: l.deskripsi,
},
});
}
console.log("sejarah desa success ...");
// =========== VISI MISI DESA ===========
for (const l of visiMisiDesa) {
await prisma.visiMisiDesa.upsert({
where: {
id: l.id,
},
update: {
visi: l.visi,
misi: l.misi,
},
create: {
id: l.id,
visi: l.visi,
misi: l.misi,
},
});
}
console.log("visi misi desa success ...");
// =========== MASKOT DESA ===========
for (const l of maskotDesa) {
await prisma.maskotDesa.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
judul: l.judul,
deskripsi: l.deskripsi,
},
});
}
console.log("maskot desa success ...");
console.log("🔄 Seeding Profile Desa Image...");
for (const m of profileDesaImage) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for profile desa image "${m.label}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.profileDesaImage.upsert({
where: { id: m.id },
update: {
label: m.label,
maskotDesaId: m.maskotDesaId,
imageId,
},
create: {
id: m.id,
label: m.label,
maskotDesaId: m.maskotDesaId,
imageId,
},
});
}
console.log("profile desa image success ...");
// =========== LAMBANG DESA ===========
for (const l of lambangDesa) {
await prisma.lambangDesa.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
deskripsi: l.deskripsi,
},
create: {
id: l.id,
judul: l.judul,
deskripsi: l.deskripsi,
},
});
}
console.log("lambang desa success ...");
// =========== PROFILE PERBEKEL PROFILE DESA ===========
console.log("🔄 Seeding Profile Perbekel...");
for (const m of profilePerbekel) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for profile perbekel "${m.biodata}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.profilPerbekel.upsert({
where: { id: m.id },
update: {
biodata: m.biodata,
pengalaman: m.pengalaman,
pengalamanOrganisasi: m.pengalamanOrganisasi,
programUnggulan: m.programUnggulan,
imageId,
},
create: {
id: m.id,
biodata: m.biodata,
pengalaman: m.pengalaman,
pengalamanOrganisasi: m.pengalamanOrganisasi,
programUnggulan: m.programUnggulan,
imageId,
},
});
}
console.log("profile perbekel desa success ...");
}

View File

@@ -0,0 +1,44 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const perbekelDariMasaKeMasa = loadJsonData("desa/profile/profile-perbekel-lalu.json");
export async function seedProfilePerbekel() {
console.log("🔄 Seeding Perbekel Dari Masa Ke Masa...");
for (const p of perbekelDariMasaKeMasa) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for Perbekel Dari Masa Ke Masa "${p.nama}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.perbekelDariMasaKeMasa.upsert({
where: { id: p.id },
update: {
nama: p.nama,
periode: p.periode,
daerah: p.daerah,
imageId,
},
create: {
id: p.id,
nama: p.nama,
periode: p.periode,
daerah: p.daerah,
imageId,
},
});
}
console.log("✅ Pejabat Desa seeding completed");
}

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const apbdesJson = loadJsonData("ekonomi/apbdes/apbdes.json");
export async function seedAPBDes() {
console.log("Seeding APBDes...");
for (const item of apbdesJson) {
let imageId: string | null = null;
let fileId: string | null = null;
if (item.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: item.imageName },
select: { id: true },
});
if (image) imageId = image.id;
}
await prisma.aPBDes.upsert({
where: { id: item.id },
update: {
tahun: item.tahun,
name: item.name,
deskripsi: item.deskripsi,
jumlah: item.jumlah,
imageId,
fileId,
},
create: {
id: item.id,
tahun: item.tahun,
name: item.name,
deskripsi: item.deskripsi,
jumlah: item.jumlah,
imageId,
fileId,
},
});
console.log(` APBDes: ${item.name}`);
}
console.log("APBDes seed selesai");
}

View File

@@ -0,0 +1,63 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const itemsJson = loadJsonData("ekonomi/apbdes/apbdes-items.json");
const realisasiJson = loadJsonData("ekonomi/apbdes/realisasi-items.json");
export async function seedAPBDesItem() {
console.log("Seeding APBDes Items...");
// Seed items first (sorted by level to ensure parents exist)
const sortedItems = [...itemsJson].sort((a, b) => a.level - b.level);
for (const item of sortedItems) {
await prisma.aPBDesItem.upsert({
where: { id: item.id },
update: {
kode: item.kode,
uraian: item.uraian,
anggaran: item.anggaran,
tipe: item.tipe,
level: item.level,
parentId: item.parentId,
apbdesId: item.apbdesId,
},
create: {
id: item.id,
kode: item.kode,
uraian: item.uraian,
anggaran: item.anggaran,
tipe: item.tipe,
level: item.level,
parentId: item.parentId,
apbdesId: item.apbdesId,
},
});
console.log(` APBDes Item: ${item.kode} - ${item.uraian}`);
}
console.log("Seeding Realisasi Items...");
for (const item of realisasiJson) {
await prisma.realisasiItem.upsert({
where: { id: item.id },
update: {
kode: item.kode,
apbdesItemId: item.apbdesItemId,
jumlah: item.jumlah,
tanggal: new Date(item.tanggal),
keterangan: item.keterangan,
},
create: {
id: item.id,
kode: item.kode,
apbdesItemId: item.apbdesItemId,
jumlah: item.jumlah,
tanggal: new Date(item.tanggal),
keterangan: item.keterangan,
},
});
console.log(` Realisasi: ${item.kode} - Rp ${item.jumlah.toLocaleString("id-ID")}`);
}
console.log("APBDes Item & Realisasi seed selesai");
}

View File

@@ -0,0 +1,27 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const demografiPekerjaan = loadJsonData("ekonomi/demografi-pekerjaan/demografi-pekerjaan.json");
export async function seedDemografiPekerjaan() {
console.log("🔄 Seeding Demografi Pekerjaan...");
for (const k of demografiPekerjaan) {
await prisma.dataDemografiPekerjaan.upsert({
where: {
id: k.id,
},
update: {
pekerjaan: k.pekerjaan,
lakiLaki: k.lakiLaki,
perempuan: k.perempuan,
},
create: {
id: k.id,
pekerjaan: k.pekerjaan,
lakiLaki: k.lakiLaki,
perempuan: k.perempuan,
},
});
}
console.log("✅ Demografi Pekerjaan seeded successfully");
}

View File

@@ -0,0 +1,25 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const jumlahPendudukMiskin = loadJsonData("ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json");
export async function seedJumlahPendudukMiskin() {
console.log("🔄 Seeding Jumlah Penduduk Miskin...");
for (const k of jumlahPendudukMiskin) {
await prisma.grafikJumlahPendudukMiskin.upsert({
where: {
id: k.id,
},
update: {
year: k.year,
totalPoorPopulation: k.totalPoorPopulation,
},
create: {
id: k.id,
year: k.year,
totalPoorPopulation: k.totalPoorPopulation,
},
});
}
console.log("✅ Jumlah Penduduk Miskin seeded successfully");
}

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const jumlahPengangguran = loadJsonData("ekonomi/jumlah-pengangguran/detail-data-pengangguran.json");
export async function seedJumlahPengangguran() {
for (const d of jumlahPengangguran) {
await prisma.detailDataPengangguran.upsert({
where: {
month_year: { month: d.month, year: d.year },
},
update: {
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
percentageChange: d.percentageChange,
},
create: {
month: d.month,
year: d.year,
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
percentageChange: d.percentageChange,
},
});
}
console.log("📊 detailDataPengangguran success ...");
}

View File

@@ -0,0 +1,37 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const lowonganKerjaLokal = loadJsonData("ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json");
export async function seedLowonganKerjaLokal() {
console.log("🔄 Seeding Lowongan Kerja Lokal...");
for (const k of lowonganKerjaLokal) {
await prisma.lowonganPekerjaan.upsert({
where: {
id: k.id,
},
update: {
posisi: k.posisi,
namaPerusahaan: k.namaPerusahaan,
lokasi: k.lokasi,
tipePekerjaan: k.tipePekerjaan,
gaji: k.gaji,
deskripsi: k.deskripsi,
kualifikasi: k.kualifikasi,
notelp: k.notelp,
},
create: {
id: k.id,
posisi: k.posisi,
namaPerusahaan: k.namaPerusahaan,
lokasi: k.lokasi,
tipePekerjaan: k.tipePekerjaan,
gaji: k.gaji,
deskripsi: k.deskripsi,
kualifikasi: k.kualifikasi,
notelp: k.notelp,
},
});
}
console.log("✅ Lowongan Kerja Lokal seeded successfully");
}

View File

@@ -0,0 +1,93 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const kategoriProduk = loadJsonData("ekonomi/pasar-desa/kategori-produk.json");
const pasarDesa = loadJsonData("ekonomi/pasar-desa/pasar-desa.json");
const kategoriToPasar = loadJsonData("ekonomi/pasar-desa/kategori-to-pasar.json");
export async function seedPasarDesa() {
console.log("🔄 Seeding Kategori Produk...");
for (const k of kategoriProduk) {
await prisma.kategoriProduk.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("✅ Kategori Produk seeded successfully");
console.log("🔄 Seeding Pasar Desa...");
for (const p of pasarDesa) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for pasar desa "${p.nama}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.pasarDesa.upsert({
where: { id: p.id },
update: {
nama: p.nama,
deskripsi: p.deskripsi,
harga: p.harga,
rating: p.rating,
alamatUsaha: p.alamatUsaha,
kontak: p.kontak,
imageId,
kategoriProdukId: p.kategoriProdukId,
},
create: {
id: p.id,
nama: p.nama,
deskripsi: p.deskripsi,
harga: p.harga,
rating: p.rating,
alamatUsaha: p.alamatUsaha,
kontak: p.kontak,
imageId,
kategoriProdukId: p.kategoriProdukId,
},
});
console.log(`✅ Pasar desa seeded: ${p.nama}`);
}
console.log("🎉 Pasar desa seed selesai");
console.log("🔄 Seeding Kategori To Pasar...");
for (const p of kategoriToPasar) {
await prisma.kategoriToPasar.upsert({
where: {
id: p.id,
},
update: {
kategoriId: p.kategoriId,
pasarDesaId: p.pasarDesaId,
},
create: {
id: p.id,
kategoriId: p.kategoriId,
pasarDesaId: p.pasarDesaId,
},
});
}
}

View File

@@ -0,0 +1,83 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const apbdes = loadJsonData("ekonomi/pendapatan-asli-desa/apbDesa.json");
const pendapatan = loadJsonData("ekonomi/pendapatan-asli-desa/pendapatanDesa.json");
const belanja = loadJsonData("ekonomi/pendapatan-asli-desa/belanjaDesa.json");
const pembiayaan = loadJsonData("ekonomi/pendapatan-asli-desa/pembiayaanDesa.json");
export async function seedPendapatanAsli() {
console.log("🔄 Seeding Pendapatan Asli...");
for (const d of apbdes) {
await prisma.apbDesa.upsert({
where: {
id: d.id,
},
update: {
tahun: d.tahun,
},
create: {
id: d.id,
tahun: d.tahun,
},
});
}
console.log("✅ Pendapatan Asli seeded successfully");
console.log("🔄 Seeding Pendapatan...");
for (const d of pendapatan) {
await prisma.pendapatan.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
value: d.nilai
},
create: {
id: d.id,
name: d.name,
value: d.nilai
},
});
}
console.log("✅ Pendapatan seeded successfully");
console.log("🔄 Seeding Belanja...");
for (const d of belanja) {
await prisma.belanja.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
value: d.nilai
},
create: {
id: d.id,
name: d.name,
value: d.nilai
},
});
}
console.log("✅ Belanja seeded successfully");
console.log("🔄 Seeding Pembiayaan...");
for (const d of pembiayaan) {
await prisma.pembiayaan.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
value: d.nilai
},
create: {
id: d.id,
name: d.name,
value: d.nilai
},
});
}
console.log("✅ Pembiayaan seeded successfully");
}

View File

@@ -0,0 +1,52 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const grafikMenganggurBerdasarkanUsia = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json");
const grafikMenganggurBerdasarkanPendidikan = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json");
export async function seedPendudukUsiaKerjaYangMenganggur() {
for (const p of grafikMenganggurBerdasarkanUsia) {
await prisma.grafikMenganggurBerdasarkanUsia.upsert({
where: {
id: p.id,
},
update: {
usia18_25: p.usia18_25,
usia26_35: p.usia26_35,
usia36_45: p.usia36_45,
usia46_keatas: p.usia46_keatas,
},
create: {
id: p.id,
usia18_25: p.usia18_25,
usia26_35: p.usia26_35,
usia36_45: p.usia36_45,
usia46_keatas: p.usia46_keatas,
},
});
}
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
for (const p of grafikMenganggurBerdasarkanPendidikan) {
await prisma.grafikMenganggurBerdasarkanPendidikan.upsert({
where: {
id: p.id,
},
update: {
SD: p.SD,
SMP: p.SMP,
SMA: p.SMA,
D3: p.D3,
S1: p.S1,
},
create: {
id: p.id,
SD: p.SD,
SMP: p.SMP,
SMA: p.SMA,
D3: p.D3,
S1: p.S1,
},
});
}
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
}

View File

@@ -0,0 +1,52 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const programKemiskinan = loadJsonData("ekonomi/program-kemiskinan/program-kemiskinan.json");
const statistikKemiskinan = loadJsonData("ekonomi/program-kemiskinan/statistik-kemiskinan.json");
export async function seedProgramKemiskinan() {
for (const s of statistikKemiskinan) {
await prisma.statistikKemiskinan.upsert({
where: { tahun: s.tahun }, // ✅ FIX
update: {
jumlah: s.jumlah,
},
create: {
id: s.id, // id boleh tetap
tahun: s.tahun,
jumlah: s.jumlah,
},
});
}
console.log("📊 Statistik Kemiskinan seeded successfully");
console.log("🔄 Seeding Program Kemiskinan...");
for (const k of programKemiskinan) {
await prisma.programKemiskinan.upsert({
where: { id: k.id },
update: {
nama: k.nama,
deskripsi: k.deskripsi,
icon: k.icon,
statistik: {
connect: {
tahun: k.tahun, // 👈 BUKAN ID
},
},
},
create: {
id: k.id,
nama: k.nama,
deskripsi: k.deskripsi,
icon: k.icon,
statistik: {
connect: {
tahun: k.tahun,
},
},
},
});
}
console.log("✅ Program Kemiskinan seeded successfully");
}

View File

@@ -0,0 +1,27 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const sektorUnggulanDesa = loadJsonData("ekonomi/sektor-unggulan/sektor-unggulan.json");
export async function seedSektorUnggulanDesa() {
console.log("🔄 Seeding Sektor Unggulan Desa...");
for (const k of sektorUnggulanDesa) {
await prisma.sektorUnggulanDesa.upsert({
where: {
id: k.id,
},
update: {
name: k.name,
description: k.description,
value: k.value,
},
create: {
id: k.id,
name: k.name,
description: k.description,
value: k.value,
},
});
}
console.log("✅ Sektor Unggulan Desa seeded successfully");
}

View File

@@ -0,0 +1,60 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const posisiOrganisasiBumDes = loadJsonData("ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json");
const pegawai = loadJsonData("ekonomi/struktur-organisasi/pegawai-bumdes.json");
export async function seedStrukturBumdes() {
const flattenedPosisi = posisiOrganisasiBumDes.flat();
// ✅ Urutkan berdasarkan hierarki
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
for (const p of sortedPosisi) {
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
if (p.parentId) {
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
if (!parentExists) {
console.warn(
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
);
continue;
}
}
await prisma.posisiOrganisasiBumDes.upsert({
where: { id: p.id },
update: p,
create: p,
});
}
console.log("posisi organisasi berhasil");
for (const p of pegawai) {
await prisma.pegawaiBumDes.upsert({
where: {
id: p.id,
},
update: {
namaLengkap: p.namaLengkap,
gelarAkademik: p.gelarAkademik,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
create: {
id: p.id,
namaLengkap: p.namaLengkap,
gelarAkademik: p.gelarAkademik,
tanggalMasuk: new Date(p.tanggalMasuk),
email: p.email,
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
});
}
console.log("pegawai success ...");
}

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const ajukanIde = loadJsonData("inovasi/ajukan-ide/ajukan-ide.json");
export async function seedAjukan() {
console.log("🔄 Seeding Ajukan Ide Inovatif...");
for (const d of ajukanIde) {
await prisma.ajukanIdeInovatif.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
alamat: d.alamat,
namaIde: d.namaIde,
deskripsi: d.deskripsi,
masalah: d.masalah,
benefit: d.benefit,
},
create: {
id: d.id,
name: d.name,
alamat: d.alamat,
namaIde: d.namaIde,
deskripsi: d.deskripsi,
masalah: d.masalah,
benefit: d.benefit,
},
});
}
console.log("✅ Ajukan Ide Inovatif seeded successfully");
}

View File

@@ -0,0 +1,44 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const desaDigital = loadJsonData("inovasi/desa-digital/desa-digital.json");
export async function seedDesaDigital() {
console.log("🔄 Seeding Desa Digital...");
for (const d of desaDigital) {
let imageId: string | null = null;
if (d.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: d.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for desa digital "${d.name}": ${d.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.desaDigital.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
deskripsi: d.deskripsi,
imageId: imageId,
},
create: {
id: d.id,
name: d.name,
deskripsi: d.deskripsi,
imageId: imageId,
},
});
}
console.log("✅ Desa Digital seeded successfully");
}

View File

@@ -0,0 +1,44 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const infoTeknologi = loadJsonData("inovasi/info-teknologi/info-teknologi.json");
export async function seedInfoTeknologi() {
console.log("🔄 Seeding Info Teknologi...");
for (const p of infoTeknologi) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for berita "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.infoTekno.upsert({
where: {
id: p.id,
},
update: {
name: p.name,
deskripsi: p.deskripsi,
imageId: imageId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
imageId: imageId,
},
});
}
console.log("✅ Info Teknologi seeded successfully");
}

View File

@@ -0,0 +1,68 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const kolaborasiInovasi = loadJsonData("inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json");
const mitraKolaborasi = loadJsonData("inovasi/kolaborasi-inovasi/mitra-kolaborasi.json");
export async function seedKolaborasiInovasi() {
console.log("🔄 Seeding Kolaborasi Inovasi...");
for (const p of kolaborasiInovasi) {
await prisma.kolaborasiInovasi.upsert({
where: {
id: p.id,
},
update: {
name: p.name,
tahun: p.tahun,
slug: p.slug,
deskripsi: p.deskripsi,
kolaborator: p.kolaborator,
},
create: {
id: p.id,
name: p.name,
tahun: p.tahun,
slug: p.slug,
deskripsi: p.deskripsi,
kolaborator: p.kolaborator,
},
});
}
console.log("✅ Kolaborasi Inovasi seeded successfully");
console.log("🔄 Seeding Mitra Kolaborasi...");
for (const p of mitraKolaborasi) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for mitra kolaborasi "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.mitraKolaborasi.upsert({
where: {
id: p.id,
},
update: {
name: p.name,
imageId: imageId,
},
create: {
id: p.id,
name: p.name,
imageId: imageId,
},
});
}
console.log("✅ Mitra Kolaborasi seeded successfully");
}

View File

@@ -0,0 +1,115 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const jenisLayanan = loadJsonData("inovasi/layanan-online-desa/jenis-layanan.json");
const administrasiOnline = loadJsonData("inovasi/layanan-online-desa/administrasi-online.json");
const jenisPengaduan = loadJsonData("inovasi/layanan-online-desa/jenis-pengaduan.json");
const pengaduanMasyarakat = loadJsonData("inovasi/layanan-online-desa/pengaduan-masyarakat.json");
export async function seedLayananOnlineDesa() {
console.log("🔄 Seeding Jenis Layanan...");
for (const j of jenisLayanan) {
await prisma.jenisLayanan.upsert({
where: {
id: j.id,
},
update: {
nama: j.nama,
deskripsi: j.deskripsi,
},
create: {
id: j.id,
nama: j.nama,
deskripsi: j.deskripsi,
},
});
}
console.log("✅ Jenis Layanan seeded successfully");
console.log("🔄 Seeding Administrasi Online...");
for (const d of administrasiOnline) {
await prisma.administrasiOnline.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
alamat: d.alamat,
nomorTelepon: d.nomorTelepon,
jenisLayananId: d.jenisLayananId,
},
create: {
id: d.id,
name: d.name,
alamat: d.alamat,
nomorTelepon: d.nomorTelepon,
jenisLayananId: d.jenisLayananId,
},
});
}
console.log("✅ Administrasi Online seeded successfully");
console.log("🔄 Seeding Jenis Pengaduan Masyarakat...");
for (const d of jenisPengaduan) {
await prisma.jenisPengaduan.upsert({
where: {
id: d.id,
},
update: {
nama: d.nama,
},
create: {
id: d.id,
nama: d.nama,
},
});
}
console.log("✅ Jenis Pengaduan Masyarakat seeded successfully");
console.log("🔄 Seeding Pengaduan Masyarakat...");
for (const d of pengaduanMasyarakat) {
let imageId: string | null = null;
if (d.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: d.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for pengaduan masyarakat "${d.name}": ${d.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.pengaduanMasyarakat.upsert({
where: {
id: d.id,
},
update: {
name: d.name,
email: d.email,
nik: d.nik,
nomorTelepon: d.nomorTelepon,
judulPengaduan: d.judulPengaduan,
lokasiKejadian: d.lokasiKejadian,
imageId: imageId,
deskripsiPengaduan: d.deskripsiPengaduan,
jenisPengaduanId: d.jenisPengaduanId,
},
create: {
id: d.id,
name: d.name,
email: d.email,
nik: d.nik,
nomorTelepon: d.nomorTelepon,
judulPengaduan: d.judulPengaduan,
lokasiKejadian: d.lokasiKejadian,
imageId: imageId,
deskripsiPengaduan: d.deskripsiPengaduan,
jenisPengaduanId: d.jenisPengaduanId,
},
});
}
console.log("✅ Pengaduan Masyarakat seeded successfully");
}

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const programKreatif = loadJsonData("inovasi/program-kreatif-desa/program-kreatif-desa.json");
export async function seedProgramKreatifDesa() {
console.log("🔄 Seeding Program Kreatif...");
for (const p of programKreatif) {
await prisma.programKreatif.upsert({
where: {
id: p.id,
},
update: {
name: p.name,
deskripsi: p.deskripsi,
icon: p.icon,
slug: p.slug,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
icon: p.icon,
slug: p.slug,
},
});
}
console.log("✅ Program Kreatif seeded successfully");
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const keamananLingkunganJson = loadJsonData("keamanan/keamanan-lingkungan/keamanan-lingkungan.json");
export async function seedKeamananLingkungan() {
console.log("🔄 Seeding Keamanan Lingkungan...");
for (const p of keamananLingkunganJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for keamanan lingkungan "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.keamananLingkungan.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
imageId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
imageId,
},
});
console.log(`✅ Keamanan lingkungan seeded: ${p.name}`);
}
console.log("🎉 Keamanan lingkungan seed selesai");
}

View File

@@ -0,0 +1,89 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const kontakDaruratKeamanan = loadJsonData("keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json");
const kontakItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakItem.json");
const kontakDaruratToItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json");
export async function seedKontakDaruratKeamanan() {
console.log("🔄 Seeding Kontak Item...");
for (const e of kontakItem) {
await prisma.kontakItem.upsert({
where: {
id: e.id,
},
update: {
nama: e.nama,
icon: e.icon,
nomorTelepon: e.nomorTelepon,
},
create: {
id: e.id, // ✅ WAJIB
nama: e.nama,
icon: e.icon,
nomorTelepon: e.nomorTelepon,
},
});
}
console.log("✅ Kontak Item seeded successfully");
console.log("🔄 Seeding Kontak Darurat Keamanan...");
for (const d of kontakDaruratKeamanan) {
await prisma.kontakDaruratKeamanan.upsert({
where: {
id: d.id,
},
update: {
nama: d.nama,
icon: d.icon,
kategoriId: d.kategoriId,
},
create: {
id: d.id,
nama: d.nama,
icon: d.icon,
kategoriId: d.kategoriId,
},
});
}
console.log("✅ Kontak Darurat Keamanan seeded successfully");
console.log("🔄 Seeding Kontak Darurat To Item...");
for (const f of kontakDaruratToItem) {
// ✅ Validasi foreign keys
const kontakDaruratExists = await prisma.kontakDaruratKeamanan.findUnique({
where: { id: f.kontakDaruratId },
});
const kontakItemExists = await prisma.kontakItem.findUnique({
where: { id: f.kontakItemId },
});
if (!kontakDaruratExists) {
console.warn(
`⚠️ KontakDarurat ${f.kontakDaruratId} not found, skipping...`,
);
continue;
}
if (!kontakItemExists) {
console.warn(`⚠️ KontakItem ${f.kontakItemId} not found, skipping...`);
continue;
}
await prisma.kontakDaruratToItem.upsert({
where: { id: f.id },
update: {
kontakDaruratId: f.kontakDaruratId,
kontakItemId: f.kontakItemId,
},
create: {
id: f.id,
kontakDaruratId: f.kontakDaruratId,
kontakItemId: f.kontakItemId,
},
});
}
console.log("✅ Kontak Darurat To Item seeded successfully");
}

View File

@@ -0,0 +1,51 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const laporanPublik = loadJsonData("keamanan/laporan-publik/laporan-publik.json");
const penangananLaporan = loadJsonData("keamanan/laporan-publik/penanganan-laporan.json");
export async function seedLaporanPublik() {
console.log("🔄 Seeding Laporan Publik...");
for (const l of laporanPublik) {
await prisma.laporanPublik.upsert({
where: {
id: l.id,
},
update: {
judul: l.judul,
lokasi: l.lokasi,
tanggalWaktu: l.tanggalWaktu,
kronologi: l.kronologi,
},
create: {
id: l.id,
judul: l.judul,
lokasi: l.lokasi,
tanggalWaktu: l.tanggalWaktu,
kronologi: l.kronologi,
},
});
}
console.log("laporan publik success ...");
console.log("🔄 Seeding Penanganan Laporan...");
for (const l of penangananLaporan) {
await prisma.penangananLaporanPublik.upsert({
where: {
id: l.id,
},
update: {
deskripsi: l.deskripsi,
laporanId: l.laporanId,
},
create: {
id: l.id,
deskripsi: l.deskripsi,
laporanId: l.laporanId,
},
});
}
console.log("penanganan laporan success ...");
}

View File

@@ -0,0 +1,30 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const pencegahanKriminalitas = loadJsonData("keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json");
export async function seedPencegahanKriminalitas() {
console.log("🔄 Seeding Pencegahan Kriminalitas...");
for (const d of pencegahanKriminalitas) {
await prisma.pencegahanKriminalitas.upsert({
where: {
id: d.id,
},
update: {
judul: d.judul,
deskripsi: d.deskripsi,
deskripsiSingkat: d.deskripsiSingkat,
linkVideo: d.linkVideo,
},
create: {
id: d.id,
judul: d.judul,
deskripsi: d.deskripsi,
deskripsiSingkat: d.deskripsiSingkat,
linkVideo: d.linkVideo,
},
});
}
console.log("✅ Pencegahan Kriminalitas seeded successfully");
}

View File

@@ -0,0 +1,82 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const layananPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-polsek.json");
const polsekTerdekat = loadJsonData("keamanan/polsek-terdekat/polsek-terdekat.json");
const layananToPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-to-polsek.json");
export async function seedPolsekTerdekat() {
console.log("🔄 Seeding Layanan Polsek...");
for (const k of layananPolsek) {
await prisma.layananPolsek.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("layanan polsek success ...");
console.log("🔄 Seeding Polsek Terdekat...");
for (const k of polsekTerdekat) {
await prisma.polsekTerdekat.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
jarakKeDesa: k.jarakKeDesa,
alamat: k.alamat,
nomorTelepon: k.nomorTelepon,
jamOperasional: k.jamOperasional,
embedMapUrl: k.embedMapUrl,
namaTempatMaps: k.namaTempatMaps,
alamatMaps: k.alamatMaps,
linkPetunjukArah: k.linkPetunjukArah,
layananPolsekId: k.layananPolsekId,
},
create: {
id: k.id,
nama: k.nama,
jarakKeDesa: k.jarakKeDesa,
alamat: k.alamat,
nomorTelepon: k.nomorTelepon,
jamOperasional: k.jamOperasional,
embedMapUrl: k.embedMapUrl,
namaTempatMaps: k.namaTempatMaps,
alamatMaps: k.alamatMaps,
linkPetunjukArah: k.linkPetunjukArah,
layananPolsekId: k.layananPolsekId,
},
});
}
console.log("polsek terdekat success ...");
console.log("🔄 Seeding Layanan To Polsek...");
for (const k of layananToPolsek) {
await prisma.layananToPolsek.upsert({
where: {
id: k.id,
},
update: {
layananId: k.layananId,
polsekTerdekatId: k.polsekTerdekatId,
},
create: {
id: k.id,
layananId: k.layananId,
polsekTerdekatId: k.polsekTerdekatId,
},
});
}
console.log("layanan to polsek success ...");
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const tipsKeamananJson = loadJsonData("keamanan/tips-keamanan/tips-keamanan.json");
export async function seedTipsKeamanan() {
console.log("🔄 Seeding Tips Keamanan...");
for (const p of tipsKeamananJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for tips keamanan "${p.judul}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.menuTipsKeamanan.upsert({
where: { id: p.id },
update: {
judul: p.judul,
deskripsi: p.deskripsi,
imageId,
},
create: {
id: p.id,
judul: p.judul,
deskripsi: p.deskripsi,
imageId,
},
});
console.log(`✅ Tips Keamanan seeded: ${p.judul}`);
}
console.log("🎉 Tips Keamanan seed selesai");
}

View File

@@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const dataBanjarJson = loadJsonData("kependudukan/data-banjar/data-banjar.json");
export async function seedDataBanjar() {
console.log("Seeding Data Banjar...");
for (const item of dataBanjarJson) {
await prisma.dataBanjar.upsert({
where: { id: item.id },
update: {
nama: item.nama,
penduduk: item.penduduk,
kk: item.kk,
miskin: item.miskin,
tahun: item.tahun,
},
create: {
id: item.id,
nama: item.nama,
penduduk: item.penduduk,
kk: item.kk,
miskin: item.miskin,
tahun: item.tahun,
},
});
console.log(` Banjar: ${item.nama} (${item.penduduk} penduduk)`);
}
console.log("Data Banjar seed selesai");
}

View File

@@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const dinamikaPendudukJson = loadJsonData("kependudukan/dinamika-penduduk/dinamika-penduduk.json");
export async function seedDinamikaPenduduk() {
console.log("Seeding Dinamika Penduduk...");
for (const item of dinamikaPendudukJson) {
await prisma.dinamikaPenduduk.upsert({
where: { id: item.id },
update: {
tahun: item.tahun,
kelahiran: item.kelahiran,
kematian: item.kematian,
masuk: item.masuk,
keluar: item.keluar,
},
create: {
id: item.id,
tahun: item.tahun,
kelahiran: item.kelahiran,
kematian: item.kematian,
masuk: item.masuk,
keluar: item.keluar,
},
});
console.log(` Tahun ${item.tahun}: ${item.kelahiran} kelahiran, ${item.kematian} kematian`);
}
console.log("Dinamika Penduduk seed selesai");
}

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const distribusiAgamaJson = loadJsonData("kependudukan/distribusi-agama/distribusi-agama.json");
export async function seedDistribusiAgama() {
console.log("Seeding Distribusi Agama...");
for (const item of distribusiAgamaJson) {
await prisma.distribusiAgama.upsert({
where: { id: item.id },
update: {
agama: item.agama,
jumlah: item.jumlah,
tahun: item.tahun,
},
create: {
id: item.id,
agama: item.agama,
jumlah: item.jumlah,
tahun: item.tahun,
},
});
console.log(` ${item.agama}: ${item.jumlah} penganut`);
}
console.log("Distribusi Agama seed selesai");
}

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const distribusiUmurJson = loadJsonData("kependudukan/distribusi-umur/distribusi-umur.json");
export async function seedDistribusiUmur() {
console.log("Seeding Distribusi Umur...");
for (const item of distribusiUmurJson) {
await prisma.distribusiUmur.upsert({
where: { id: item.id },
update: {
rentangUmur: item.rentangUmur,
jumlah: item.jumlah,
tahun: item.tahun,
},
create: {
id: item.id,
rentangUmur: item.rentangUmur,
jumlah: item.jumlah,
tahun: item.tahun,
},
});
console.log(` Rentang ${item.rentangUmur}: ${item.jumlah} jiwa`);
}
console.log("Distribusi Umur seed selesai");
}

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const migrasiPendudukJson = loadJsonData("kependudukan/migrasi-penduduk/migrasi-penduduk.json");
export async function seedMigrasiPenduduk() {
console.log("Seeding MigrASI PENDUDUK...");
for (const item of migrasiPendudukJson) {
await prisma.migrasiPenduduk.upsert({
where: { id: item.id },
update: {
nama: item.nama,
jenis: item.jenis,
tanggal: new Date(item.tanggal),
asal: item.asal,
tujuan: item.tujuan,
alasan: item.alasan,
},
create: {
id: item.id,
nama: item.nama,
jenis: item.jenis,
tanggal: new Date(item.tanggal),
asal: item.asal,
tujuan: item.tujuan,
alasan: item.alasan,
},
});
console.log(` ${item.nama}: ${item.jenis} (${item.alasan})`);
}
console.log("Migrasi Penduduk seed selesai");
}

View File

@@ -0,0 +1,106 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const artikelJson = loadJsonData("kesehatan/artikel-kesehatan/artikel-kesehatan.json");
const introJson = loadJsonData("kesehatan/artikel-kesehatan/introduction.json");
const symptomJson = loadJsonData("kesehatan/artikel-kesehatan/symptom.json");
const preventionJson = loadJsonData("kesehatan/artikel-kesehatan/prevention.json");
const firstAidJson = loadJsonData("kesehatan/artikel-kesehatan/first-aid.json");
const mythVsFactJson = loadJsonData("kesehatan/artikel-kesehatan/myth-vs-fact.json");
const doctorSignJson = loadJsonData("kesehatan/artikel-kesehatan/doctor-sign.json");
export async function seedArtikelKesehatan() {
console.log("Seeding Introduction...");
for (const item of introJson) {
await prisma.introduction.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Symptom...");
for (const item of symptomJson) {
await prisma.symptom.upsert({
where: { id: item.id },
update: { title: item.title, content: item.content },
create: { id: item.id, title: item.title, content: item.content },
});
}
console.log("Seeding Prevention...");
for (const item of preventionJson) {
await prisma.prevention.upsert({
where: { id: item.id },
update: { title: item.title, content: item.content },
create: { id: item.id, title: item.title, content: item.content },
});
}
console.log("Seeding First Aid...");
for (const item of firstAidJson) {
await prisma.firstAid.upsert({
where: { id: item.id },
update: { title: item.title, content: item.content },
create: { id: item.id, title: item.title, content: item.content },
});
}
console.log("Seeding Myth vs Fact...");
for (const item of mythVsFactJson) {
await prisma.mythVsFact.upsert({
where: { id: item.id },
update: {
title: item.title,
mitos: item.mitos,
fakta: item.fakta,
},
create: {
id: item.id,
title: item.title,
mitos: item.mitos,
fakta: item.fakta,
},
});
}
console.log("Seeding Doctor Sign...");
for (const item of doctorSignJson) {
await prisma.doctorSign.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Artikel Kesehatan...");
for (const item of artikelJson) {
await prisma.artikelKesehatan.upsert({
where: { id: item.id },
update: {
title: item.title,
content: item.content,
introductionId: item.introductionId,
symptomId: item.symptomId,
preventionId: item.preventionId,
firstAidId: item.firstAidId,
mythVsFactId: item.mythVsFactId,
doctorSignId: item.doctorSignId,
},
create: {
id: item.id,
title: item.title,
content: item.content,
introductionId: item.introductionId,
symptomId: item.symptomId,
preventionId: item.preventionId,
firstAidId: item.firstAidId,
mythVsFactId: item.mythVsFactId,
doctorSignId: item.doctorSignId,
},
});
console.log(` Artikel: ${item.title}`);
}
console.log("Artikel Kesehatan seed selesai");
}

View File

@@ -0,0 +1,122 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const faskesJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-kesehatan.json");
const infoUmumJson = loadJsonData("kesehatan/fasilitas-kesehatan/informasi-umum.json");
const layananUnggulanJson = loadJsonData("kesehatan/fasilitas-kesehatan/layanan-unggulan.json");
const dokterJson = loadJsonData("kesehatan/fasilitas-kesehatan/dokter-tenaga-medis.json");
const fasilitasPendukungJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-pendukung.json");
const prosedurJson = loadJsonData("kesehatan/fasilitas-kesehatan/prosedur-pendaftaran.json");
const tarifJson = loadJsonData("kesehatan/fasilitas-kesehatan/tarif-layanan.json");
export async function seedFasilitasKesehatan() {
console.log("Seeding Informasi Umum...");
for (const item of infoUmumJson) {
await prisma.informasiUmum.upsert({
where: { id: item.id },
update: {
fasilitas: item.fasilitas,
alamat: item.alamat,
jamOperasional: item.jamOperasional,
},
create: {
id: item.id,
fasilitas: item.fasilitas,
alamat: item.alamat,
jamOperasional: item.jamOperasional,
},
});
console.log(` Informasi Umum: ${item.fasilitas}`);
}
console.log("Seeding Layanan Unggulan...");
for (const item of layananUnggulanJson) {
await prisma.layananUnggulan.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Fasilitas Pendukung...");
for (const item of fasilitasPendukungJson) {
await prisma.fasilitasPendukung.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Prosedur Pendaftaran...");
for (const item of prosedurJson) {
await prisma.prosedurPendaftaran.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Tarif dan Layanan...");
for (const item of tarifJson) {
await prisma.tarifDanLayanan.upsert({
where: { id: item.id },
update: { layanan: item.layanan, tarif: item.tarif },
create: { id: item.id, layanan: item.layanan, tarif: item.tarif },
});
console.log(` Tarif: ${item.layanan}`);
}
console.log("Seeding Dokter dan Tenaga Medis...");
for (const item of dokterJson) {
await prisma.dokterdanTenagaMedis.upsert({
where: { id: item.id },
update: {
name: item.name,
specialist: item.specialist,
jadwal: item.jadwal,
jadwalLibur: item.jadwalLibur,
jamBukaOperasional: item.jamBukaOperasional,
jamTutupOperasional: item.jamTutupOperasional,
jamBukaLibur: item.jamBukaLibur,
jamTutupLibur: item.jamTutupLibur,
},
create: {
id: item.id,
name: item.name,
specialist: item.specialist,
jadwal: item.jadwal,
jadwalLibur: item.jadwalLibur,
jamBukaOperasional: item.jamBukaOperasional,
jamTutupOperasional: item.jamTutupOperasional,
jamBukaLibur: item.jamBukaLibur,
jamTutupLibur: item.jamTutupLibur,
},
});
console.log(` Dokter: ${item.name}`);
}
console.log("Seeding Fasilitas Kesehatan...");
for (const item of faskesJson) {
await prisma.fasilitasKesehatan.upsert({
where: { id: item.id },
update: {
name: item.name,
informasiUmumId: item.informasiUmumId,
layananUnggulanId: item.layananUnggulanId,
fasilitasPendukungId: item.fasilitasPendukungId,
prosedurPendaftaranId: item.prosedurPendaftaranId,
},
create: {
id: item.id,
name: item.name,
informasiUmumId: item.informasiUmumId,
layananUnggulanId: item.layananUnggulanId,
fasilitasPendukungId: item.fasilitasPendukungId,
prosedurPendaftaranId: item.prosedurPendaftaranId,
},
});
console.log(` Fasilitas Kesehatan: ${item.name}`);
}
console.log("Fasilitas Kesehatan seed selesai");
}

View File

@@ -0,0 +1,48 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const infoWabahPenyakitJson = loadJsonData("kesehatan/infowabahpenyakit/infowabahpenyakit.json");
export async function seedInfoWabahPenyakit() {
console.log("🔄 Seeding Info Wabah Penyakit...");
for (const p of infoWabahPenyakitJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for info wabah penyakit "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.infoWabahPenyakit.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsiSingkat: p.deskripsiSingkat,
deskripsiLengkap: p.deskripsiLengkap,
imageId,
},
create: {
id: p.id,
name: p.name,
deskripsiSingkat: p.deskripsiSingkat,
deskripsiLengkap: p.deskripsiLengkap,
imageId,
},
});
console.log(`✅ Info wabah penyakit seeded: ${p.name}`);
}
console.log("🎉 Info wabah penyakit seed selesai");
}

View File

@@ -0,0 +1,123 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const jadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/jadwal-kegiatan.json");
const infoJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/informasi-jadwal.json");
const deskJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/deskripsi-jadwal.json");
const layananJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/layanan-jadwal.json");
const syaratJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/syarat-ketentuan.json");
const dokumenJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/dokumen-jadwal.json");
const daftarJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/pendaftaran-jadwal.json");
export async function seedJadwalKegiatan() {
console.log("Seeding Informasi Jadwal Kegiatan...");
for (const item of infoJadwalJson) {
await prisma.informasiJadwalKegiatan.upsert({
where: { id: item.id },
update: {
name: item.name,
tanggal: item.tanggal,
waktu: item.waktu,
lokasi: item.lokasi,
},
create: {
id: item.id,
name: item.name,
tanggal: item.tanggal,
waktu: item.waktu,
lokasi: item.lokasi,
},
});
console.log(` Informasi: ${item.name}`);
}
console.log("Seeding Deskripsi Jadwal Kegiatan...");
for (const item of deskJadwalJson) {
await prisma.deskripsiJadwalKegiatan.upsert({
where: { id: item.id },
update: { deskripsi: item.deskripsi },
create: { id: item.id, deskripsi: item.deskripsi },
});
}
console.log("Seeding Layanan Jadwal Kegiatan...");
for (const item of layananJadwalJson) {
await prisma.layananJadwalKegiatan.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Syarat & Ketentuan Jadwal...");
for (const item of syaratJadwalJson) {
await prisma.syaratKetentuanJadwalKegiatan.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Dokumen Jadwal Kegiatan...");
for (const item of dokumenJadwalJson) {
await prisma.dokumenJadwalKegiatan.upsert({
where: { id: item.id },
update: { content: item.content },
create: { id: item.id, content: item.content },
});
}
console.log("Seeding Pendaftaran Jadwal Kegiatan...");
for (const item of daftarJadwalJson) {
await prisma.pendaftaranJadwalKegiatan.upsert({
where: { id: item.id },
update: {
name: item.name,
tanggal: item.tanggal,
namaOrangtua: item.namaOrtu,
nomor: item.nomor,
alamat: item.alamat,
catatan: item.catatan,
},
create: {
id: item.id,
name: item.name,
tanggal: item.tanggal,
namaOrangtua: item.namaOrtu,
nomor: item.nomor,
alamat: item.alamat,
catatan: item.catatan,
},
});
console.log(` Pendaftaran: ${item.name}`);
}
console.log("Seeding Jadwal Kegiatan...");
for (const item of jadwalJson) {
await prisma.jadwalKegiatan.upsert({
where: { id: item.id },
update: {
content: item.content,
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
},
create: {
id: item.id,
content: item.content,
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
},
});
console.log(` Jadwal Kegiatan seeded`);
}
console.log("Jadwal Kegiatan seed selesai");
}

View File

@@ -0,0 +1,48 @@
import { loadJsonData } from "../../../load-json";
import prisma from "@/lib/prisma";
const kontakDaruratJson = loadJsonData("kesehatan/kontak-darurat/kontak-darurat.json");
export async function seedKontakDarurat() {
console.log("🔄 Seeding Kontak Darurat...");
for (const p of kontakDaruratJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for kontak darurat "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.kontakDarurat.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
imageId,
whatsapp: p.whatsapp,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
imageId,
whatsapp: p.whatsapp,
},
});
console.log(`✅ Kontak darurat seeded: ${p.name}`);
}
console.log("🎉 Kontak darurat seed selesai");
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const penangananDaruratJson = loadJsonData("kesehatan/penanganan-darurat/penganan-darurat.json");
export async function seedPenangananDarurat() {
console.log("🔄 Seeding Penanganan Darurat...");
for (const p of penangananDaruratJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for penanganan darurat "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.penangananDarurat.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
imageId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
imageId,
},
});
console.log(`✅ Penanganan darurat seeded: ${p.name}`);
}
console.log("🎉 Penanganan darurat seed selesai");
}

View File

@@ -0,0 +1,50 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const posyanduJson = loadJsonData("kesehatan/posyandu/posyandu.json");
export async function seedPosyandu() {
console.log("🔄 Seeding Posyandu...");
for (const p of posyanduJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for posyandu "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.posyandu.upsert({
where: { id: p.id },
update: {
name: p.name,
nomor: p.nomor,
deskripsi: p.deskripsi,
jadwalPelayanan: p.jadwalPelayanan,
imageId,
},
create: {
id: p.id,
name: p.name,
nomor: p.nomor,
deskripsi: p.deskripsi,
jadwalPelayanan: p.jadwalPelayanan,
imageId,
},
});
console.log(`✅ Posyandu seeded: ${p.name}`);
}
console.log("🎉 Posyandu seed selesai");
}

View File

@@ -0,0 +1,44 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const programKesehatanJson = loadJsonData("kesehatan/program-kesehatan/program-kesehatan.json");
export async function seedProgramKesehatan() {
for (const p of programKesehatanJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for program kesehatan "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.programKesehatan.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsiSingkat: p.deskripsiSingkat,
deskripsi: p.deskripsi,
imageId,
},
create: {
id: p.id,
name: p.name,
deskripsiSingkat: p.deskripsiSingkat,
deskripsi: p.deskripsi,
imageId,
},
});
console.log(`✅ Program kesehatan seeded: ${p.name}`);
}
}

View File

@@ -0,0 +1,97 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const puskesmasJson = loadJsonData("kesehatan/puskesmas/puskesmas.json");
const kontakPuskesmasJson = loadJsonData("kesehatan/puskesmas/kontak-puskesmas/kontak.json");
const jamPuskesmasJson = loadJsonData("kesehatan/puskesmas/jam-puskesmas/jam.json");
export async function seedPuskesmas() {
console.log("🔄 Seeding Kontak Puskesmas...");
for (const k of kontakPuskesmasJson) {
await prisma.kontakPuskesmas.upsert({
where: {
id: k.id,
},
update: {
kontakPuskesmas: k.kontakPuskesmas,
email: k.email,
facebook: k.facebook,
kontakUGD: k.kontakUGD,
},
create: {
id: k.id,
kontakPuskesmas: k.kontakPuskesmas,
email: k.email,
facebook: k.facebook,
kontakUGD: k.kontakUGD,
},
});
}
console.log("kontak puskesmas success ...");
console.log("🔄 Seeding Jam Puskesmas...");
for (const k of jamPuskesmasJson) {
await prisma.jamOperasional.upsert({
where: {
id: k.id,
},
update: {
workDays: k.workDays,
weekDays: k.weekDays,
holiday: k.holiday,
},
create: {
id: k.id,
workDays: k.workDays,
weekDays: k.weekDays,
holiday: k.holiday,
},
});
}
console.log("jam puskesmas success ...");
console.log("🔄 Seeding Puskesmas...");
for (const p of puskesmasJson) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for puskesmas "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.puskesmas.upsert({
where: { id: p.id },
update: {
name: p.name,
alamat: p.alamat,
jamId: p.jamId,
kontakId: p.kontakId,
imageId,
},
create: {
id: p.id,
name: p.name,
alamat: p.alamat,
jamId: p.jamId,
kontakId: p.kontakId,
imageId,
},
});
console.log(`✅ Puskesmas seeded: ${p.name}`);
}
console.log("🎉 Puskesmas seed selesai");
}

View File

@@ -0,0 +1,32 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const grafikKepuasanJson = loadJsonData("kesehatan/grafik-kepuasan/grafik-kepuasan.json");
export async function seedGrafikKepuasan() {
console.log("Seeding Grafik Kepuasan...");
for (const item of grafikKepuasanJson) {
await prisma.grafikKepuasan.upsert({
where: { id: item.id },
update: {
nama: item.nama,
tanggal: new Date(item.tanggal),
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyakit: item.penyakit,
},
create: {
id: item.id,
nama: item.nama,
tanggal: new Date(item.tanggal),
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyakit: item.penyakit,
},
});
console.log(` Grafik Kepuasan: ${item.nama}`);
}
console.log("Grafik Kepuasan seed selesai");
}

View File

@@ -0,0 +1,70 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const kelahiranJson = loadJsonData("kesehatan/kelahiran/kelahiran.json");
const kematianJson = loadJsonData("kesehatan/kematian/kematian.json");
const dataKematianKelahiranJson = loadJsonData("kesehatan/kematian-kelahiran/data-kematian-kelahiran.json");
export async function seedKelahiranKematian() {
console.log("Seeding Kelahiran...");
for (const item of kelahiranJson) {
await prisma.kelahiran.upsert({
where: { id: item.id },
update: {
nama: item.nama,
tanggal: new Date(item.tanggal),
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
},
create: {
id: item.id,
nama: item.nama,
tanggal: new Date(item.tanggal),
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
},
});
console.log(` Kelahiran: ${item.nama}`);
}
console.log("Seeding Kematian...");
for (const item of kematianJson) {
await prisma.kematian.upsert({
where: { id: item.id },
update: {
nama: item.nama,
tanggal: new Date(item.tanggal),
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyebab: item.penyebab,
},
create: {
id: item.id,
nama: item.nama,
tanggal: new Date(item.tanggal),
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyebab: item.penyebab,
},
});
console.log(` Kematian: ${item.nama}`);
}
console.log("Seeding Data Kematian-Kelahiran...");
for (const item of dataKematianKelahiranJson) {
await prisma.dataKematian_Kelahiran.upsert({
where: { id: item.id },
update: {
kematianId: item.kematianId,
kelahiranId: item.kelahiranId,
},
create: {
id: item.id,
kematianId: item.kematianId,
kelahiranId: item.kelahiranId,
},
});
}
console.log("Kelahiran & Kematian seed selesai");
}

View File

@@ -0,0 +1,40 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const kategoriDesaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json");
const desaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/desaantiKorpusi.json");
export async function seedDesaAntiKorupsi() {
for (const k of kategoriDesaAntiKorupsi) {
await prisma.kategoriDesaAntiKorupsi.upsert({
where: { id: k.id },
update: {
name: k.name,
},
create: {
id: k.id,
name: k.name,
},
});
}
console.log("kategori desa anti korupsi success ...");
// =========== DESA ANTI KORUPSI ===========
for (const p of desaAntiKorupsi) {
await prisma.desaAntiKorupsi.upsert({
where: { id: p.id },
update: {
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
create: {
id: p.id,
name: p.name,
deskripsi: p.deskripsi,
kategoriId: p.kategoriId,
},
});
}
console.log("desa anti korupsi success ...");
}

View File

@@ -0,0 +1,63 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const prestasiDesa = loadJsonData("landing-page/prestasi-desa/prestasi-desa.json");
const kategoriPrestasiDesa = loadJsonData("landing-page/prestasi-desa/kategori-prestasi.json");
export async function seedPrestasiDesa() {
console.log("🔄 Seeding Kategori Prestasi Desa...");
for (const c of kategoriPrestasiDesa) {
await prisma.kategoriPrestasiDesa.upsert({
where: { id: c.id },
update: {
name: c.name,
},
create: {
id: c.id,
name: c.name,
},
});
}
console.log("kategori prestasi desa success ...");
console.log("🔄 Seeding Prestasi Desa...");
for (const m of prestasiDesa) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for prestasi desa "${m.name}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.prestasiDesa.upsert({
where: { id: m.id },
update: {
name: m.name,
deskripsi: m.deskripsi,
kategoriId: m.kategoriId,
imageId,
},
create: {
id: m.id,
name: m.name,
deskripsi: m.deskripsi,
kategoriId: m.kategoriId,
imageId,
},
});
}
console.log("prestasi desa success ...");
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const mediaSosial = loadJsonData("landing-page/profile/mediaSosial.json");
export async function seedMediaSosial() {
console.log("🔄 Seeding Media Sosial...");
for (const m of mediaSosial) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for berita "${m.name}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.mediaSosial.upsert({
where: { id: m.id },
update: {
name: m.name,
iconUrl: m.iconUrl,
imageId,
},
create: {
id: m.id,
name: m.name,
iconUrl: m.iconUrl,
imageId,
},
});
}
console.log("media sosial success ...");
}

View File

@@ -0,0 +1,42 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const profilePejabatDesa = loadJsonData("landing-page/profile/profile.json");
export async function seedProfileLP() {
console.log("🔄 Seeding Pejabat Desa...");
for (const p of profilePejabatDesa) {
let imageId: string | null = null;
if (p.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: p.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for profile "${p.name}": ${p.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.pejabatDesa.upsert({
where: { id: p.id },
update: {
name: p.name,
position: p.position,
imageId,
},
create: {
id: p.id,
name: p.name,
position: p.position,
imageId,
},
});
}
console.log("✅ Pejabat Desa seeding completed");
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const programInovasi = loadJsonData("landing-page/profile/programInovasi.json");
export async function seedProgramInovasi() {
console.log("🔄 Seeding Program Inovasi...");
for (const b of programInovasi) {
let imageId: string | null = null;
if (b.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: b.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for program inovasi "${b.name}": ${b.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.programInovasi.upsert({
where: { id: b.id },
update: {
name: b.name,
description: b.description,
link: b.link,
imageId,
},
create: {
id: b.id,
name: b.name,
description: b.description,
link: b.link,
imageId,
},
});
console.log(`✅ Program Inovasi seeded: ${b.name}`);
}
}

View File

@@ -0,0 +1,43 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../../load-json";
const sdgsDesa = loadJsonData("landing-page/sdgs-desa/sdgs-desa.json");
export async function seedSDGSDesa() {
console.log("🔄 Seeding SDGS Desa...");
for (const m of sdgsDesa) {
let imageId: string | null = null;
if (m.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: m.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for sdgs desa "${m.name}": ${m.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.sdgsDesa.upsert({
where: { id: m.id },
update: {
name: m.name,
jumlah: m.jumlah,
imageId,
},
create: {
id: m.id,
name: m.name,
jumlah: m.jumlah,
imageId,
},
});
}
console.log("sdgs desa success ...");
}

View File

@@ -0,0 +1,73 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const kategoriGotongRoyong = loadJsonData("lingkungan/gotong-royong/kategori-gotong-royong.json");
const gotongRoyong = loadJsonData("lingkungan/gotong-royong/gotong-royong.json");
export async function seedDataGotongRoyong() {
console.log("🔄 Seeding Kategori Gotong Royong...");
for (const k of kategoriGotongRoyong) {
await prisma.kategoriKegiatan.upsert({
where: {
id: k.id,
},
update: {
nama: k.nama,
},
create: {
id: k.id,
nama: k.nama,
},
});
}
console.log("✅ Kategori Gotong Royong seeded successfully");
console.log("🔄 Seeding Gotong Royong...");
for (const k of gotongRoyong) {
let imageId: string | null = null;
if (k.imageName) {
const image = await prisma.fileStorage.findUnique({
where: { name: k.imageName },
select: { id: true },
});
if (!image) {
console.warn(
`⚠️ Image not found for gotong royong "${k.judul}": ${k.imageName}`,
);
} else {
imageId = image.id;
}
}
await prisma.kegiatanDesa.upsert({
where: {
id: k.id,
},
update: {
judul: k.judul,
deskripsiSingkat: k.deskripsiSingkat,
deskripsiLengkap: k.deskripsiLengkap,
tanggal: k.tanggal,
lokasi: k.lokasi,
partisipan: k.partisipan,
imageId: imageId,
kategoriKegiatanId: k.kategoriKegiatanId,
},
create: {
id: k.id,
judul: k.judul,
deskripsiSingkat: k.deskripsiSingkat,
deskripsiLengkap: k.deskripsiLengkap,
tanggal: k.tanggal,
lokasi: k.lokasi,
partisipan: k.partisipan,
imageId: imageId,
kategoriKegiatanId: k.kategoriKegiatanId,
},
});
}
console.log("✅ Gotong Royong seeded successfully");
}

View File

@@ -0,0 +1,29 @@
import prisma from "@/lib/prisma";
import { loadJsonData } from "../../load-json";
const dataLingkunganDesa = loadJsonData("lingkungan/data-lingkungan-desa/data-lingkungan-desa.json");
export async function seedDataLingkunganDesa() {
console.log("🔄 Seeding Data Lingkungan Desa...");
for (const p of dataLingkunganDesa) {
await prisma.dataLingkunganDesa.upsert({
where: {
id: p.id,
},
update: {
name: p.name,
jumlah: p.jumlah,
deskripsi: p.deskripsi,
icon: p.icon,
},
create: {
id: p.id,
name: p.name,
jumlah: p.jumlah,
deskripsi: p.deskripsi,
icon: p.icon,
},
});
}
console.log("✅ Data Lingkungan Desa seeded successfully");
}

Some files were not shown because too many files have changed in this diff Show More