Compare commits

...

417 Commits

Author SHA1 Message Date
b9d43eb723 fix(storage): migrate domain-specific delete and update handlers to MinIO
- Replaces local filesystem operations (fs.unlink, path.join) with MinIO removeObject in all domain lib handlers
- Updated handlers for: Berita, GalleryFoto, Layanan, Musik, Penghargaan, Potensi, Profile, Inovasi, Keamanan, Kesehatan, LandingPage, and PPID
- bump: version 0.1.19 -> 0.1.20
2026-04-23 17:05:44 +08:00
37940fc7e2 fix(img): fix WebP Content-Type bug and seed 232 missing FileStorage records
- img.ts: replace hardcoded 'image/jpeg' Content-Type with dynamic MIME_MAP
  lookup per file extension — WebP files now served with correct 'image/webp'
  header so browsers can decode them
- img.ts: skip sharp resize when no size param (serve original buffer directly)
- Adds migration 20260423072135 to add stok and umkmId columns to PasarDesa
- FileStorage DB now has all 232 MinIO images seeded (was 80, missing 152)
- All domain records (Berita 18/18, GalleryFoto 3/3, PasarDesa 4/4, etc.)
  now have imageId properly linked after full re-seed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 15:41:20 +08:00
2958950585 fix(storage): migrate fileStorage handlers and seeder from local disk/Seafile to MinIO
Root cause: images not showing because:
1. seed_assets.ts was listing from Seafile (unreliable, 502 errors)
2. fileStorage create/findUniq/del handlers used local disk (not available in containers)
3. link format in file-storage.json had inconsistent path prefix

Changes:
- seed_assets.ts: list objects from MinIO bucket instead of Seafile API; seed
  link as /api/img/{name}, path as "image" (matches MinIO prefix)
- fileStorage/create.ts: upload image/audio/document to MinIO via putObject;
  remove WIBU_UPLOAD_DIR dependency and all fs.writeFile calls
- fileStorage/findUniq.ts: stream file from MinIO via getObject; remove
  WIBU_UPLOAD_DIR and fs.readFile
- fileStorage/del.ts: delete from MinIO via removeObject; remove fs.unlink
- file-storage.json: fix path field to "image" (was full path); all 80 entries
  already have correct link=/api/img/{name} format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 14:28:08 +08:00
d145611221 bump: version 0.1.18 -> 0.1.19 2026-04-23 13:53:17 +08:00
437e9aa13c fix(build): resolve type errors in img.ts and cleanup route.ts 2026-04-23 13:52:57 +08:00
55d0735fcf merge(stg): merge main into stg and resolve storage conflicts 2026-04-23 13:48:26 +08:00
00a81d7dba Migrate Minio 2026-04-23 12:13:25 +08:00
b9b00f0a20 docs(qc): add quality control summaries for various modules
Added comprehensive QC reports and fix summaries for:
- Desa (Berita, Potensi, Profil, Layanan, Penghargaan, Pengumuman)
- Kesehatan (Posyandu)
- Landing Page (APBDes, SDGS, Anti-Korupsi, Profil, Prestasi)
- PPID (Daftar Informasi, Dasar Hukum, IKM, Permohonan, Struktur, Visi Misi)
2026-04-23 12:11:55 +08:00
fec6b79743 feat(storage): add Seafile→MinIO migration script and asset manifest
- migrate-seafile-to-minio.ts: downloads 80 assets from Seafile public
  share and re-uploads to MinIO with identical filenames (idempotent,
  skips existing objects)
- file-storage.json: asset manifest with names and Seafile download URLs
  used as migration source

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 12:06:40 +08:00
6fc79f7541 feat(storage): migrate file storage from local disk to MinIO
Replace local filesystem-based image storage with MinIO S3-compatible
object storage. All upload, serve, delete, and list operations now use
the MinIO bucket defined in MINIO_* env vars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 11:40:43 +08:00
1a48c15c87 refactor(ekonomi): consolidate Pasar Desa into UMKM module
- Remove "Pasar Desa" as a separate entity; products are now strictly linked to UMKM.
- Delete redundant Pasar Desa API endpoints and state management.
- Update Admin UI: remove "Pasar Desa" menu and unified product management under UMKM.
- Update Public UI: replace "Pasar Desa" with "UMKM" in navbar and unified hub at /darmasaba/ekonomi/umkm.
- Implement mandatory umkmId in PasarDesa model and update seeders accordingly.
- Fix UI bugs, missing imports, and invalid API filters for mandatory umkmId.
- Increment version to 0.1.18.
2026-04-21 17:52:08 +08:00
e286cb4f2b feat(ekonomi): unify UMKM and Pasar Desa models, add business profile and product forms 2026-04-21 15:19:57 +08:00
a2d157ee02 fix(umkm): fix TypeError, 404 API URL, and Recharts warnings 2026-04-21 12:23:22 +08:00
ece84fabf0 docs: update memory and deployment workflow in GEMINI.md and QWEN.md 2026-04-21 12:11:30 +08:00
59981683db feat(public-ui): unify Pasar Desa and UMKM into a single tabbed page 2026-04-21 11:34:10 +08:00
1a74a1f683 feat(public-ui): implement public UMKM directory, detail and product catalog pages 2026-04-20 17:44:36 +08:00
b673e36a45 feat(admin-ui): implement UMKM admin dashboard and CRUD pages 2026-04-20 17:15:54 +08:00
62aa9b63b2 merge: resolve conflicts with stg 2026-04-20 16:54:19 +08:00
58ab306428 feat(ekonomi): implement UMKM module with CRUD API and Dashboard analytics 2026-04-20 16:51:59 +08:00
97902f6277 bump: version 0.1.10 -> 0.1.11
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:18:58 +08:00
ef7d1752de fix(docker): remove incorrect _seeder_list path, seeders are in prisma/ already
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:18:49 +08:00
f9de4b7a35 bump: version 0.1.9 -> 0.1.10
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:03:01 +08:00
13873c9fe7 fix(docker): copy only seed-related files to runner stage
Instead of full src/, copy only prisma/, src/lib/, src/_seeder_list/
and tsconfig.json for seed script support. Reduces image size.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:02:54 +08:00
03b084d9d4 bump: version 0.1.8 -> 0.1.9 - fix tsconfig.json in docker image
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 14:31:11 +08:00
5df9698599 fix(docker): copy tsconfig.json to runner stage for prisma seed support
Seed script uses @/* path aliases which require tsconfig.json.
Without it, 'bunx prisma db seed' fails with 'no such file or directory'.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 14:30:52 +08:00
3d3e5ffc87 docs: update deployment workflow instructions in QWEN.md
Add detailed deployment workflow:
- Version bump before deploy
- Trigger publish.yml with ref=main, stack_env=stg, tag=<version>
- Wait for publish to complete
- Trigger re-pull.yml with ref=main, stack_name=desa-darmasaba, stack_env=stg
- Include GitHub CLI commands for reference

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 12:08:21 +08:00
e80e333eed fix(migration): move JenisMigrasi enum creation before MigrasiPenduduk table
Enum must be created before it's used in CREATE TABLE.
This fixes P3018 error on staging where JenisMigrasi type does not exist.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:58:08 +08:00
b1289831f3 Revert "fix(deployment): handle failed migrations automatically in docker-entrypoint"
This reverts commit 3c4e273e26.
2026-04-14 11:53:35 +08:00
3c4e273e26 fix(deployment): handle failed migrations automatically in docker-entrypoint
- Detect failed migrations in _prisma_migrations table
- Auto resolve with 'prisma migrate resolve --rolled-back'
- Fallback to 'prisma db push' if resolve fails
- Ensures staging/prod can deploy even with migration history issues

Fixes P3009 error on staging where 20260406032433_init migration failed

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:26:49 +08:00
de4563c914 fix: fileStorage path issues between local and staging environments
- Store relative paths in database instead of absolute paths
- Reconstruct absolute paths at runtime using UPLOAD_DIR env var
- Add VOLUME for /app/uploads in Dockerfile for persistent storage
- Create uploads directory with proper permissions in Docker
- Add error handling for missing UPLOAD_DIR in findUniq.ts
- Simplify GitHub workflow memory in QWEN.md (manual handling)

This fixes the 500 errors on staging for file create/delete operations
caused by environment-specific absolute paths stored in database.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
11ff5f5c01 bump: version 0.1.7 -> 0.1.8 - add kependudukan migration
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
ed44222594 fix(database): add migration for kependudukan tables
- Add DataBanjar, DistribusiAgama, DistribusiUmur, MigrasiPenduduk, DinamikaPenduduk tables
- Add indexes for performance (tahun, isActive)
- Add JenisMigrasi enum (MASUK, KELUAR)
- Fixes: Error 500 on all CRUD kependudukan endpoints in staging

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
fd7579d6d3 bump: version 0.1.6 -> 0.1.7 - auto migration on startup
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
e7c3c020c2 fix(deployment): add auto database migration on container startup
- Create docker-entrypoint.sh to run prisma migrate deploy before app start
- Update Dockerfile to use entrypoint script
- Ensures database schema is always up-to-date after deployment
- Fixes: CRUD kependudukan error 500 di staging karena tabel belum dibuat

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
6873e84848 bump: version 0.1.5 -> 0.1.6 - fix migrasi penduduk schema mismatch
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
74dc9e5c18 docs: update QWEN.md
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
04001c905b fix(kependudukan): remove jenisKelamin field and align MigrasiPenduduk with database schema
- Remove jenisKelamin field from API, state, and UI components
- Fix MigrasiPenduduk API to use null instead of undefined for optional fields
- Update create/edit forms to properly handle asal/tujuan fields based on jenis
- Fix DatePickerInput type handling with valueFormat prop
- Update list page to display asal or tujuan conditionally
- Add proper select statements in API responses
- Fix TypeScript type errors in migrasi-penduduk module

Closes: Schema mismatch causing errors when inputting migrasi penduduk data

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 11:03:11 +08:00
656ffcc561 bump: version 0.1.7 -> 0.1.8 - add kependudukan migration
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 17:41:16 +08:00
76ffa662c5 fix(database): add migration for kependudukan tables
- Add DataBanjar, DistribusiAgama, DistribusiUmur, MigrasiPenduduk, DinamikaPenduduk tables
- Add indexes for performance (tahun, isActive)
- Add JenisMigrasi enum (MASUK, KELUAR)
- Fixes: Error 500 on all CRUD kependudukan endpoints in staging

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 17:40:53 +08:00
46423409fd bump: version 0.1.6 -> 0.1.7 - auto migration on startup
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 17:01:08 +08:00
2edf5e9b11 fix(deployment): add auto database migration on container startup
- Create docker-entrypoint.sh to run prisma migrate deploy before app start
- Update Dockerfile to use entrypoint script
- Ensures database schema is always up-to-date after deployment
- Fixes: CRUD kependudukan error 500 di staging karena tabel belum dibuat

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 17:00:53 +08:00
af368eeee0 bump: version 0.1.5 -> 0.1.6 - fix migrasi penduduk schema mismatch
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 16:08:10 +08:00
e104cd8fcc docs: update QWEN.md
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 16:08:01 +08:00
50801e5c8a fix(kependudukan): remove jenisKelamin field and align MigrasiPenduduk with database schema
- Remove jenisKelamin field from API, state, and UI components
- Fix MigrasiPenduduk API to use null instead of undefined for optional fields
- Update create/edit forms to properly handle asal/tujuan fields based on jenis
- Fix DatePickerInput type handling with valueFormat prop
- Update list page to display asal or tujuan conditionally
- Add proper select statements in API responses
- Fix TypeScript type errors in migrasi-penduduk module

Closes: Schema mismatch causing errors when inputting migrasi penduduk data

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 15:53:58 +08:00
62a9a49502 Merge pull request #21 from bipprojectbali/tasks/kependudukan/fix-typescript-types-and-cleanup-20260413-1500
refactor(kependudukan): improve TypeScript types and clean up code
2026-04-13 15:01:39 +08:00
80186bf493 refactor(kependudukan): improve TypeScript types and clean up code
- Add proper TypeScript interfaces for seeder files
- Rename MigrasiPendudukForm interface for consistency
- Separate asal/tujuan fields in MigrasiPenduduk API based on jenis
- Remove unnecessary eslint-disable comments
- Add local type definitions for public kependudukan pages
- Clean up unused imports (React, Flex, IconBuilding)
- Improve type safety in form handlers (handleChangeText vs handleChangeSelect)
- Add explicit type casting where needed to fix type errors

Co-authored-by: Qwen Code

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 15:00:33 +08:00
e669dcee25 Merge pull request #20 from bipprojectbali/tasks/kependudukan/add-seeders-api-year-filter-navbar-menu-20260410-1430
Tasks/kependudukan/add seeders api year filter navbar menu 20260410 1430
2026-04-13 11:09:09 +08:00
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
2028 changed files with 194151 additions and 58765 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

14
.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,15 @@ next-env.d.ts
# uploads
/uploads
# download
/download
# cache
/cache
.github/
.env.*
*.tar.gz
# local scripts
ai.sh

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": []
}

View File

@@ -0,0 +1,5 @@
# Memory Context from Past Sessions
*No context yet. Complete your first session and context will appear here.*
Use claude-mem's MCP search tools for manual memory queries.

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.).

137
CLAUDE.md Normal file
View File

@@ -0,0 +1,137 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Desa Darmasaba is a full-stack digital village management platform for a village in Badung, Bali. It serves both a public-facing website (`/darmasaba/*`) and an admin CMS (`/admin/*`).
## Commands
```bash
# Development
bun run dev # Start dev server (port 3000)
bun run build # Production build
bun run tsc --noEmit # Type-check only
# Testing
bun run test # All tests
bun run test:api # Unit tests (Vitest)
bun run test:e2e # E2E tests (Playwright)
# Database
bunx prisma migrate deploy # Apply migrations
bunx prisma migrate dev --name <name> # Create migration
bun run prisma/seed.ts # Seed database
bunx prisma studio # Interactive DB viewer
# Linting
bun eslint . --fix
```
## Architecture
### Tech Stack
- **Framework**: Next.js 15 (App Router) + React 19
- **Runtime/Package manager**: Bun (not npm)
- **API server**: Elysia.js (mounted at `/api/[[...slugs]]`)
- **ORM**: Prisma + PostgreSQL
- **UI**: Mantine UI v7-8
- **State**: Jotai (atoms), Valtio (proxies), SWR (data fetching)
- **Auth**: iron-session + JWT
- **File storage**: Local uploads + Seafile (self-hosted)
### Request Flow
```
Browser → Next.js middleware (src/middleware.ts)
→ Public pages: src/app/darmasaba/
→ Admin pages: src/app/admin/
→ API: src/app/api/[[...slugs]]/route.ts (Elysia.js)
└── _lib/*.ts (domain modules)
```
The Elysia server is a single entry point with domain-specific modules: `desa.ts`, `kesehatan.ts`, `ekonomi.ts`, `keamanan.ts`, `lingkungan.ts`, `pendidikan.ts`, `kependudukan.ts`, `ppid.ts`, `inovasi.ts`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`.
### Domain Modules
Each domain (desa, kesehatan, ekonomi, etc.) has:
- API handler in `src/app/api/[[...slugs]]/_lib/<domain>.ts`
- Admin CMS pages in `src/app/admin/(dashboard)/<domain>/`
- Public pages in `src/app/darmasaba/(pages)/<domain>/`
### Database (Prisma)
- Schema at `prisma/schema.prisma` (~2400 lines, 100+ models)
- Common model conventions: `@default(cuid())` IDs, `createdAt`/`updatedAt` timestamps, `deletedAt DateTime?` (soft delete), `isActive Boolean @default(true)`
- Seeders per-module in `prisma/_seeder_list/`, orchestrated by `prisma/seed.ts`
### Authentication Flow
1. User submits phone → OTP sent (email/SMS)
2. OTP validated → JWT created + iron-session stored
3. `UserSession` model tracks active sessions
4. `src/middleware.ts` validates on each request
5. `src/lib/api-auth.ts` handles JWT/session checks in API routes
### File Handling
All uploaded files reference the `FileStorage` Prisma model. Uploads land in `WIBU_UPLOAD_DIR` (default: `uploads/`). Seafile is the external storage fallback.
## Key Files
| File | Purpose |
|------|---------|
| `src/middleware.ts` | Route guards and auth |
| `src/lib/prisma.ts` | Prisma client singleton |
| `src/lib/api-auth.ts` | JWT/session validation |
| `src/lib/api-fetch.ts` | Typed fetch wrapper used by frontend |
| `src/lib/session.ts` | iron-session config |
| `next.config.ts` | Next.js config (cache headers, allowed origins) |
| `postcss.config.cjs` | Mantine CSS preset and breakpoints |
| `docker-entrypoint.sh` | Runs `prisma migrate deploy` then starts app |
## Environment Variables
Copy `.env.example` to `.env`. Required variables:
```env
DATABASE_URL="postgresql://..."
NEXT_PUBLIC_BASE_URL="/"
BASE_SESSION_KEY="..." # random string
BASE_TOKEN_KEY="..." # random string
SESSION_PASSWORD="..." # min 32 chars
SEAFILE_TOKEN="..."
SEAFILE_REPO_ID="..."
SEAFILE_URL="..."
```
## Docker
Multi-stage build: `oven/bun:1-debian` → builder → runner. The runner creates a `nextjs` user (UID 1001), exposes port 3000, and mounts `/app/uploads` as a volume. Entrypoint runs migrations automatically.
## CI/CD
GitHub Actions workflows in `.github/workflows/`:
- `docker-publish.yml` — triggers on `v*` tags, pushes to GHCR
- `publish.yml` — manual build & push
- `re-pull.yml` — triggers Portainer to redeploy latest image
To release: tag with `git tag -a v0.1.x -m "..."` and push the tag.
### Workflow for Code Changes
1. **Commit** existing changes before starting new work
2. **Create plan** at `MIND/PLAN/[plan-name].md`
3. **Create task** at `MIND/PLAN/[task-name].md`
4. **Execute the task** and update task progress
5. **Create summary** at `MIND/SUMMARY/[summary-name].md` when done
6. **Run build** (`bun run build`) to ensure no compile errors
7. **Fix any build errors** if they occur
8. **Commit** all changes AFTER successful build
9. **Update version** in `package.json` for every change
10. **Push** to new branch with format: `tasks/[task-name]/[what-is-being-done]/[date-time]`
11. **Push ke 2 Remote** - Push ke 2 remote origin dan deploy
12. **Merge ke Branch** - Merge ke branch target (biasanya `stg` untuk staging atau `prod` untuk production) ke 2 remote origin dan deploy
### GitHub Workflows
1. **publish.yml**: Uses branch `main`, stack env and image tag matching version from `package.json`.
2. **re-pull.yml**: **Wait for `publish.yml` to complete successfully before running.** Uses branch `main`, stack env and stack name `desa-darmasaba`.
### After Progress
- Always give option to continue to GitHub workflows or not

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)

76
Dockerfile Normal file
View File

@@ -0,0 +1,76 @@
# ==============================
# 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/src/lib ./src/lib
COPY --from=builder --chown=nextjs:nodejs /app/tsconfig.json ./tsconfig.json
COPY --from=builder --chown=nextjs:nodejs /app/next.config.* ./
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
# Create uploads directory with proper permissions
RUN mkdir -p /app/uploads && chown nextjs:nodejs /app/uploads
USER nextjs
# Persistent storage for uploaded files
VOLUME ["/app/uploads"]
EXPOSE 3000
CMD ["/app/docker-entrypoint.sh"]

244
GEMINI.md Normal file
View File

@@ -0,0 +1,244 @@
# 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
### Workflow for Code Changes
1. **Commit** existing changes before starting new work
2. **Create plan** at `MIND/PLAN/[plan-name].md`
3. **Create task** at `MIND/PLAN/[task-name].md`
4. **Execute the task** and update task progress
5. **Create summary** at `MIND/SUMMARY/[summary-name].md` when done
6. **Run build** (`bun run build`) to ensure no compile errors
7. **Fix any build errors** if they occur
8. **Commit** all changes AFTER successful build
9. **Update version** in `package.json` for every change
10. **Push** to new branch with format: `tasks/[task-name]/[what-is-being-done]/[date-time]`
11. **Push ke 2 Remote** - Push ke 2 remote origin dan deploy
12. **Merge ke Branch** - Merge ke branch target (biasanya `stg` untuk staging atau `prod` untuk production) ke 2 remote origin dan deploy
### GitHub Workflows
1. **publish.yml**: Uses branch `main`, stack env and image tag matching version from `package.json`.
2. **re-pull.yml**: **Wait for `publish.yml` to complete successfully before running.** Uses branch `main`, stack env and stack name `desa-darmasaba`.
### After Progress
- Always give option to continue to GitHub workflows or not

View File

@@ -0,0 +1,24 @@
# Plan: Fix 3 Bugs in UMKM Module
## 1. TypeError: Cannot set properties of undefined (setting 'loading')
- **File**: `src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx`
- **Root Cause**: `load` method is destructured from Valtio proxy, causing `this` binding to be lost.
- **Fix**: Remove `load` from destructuring and call it directly via `umkmState.produk.findMany.load` or `umkmState.umkm.findMany.load`.
## 2. 404 Not Found - Category Product API
- **File**: `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`
- **Root Cause**: Incorrect API URL for fetching category products.
- **Fix**: Update URL from `/api/ekonomi/pasar-desa/kategori-produk/find-many-all` to `/api/ekonomi/kategoriproduk/find-many-all`.
## 3. Recharts Warning: width(-1) height(-1)
- **Location**: UMKM Admin Dashboard.
- **Root Cause**: Missing explicit height on chart container.
- **Fix**: Add `style={{ height: 300 }}` to the container and wrap charts with `ResponsiveContainer`.
## Steps:
1. Fix `src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx`.
2. Fix `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`.
3. Locate and fix chart containers in UMKM admin dashboard.
4. Verify changes locally.
5. Run build to ensure no compile errors.
6. Commit and deploy.

View File

@@ -0,0 +1,24 @@
# Plan: Refactor UMKM and Pasar Desa (Consolidation)
## Objective
Consolidate "Pasar Desa" into the UMKM module. Pasar Desa is no longer a separate entity; it is now strictly a collection of products belonging to UMKM entities.
## Steps:
1. **Cleanup API**: Remove `PasarDesa` and `KategoriProduk` (from `pasar-desa` folder) imports from `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts`.
2. **Admin UI**:
- Remove "Pasar Desa" menu from `src/app/admin/_com/list_PageAdmin.tsx`.
- Ensure "UMKM" menu handles all product management.
3. **Public UI**:
- Remove "Pasar Desa" from `src/con/navbar-list-menu.ts`.
- Refactor `src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx` to remove the "Produk Pasar Desa" tab.
- Rename the page or adjust its purpose to be the unified UMKM/Product hub.
4. **Prisma Schema**:
- Ensure `umkmId` is mandatory in `PasarDesa` model (already seems to be).
- (Optional) Rename `PasarDesa` to `ProdukUmkm` if requested, but user said it's optional. For now, keep it as `PasarDesa` to minimize breaking changes.
5. **Build & Verify**: Run `bun run build` and check for any broken references.
## Verification:
- No "Pasar Desa" menu in Admin.
- No "Pasar Desa" menu in Public Navbar.
- Public page `/darmasaba/ekonomi/pasar-desa` (or new path) shows UMKM products only.
- Successful build.

View File

@@ -0,0 +1,26 @@
# Plan: Refactor UMKM and Pasar Desa Model
## Objective
Unify `ProdukUmkm` and `PasarDesa` into a single `PasarDesa` model to avoid data redundancy and simplify management.
## Changes:
1. **Schema Refactor**:
- Merge fields from `ProdukUmkm` (`stok`, `umkmId`) into `PasarDesa`.
- Update `PenjualanProduk` to relate directly to `PasarDesa`.
- Remove `ProdukUmkm` model.
- Update `FileStorage` relations.
2. **Backend/API Refactor**:
- Update Pasar Desa `findMany` to only show products where `umkmId` is null.
- Update UMKM Produk APIs (`create`, `updt`, `findMany`, `del`) to use the `PasarDesa` model with `umkmId` filter.
- Update Penjualan logic to adjust `stok` in `PasarDesa`.
- Update UMKM Dashboard analytics to query `PasarDesa`.
3. **Admin UI Refactor**:
- Update `umkmState` to handle `kategoriId` for products.
- Create "Tambah UMKM" form for business profile management.
- Create "Tambah Produk UMKM" form for product management with `umkmId` binding.
- Update list views to link to the new forms.
- Implement logical separation between "Pasar Desa Admin" and "UMKM Admin" contexts.
## Verification:
- Successful build (`bun run build`).
- Verify API responses for both Pasar Desa and UMKM Produk filters.

View File

@@ -0,0 +1,6 @@
# Task: Fix UMKM Module Bugs
- [x] Fix TypeError in `src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx` <!-- id: 0 -->
- [x] Fix 404 API URL in `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts` <!-- id: 1 -->
- [x] Fix Recharts warning in UMKM admin dashboard <!-- id: 2 -->
- [x] Run build and verify <!-- id: 3 -->

View File

@@ -0,0 +1,8 @@
# Task: Refactor UMKM and Pasar Desa (Consolidation)
- [ ] Cleanup API imports in `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts` <!-- id: 0 -->
- [ ] Remove "Pasar Desa" menu in `src/app/admin/_com/list_PageAdmin.tsx` <!-- id: 1 -->
- [ ] Remove "Pasar Desa" from public navbar in `src/con/navbar-list-menu.ts` <!-- id: 2 -->
- [ ] Refactor public page `src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx` <!-- id: 3 -->
- [ ] Run build and fix errors <!-- id: 4 -->
- [ ] Update version and commit <!-- id: 5 -->

View File

@@ -0,0 +1,10 @@
# Task: Refactor UMKM and Pasar Desa Model
- [x] Refactor `prisma/schema.prisma` and run `db push` <!-- id: 0 -->
- [x] Update Pasar Desa `findMany` API with `umkmId: null` filter <!-- id: 1 -->
- [x] Update UMKM Produk APIs (CRUD) to use `PasarDesa` model <!-- id: 2 -->
- [x] Update UMKM Dashboard analytics and Penjualan logic <!-- id: 3 -->
- [x] Create Admin Form for "Data UMKM" (Business Profile) <!-- id: 4 -->
- [x] Create Admin Form for "Produk UMKM" (Product) <!-- id: 5 -->
- [x] Link list views to new forms and update state <!-- id: 6 -->
- [ ] Run build and verify <!-- id: 7 -->

34
MIND/PLAN/umkm-module.md Normal file
View File

@@ -0,0 +1,34 @@
# Plan: UMKM Module Implementation
## Goal
Implement UMKM, ProdukUmkm, and PenjualanProduk module with CRUD API and Dashboard analytics.
## Steps
1. Update Prisma Schema (already done in file).
2. Run database migration and seed data.
3. Implement UMKM CRUD API.
4. Implement ProdukUmkm CRUD API.
5. Implement PenjualanProduk CRUD API.
6. Implement Dashboard API (KPI, Summary, Top Produk, Detail Penjualan).
7. Register all routers in the ekonomi module.
8. Verify with type check and build.
## Progress
- [x] Step 1: Update Prisma Schema
- [x] Step 2: Run database migration
- [x] Step 3: Implement UMKM CRUD API
- [x] Step 4: Implement ProdukUmkm CRUD API
- [x] Step 5: Implement PenjualanProduk CRUD API
- [x] Step 6: Implement Dashboard API
- [x] Step 7: Register routers
- [x] Step 8: Verify changes
- [x] Step 9: Implement Admin UI Layout and Tabs
- [x] Step 10: Implement Dashboard UI Page
- [x] Step 11: Implement Data UMKM UI Page
- [x] Step 12: Implement Produk UI Page
- [x] Step 13: Implement Penjualan UI Page
- [x] Step 14: Register UI pages in Admin Menu
- [x] Step 15: Implement Public UMKM Directory Page
- [x] Step 16: Implement Public UMKM Detail Page
- [x] Step 17: Implement Public Product Catalog Page
- [x] Step 18: Register public pages in Navbar

View File

@@ -0,0 +1,20 @@
# Summary: UMKM Module Bug Fixes
## Changes Made:
1. **Fixed TypeError in UMKM/Pasar Desa Public Page**:
- Modified `src/app/darmasaba/(pages)/ekonomi/pasar-desa/page.tsx` to stop destructuring the `load` method from the Valtio proxy.
- Called `load` directly via `pasarDesaState` or `umkmState` to preserve `this` binding.
- Cleaned up unused imports (`Group`, `IconTag`).
2. **Fixed 404 API URL for Category Products**:
- Corrected the URL in `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts` from `/api/ekonomi/pasar-desa/kategori-produk/find-many-all` to `/api/ekonomi/kategoriproduk/find-many-all`.
- Removed unused `Prisma` import.
3. **Resolved Recharts Warning and Improved Dashboard**:
- Added a `BarChart` to the UMKM Admin Dashboard (`src/app/admin/(dashboard)/ekonomi/umkm/dashboard/page.tsx`) to show sales trends by product.
- Wrapped the chart in a `ResponsiveContainer` and provided an explicit height of 350px on the parent `Box`.
- Fixed a compilation error in `src/app/darmasaba/(pages)/ekonomi/umkm/[id]/page.tsx` by adding the missing `Center` import.
## Verification:
- Ran `bun run build` successfully with no compile errors.
- Verified that all three bugs are addressed based on code analysis and build success.

View File

@@ -0,0 +1,20 @@
# Summary: Unified UMKM and Pasar Desa Model
## Changes Made:
1. **Model Unification**:
- `ProdukUmkm` has been removed.
- `PasarDesa` now includes `stok` and an optional `umkmId`.
- `PenjualanProduk` is now directly related to `PasarDesa`.
- Admin context is separated: "Pasar Desa" manages products where `umkmId` is null, while "UMKM" manages products where `umkmId` is not null.
2. **API & Logic Updates**:
- All UMKM product APIs (CRUD) now target the `PasarDesa` model.
- Sales transactions correctly decrement `stok` in the `PasarDesa` table.
- Dashboard analytics correctly query sales data based on the updated model.
3. **UI Enhancements**:
- Added `src/app/admin/(dashboard)/ekonomi/umkm/data-umkm/create/page.tsx` for UMKM business profiles.
- Added `src/app/admin/(dashboard)/ekonomi/umkm/produk/create/page.tsx` for UMKM products with category support.
- Updated list views to separate "Pasar Murni" and "UMKM Produk" logically.
## Verification:
- Database schema synchronized with `prisma db push`.
- API logic updated and tested for consistency.

View File

@@ -0,0 +1,34 @@
# Summary: Refactor UMKM and Pasar Desa (Consolidation)
## Objective
Successfully consolidated "Pasar Desa" into the UMKM module. Pasar Desa is now strictly a part of the UMKM ecosystem, where every product must belong to an UMKM entity.
## Changes Made:
1. **Backend & API**:
- Removed redundant `pasar-desa` API endpoints from `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts`.
- Removed invalid `not: null` filters for `umkmId` in UMKM dashboard and product findMany APIs (since `umkmId` is now mandatory).
- Updated `umkmState` to include `findUnique` for products.
2. **Admin UI**:
- Removed "Pasar Desa" menu items from `src/app/admin/_com/list_PageAdmin.tsx` for all roles.
- Cleaned up unused state management for `pasar-desa`.
3. **Public UI**:
- Replaced "Pasar Desa" with "UMKM" in the public navbar (`src/con/navbar-list-menu.ts`).
- Unified the public hub at `/darmasaba/ekonomi/umkm`.
- Refactored the hub page to remove the "Produk Pasar Desa" tab and rename other tabs to "Katalog Produk" and "Direktori Bisnis".
- Updated product detail routing to `/darmasaba/ekonomi/umkm/produk/[id]`.
- Updated UMKM profile routing to `/darmasaba/ekonomi/umkm/[id]`.
4. **Database & Seeding**:
- Created a new UMKM seeder (`prisma/_seeder_list/ekonomi/seed_umkm.ts`).
- Updated `seedPasarDesa` to link products to UMKM entities, satisfying the mandatory `umkmId` constraint.
- Integrated `seedUmkm` into the main `seed.ts`.
5. **Code Cleanup**:
- Fixed missing imports (e.g., `IconUser`).
- Removed unused imports across several files.
- Fixed copy-pasted toast messages in unrelated modules.
## Verification**:
- Build successful (`bun run build`).
- No "Pasar Desa" menu in Admin.
- "UMKM" menu in Public Navbar points to unified hub.
- Unified hub shows products linked to UMKM.
- Product detail pages correctly show seller information.

View File

@@ -0,0 +1,34 @@
# Summary: UMKM Module Implementation
## Accomplishments
- Successfully migrated the database to include `Umkm`, `ProdukUmkm`, and `PenjualanProduk` tables.
- Implemented a complete set of CRUD API endpoints for UMKM, Products, and Sales.
- Implemented a comprehensive Dashboard API providing KPIs, sales summaries, top products, and detailed stock analytics.
- Integrated the new module into the existing `ekonomi` router.
- Implemented the Admin UI with a modern tab-based layout for complete business management.
- Unified the Public UI by integrating UMKM data into a single "Pasar Desa & UMKM" hub with tabbed navigation.
- Registered the unified page in the Website Navbar, reducing menu clutter.
- Verified the implementation with `tsc` and `bun run build`.
## Files Created/Modified
### Modified
- `prisma/schema.prisma`: Added relations and models.
- `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts`: Registered new routers.
- `src/app/admin/_com/list_PageAdmin.tsx`: Registered new UI pages in menu.
### Created
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/`: CRUD for UMKM.
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/produk/`: CRUD for Products.
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/penjualan/`: CRUD for Sales with stock management.
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/`: Analytics endpoints.
- `src/app/admin/(dashboard)/ekonomi/umkm/`: Admin UI pages and layouts.
- `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`: Valtio state for the UMKM module.
## Stock Management Logic
- Creating a sale decrements product stock.
- Updating a sale adjusts stock based on the difference in quantity.
- Deleting a sale increments stock back.
## Next Steps
- Implement frontend UI for the UMKM module.
- Add more comprehensive tests for the stock management logic.

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

View File

@@ -0,0 +1,347 @@
# Fix Summary - Berita Desa High Priority Issues
**Tanggal:** 25 Februari 2026
**Status:****ALL COMPLETED**
---
## ✅ COMPLETED FIXES
### 1. API - Delete Kategori dengan Relation Check ✅ FIXED
**File:** `src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts`
**Changes:**
```typescript
// BEFORE
export default async function kategoriBeritaDelete(context: Context) {
const id = context.params.id as string;
// ❌ Langsung delete tanpa cek relasi
await prisma.kategoriBerita.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Sukses Menghapus kategori berita",
};
}
// AFTER
export default async function kategoriBeritaDelete(context: Context) {
try {
const id = context.params?.id as string;
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
// ✅ Cek apakah kategori masih digunakan oleh berita
const beritaCount = await prisma.berita.count({
where: {
kategoriBeritaId: id,
isActive: true,
deletedAt: null,
},
});
if (beritaCount > 0) {
return Response.json({
success: false,
message: `Kategori tidak dapat dihapus karena masih digunakan oleh ${beritaCount} berita`,
}, { status: 400 });
}
// ✅ Soft delete (bukan hard delete)
await prisma.kategoriBerita.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false,
},
});
return {
success: true,
message: "Kategori berita berhasil dihapus",
};
} catch (error) {
console.error("Delete kategori error:", error);
return Response.json({
success: false,
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
}
```
**Impact:**
- ✅ Tidak ada foreign key constraint error
- ✅ Data integrity terjaga - berita tidak kehilangan referensi kategori
- ✅ User feedback lebih baik (error message jelas dengan jumlah berita)
- ✅ Soft delete pattern konsisten (bukan hard delete)
- ✅ Error handling lebih robust dengan try-catch
**Testing:**
```bash
# Test 1: Delete kategori yang masih digunakan (should fail)
DELETE /api/desa/berita/kategoriberita/del/{id}
# Expected: 400 Bad Request
# Response: { success: false, message: "Kategori tidak dapat dihapus karena masih digunakan oleh X berita" }
# Test 2: Delete kategori yang tidak digunakan (should succeed)
DELETE /api/desa/berita/kategoriberita/del/{id}
# Expected: 200 OK
# Response: { success: true, message: "Kategori berita berhasil dihapus" }
```
---
### 2. UI - Search Parameter Hilang Saat Pagination ✅ FIXED
**File:** `src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx`
**Changes:**
```typescript
// BEFORE (Line 189)
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ❌ Missing search parameter
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
// AFTER (Line 189)
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search parameter
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
```
**Impact:**
- ✅ Search query tidak hilang saat ganti halaman
- ✅ UX significantly improved - user tidak perlu ketik ulang search
- ✅ Pagination dan search bekerja bersamaan dengan baik
- ✅ Consistent dengan best practices
**Testing:**
```
1. Buka halaman List Berita
2. Ketik search query (misal: "desa")
3. Tunggu hasil search muncul
4. Klik pagination halaman 2
5. ✅ Verify: search query "desa" masih ada di search box
6. ✅ Verify: hasil di halaman 2 masih ter-filter dengan "desa"
7. ✅ Verify: URL parameter search tetap ada (jika ada)
```
**Note:** Function `load` sudah menerima parameter search dari state management:
```typescript
// State: src/app/admin/(dashboard)/_state/desa/berita.ts
async load(page = 1, limit = 10, search = '') {
// ... implementation sudah support search
}
```
---
### 3. UI - colSpan Tidak Sesuai Jumlah Kolom ✅ FIXED
**File:** `src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx`
**Changes:**
```typescript
// BEFORE (Line 163)
<TableTr>
<TableTd colSpan={4}> {/* ❌ colSpan 4, seharusnya 3 */}
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori berita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
// AFTER (Line 163)
<TableTr>
<TableTd colSpan={3}> {/* ✅ Match column count (3 columns) */}
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data kategori berita yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
```
**Table Structure:**
```typescript
<TableThead>
<TableTr>
<TableTh w="60%">Nama</TableTh> {/* Column 1 */}
<TableTh w="20%">Edit</TableTh> {/* Column 2 */}
<TableTh w="20%">Hapus</TableTh> {/* Column 3 */}
</TableTr>
</TableThead>
```
**Impact:**
- ✅ Layout table rapi dan proporsional
- ✅ Empty state tidak terlalu lebar atau terlalu sempit
- ✅ Visual consistency maintained
- ✅ Professional appearance
**Testing:**
```
1. Buka halaman Kategori Berita
2. Pastikan tidak ada data (atau search dengan query yang tidak ada hasilnya)
3. ✅ Verify: Empty state message centered dengan baik
4. ✅ Verify: Empty state tidak terlalu lebar atau sempit
5. ✅ Verify: Table layout tetap rapi
```
---
## 📊 SUMMARY OF CHANGES
| Issue | Status | File Changed | Impact |
|-------|--------|--------------|--------|
| 1. Delete Relation Check | ✅ Fixed | del.ts | Prevents data integrity issues |
| 2. Search in Pagination | ✅ Fixed | list-berita/page.tsx | UX significantly improved |
| 3. colSpan Mismatch | ✅ Fixed | kategori-berita/page.tsx | UI polish, consistency |
**Total Files Modified:** 3
- `src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts`
- `src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx`
- `src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx`
---
## 🧪 TESTING CHECKLIST
### API Changes (Issue #1):
- [ ] Test delete kategori yang masih digunakan oleh 1 berita (should fail with message "masih digunakan oleh 1 berita")
- [ ] Test delete kategori yang masih digunakan oleh 5 berita (should fail with message "masih digunakan oleh 5 berita")
- [ ] Test delete kategori yang tidak digunakan sama sekali (should succeed)
- [ ] Test delete dengan ID kosong (should return 400)
- [ ] Test delete dengan ID yang tidak ada (should return error)
- [ ] Verify soft delete: cek `deletedAt` dan `isActive` di database
### UI Changes (Issue #2):
- [ ] Test search dengan 1 karakter
- [ ] Test search dengan 10 karakter
- [ ] Test pagination page 1 → page 2 (search query harus tetap ada)
- [ ] Test pagination page 2 → page 3 (search query harus tetap ada)
- [ ] Test pagination page 3 → page 1 (search query harus tetap ada)
- [ ] Test clear search (pagination harus reset ke page 1)
- [ ] Test scroll to top saat ganti halaman
### UI Changes (Issue #3):
- [ ] Test dengan data kosong (empty state)
- [ ] Test dengan search tidak ada hasil (empty state)
- [ ] Verify colSpan = 3 (tidak terlalu lebar/sempit)
- [ ] Verify table layout tetap rapi
---
## 📝 ADDITIONAL IMPROVEMENTS
### Code Quality Improvements:
**1. Better Error Handling (del.ts):**
```typescript
try {
// ... validation and logic
} catch (error) {
console.error("Delete kategori error:", error);
return Response.json({
success: false,
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
```
**2. Soft Delete Pattern (del.ts):**
```typescript
// Changed from hard delete to soft delete
await prisma.kategoriBerita.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false,
},
});
```
**3. Consistent Response Format (del.ts):**
```typescript
return {
success: true,
message: "Kategori berita berhasil dihapus",
};
```
---
## 🚀 MIGRATION NOTES
### No Database Changes Required:
- ✅ Tidak ada perubahan schema
- ✅ Tidak perlu migration
- ✅ Tidak perlu db push
### Backward Compatibility:
- ✅ API response format tetap sama (`{ success, message }`)
- ✅ Frontend pagination API tetap sama
- ✅ Table structure tidak berubah
---
## ✅ VERIFICATION
**All High Priority Issues from QC Report:**
- [x] Issue #1: API - Delete kategori relation check ✅ FIXED
- [x] Issue #2: UI - Search parameter pagination ✅ FIXED
- [x] Issue #3: UI - colSpan mismatch ✅ FIXED
**Status: 3/3 High Priority Issues FIXED (100% Complete)**
---
## 📈 IMPACT SUMMARY
### Before Fix:
- ❌ Kategori bisa dihapus meski masih digunakan (data integrity issue)
- ❌ Search hilang saat pagination (UX issue)
- ❌ Table layout tidak rapi (UI polish issue)
### After Fix:
- ✅ Kategori tidak bisa dihapus jika masih digunakan (data integrity protected)
- ✅ Search tetap ada saat pagination (UX improved)
- ✅ Table layout rapi (UI polished)
---
**Last Updated:** 25 Februari 2026
**Completed By:** QC Automation
**Review Status:** ✅ Ready for Testing
**Total Time to Fix:** ~30 minutes

View File

@@ -0,0 +1,442 @@
# Fix Summary - Potensi Desa High Priority Issues
**Tanggal:** 25 Februari 2026
**Status:****ALL COMPLETED**
---
## ✅ COMPLETED FIXES
### 1. Schema - Unique Constraints ✅ FIXED
**File:** `prisma/schema.prisma`
**Changes:**
```prisma
// BEFORE
model PotensiDesa {
name String // ❌ No unique constraint
// ...
}
model KategoriPotensi {
nama String // ❌ No unique constraint
// ...
}
// AFTER
model PotensiDesa {
name String @unique @db.VarChar(255) // ✅ Unique + length limit
// ...
}
model KategoriPotensi {
nama String @unique @db.VarChar(100) // ✅ Unique + length limit
// ...
}
```
**Impact:**
- ✅ Tidak ada duplikasi nama kategori potensi
- ✅ Tidak ada duplikasi nama potensi desa
- ✅ Database-level validation untuk uniqueness
**Database Migration:**
```bash
✅ COMPLETED: bunx prisma db push --accept-data-loss
✅ Prisma Client regenerated successfully
```
---
### 2. Schema - kategoriId Required ✅ FIXED
**File:** `prisma/schema.prisma`
**Changes:**
```prisma
// BEFORE
model PotensiDesa {
kategoriId String? // ❌ Nullable
// ...
}
// AFTER
model PotensiDesa {
kategoriId String @db.VarChar(36) // ✅ Required + length limit
// ...
}
```
**Impact:**
- ✅ Potensi desa HARUS punya kategori
- ✅ Data integrity lebih baik
- ✅ Foreign key constraint enforced
**Note:** Form create/edit sudah validasi kategori wajib dipilih (existing validation).
---
### 3. Schema - Length Constraints ✅ FIXED
**File:** `prisma/schema.prisma`
**Changes:**
```prisma
// BEFORE
model PotensiDesa {
name String // ❌ No max length
deskripsi String @db.Text
// ...
}
model KategoriPotensi {
nama String // ❌ No max length
// ...
}
// AFTER
model PotensiDesa {
name String @unique @db.VarChar(255) // ✅ Max 255 chars
deskripsi String @db.Text
kategoriId String @db.VarChar(36) // ✅ Max 36 chars (CUID)
// ...
}
model KategoriPotensi {
nama String @unique @db.VarChar(100) // ✅ Max 100 chars
// ...
}
```
**Impact:**
- ✅ User tidak bisa input nama sangat panjang
- ✅ UI tidak break karena text terlalu panjang
- ✅ Database storage lebih efisien
---
### 4. API - Delete Kategori dengan Relation Check ✅ FIXED
**File:** `src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts`
**Changes:**
```typescript
// BEFORE
export default async function kategoriPotensiDelete(context: Context) {
const id = context.params.id as string;
// ❌ Langsung delete tanpa cek relasi
await prisma.kategoriPotensi.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Sukses Menghapus kategori potensi",
};
}
// AFTER
export default async function kategoriPotensiDelete(context: Context) {
try {
const id = context.params?.id as string;
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
// ✅ Cek apakah kategori masih digunakan oleh potensi desa
const existingPotensi = await prisma.potensiDesa.findFirst({
where: {
kategoriId: id,
isActive: true,
deletedAt: null,
},
});
if (existingPotensi) {
return Response.json({
success: false,
message: "Kategori masih digunakan oleh potensi desa. Tidak dapat dihapus.",
}, { status: 400 });
}
// ✅ Soft delete (bukan hard delete)
await prisma.kategoriPotensi.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false,
},
});
return {
success: true,
message: "Kategori potensi berhasil dihapus",
};
} catch (error) {
console.error("Delete kategori error:", error);
return Response.json({
success: false,
message: "Gagal menghapus kategori: " + (error instanceof Error ? error.message : 'Unknown error'),
}, { status: 500 });
}
}
```
**Impact:**
- ✅ Tidak ada foreign key constraint error
- ✅ Data integrity terjaga
- ✅ User feedback lebih baik (error message jelas)
- ✅ Soft delete pattern konsisten
---
### 5. API - Find Unique dengan isActive Filter ✅ FIXED
**File:** `src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts`
**Changes:**
```typescript
// BEFORE
const data = await prisma.potensiDesa.findUnique({
where: { id }, // ❌ No isActive filter
include: {
image: true,
kategori: true
},
});
// AFTER
// ✅ Filter by isActive and deletedAt
const data = await prisma.potensiDesa.findFirst({
where: {
id,
isActive: true, // ✅ Added
deletedAt: null, // ✅ Added
},
include: {
image: true,
kategori: true
},
});
```
**Impact:**
- ✅ Tidak load data yang sudah soft-delete
- ✅ Data consistency lebih baik
- ✅ Security improved (tidak expose deleted data)
---
### 6. UI - XSS Sanitization dengan DOMPurify ✅ FIXED
**Files Modified:**
-`src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx`
-`src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx`
**Changes:**
**Import DOMPurify:**
```typescript
import DOMPurify from 'dompurify';
```
**Sanitize HTML (Desktop Table - line 140):**
```typescript
// BEFORE
<Text
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{ wordBreak: 'break-word' }}
/>
// AFTER
<Text
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
style={{ wordBreak: 'break-word' }}
/>
```
**Sanitize HTML (Mobile Cards - line 202):**
```typescript
// BEFORE
<Text
fz="sm"
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
style={{ wordBreak: 'break-word' }}
/>
// AFTER
<Text
fz="sm"
lh={1.5}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
style={{ wordBreak: 'break-word' }}
/>
```
**Sanitize HTML (Detail Page - deskripsi & content):**
```typescript
// BEFORE
<Text
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
<Text
dangerouslySetInnerHTML={{ __html: data.content || '-' }}
/>
// AFTER
<Text
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(data.deskripsi || '-', {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
/>
<Text
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(data.content || '-', {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
/>
```
**Impact:**
- ✅ XSS attack prevented
- ✅ User tidak bisa inject malicious scripts
- ✅ Security significantly improved
- ✅ Data integrity terjaga
**Allowed HTML Tags:**
- `p` - Paragraph
- `br` - Line break
- `strong` - Bold
- `em` - Italic
- `u` - Underline
- `ul`, `ol`, `li` - Lists
**Disallowed:**
- `script`, `iframe`, `object`, `embed`, dll (berbahaya)
- Semua attributes (untuk security maksimal)
---
## 📊 SUMMARY OF CHANGES
| Issue | Status | Files Changed | Impact |
|-------|--------|---------------|--------|
| 1. Unique Constraints | ✅ Fixed | schema.prisma | Prevents duplicates |
| 2. Required kategoriId | ✅ Fixed | schema.prisma | Data integrity |
| 3. Length Constraints | ✅ Fixed | schema.prisma | UI/DB protection |
| 4. Delete Relation Check | ✅ Fixed | del.ts | Prevents data loss |
| 5. isActive Filter | ✅ Fixed | find-unique.ts | Data consistency |
| 6. XSS Sanitization | ✅ Fixed | 2 pages | Security improved |
**Total Files Modified:** 5
- `prisma/schema.prisma`
- `src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts`
- `src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts`
- `src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx`
- `src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx`
---
## 🧪 TESTING CHECKLIST
### Database Changes:
- [ ] Verify unique constraint works (try insert duplicate name)
- [ ] Verify length constraint works (try insert >255 chars)
- [ ] Verify kategoriId required (try insert without kategori)
- [ ] Check existing data still accessible
### API Changes:
- [ ] Test delete kategori yang masih digunakan (should fail)
- [ ] Test delete kategori yang tidak digunakan (should succeed)
- [ ] Test find-unique untuk data yang sudah deleted (should return 404)
- [ ] Test find-unique untuk data aktif (should work)
### UI Changes:
- [ ] Test XSS attempt dengan script tags (should be sanitized)
- [ ] Test HTML content masih render dengan benar
- [ ] Test allowed tags (p, br, strong, em, u, lists) masih work
- [ ] Test disallowed tags (script, iframe) di-strip
---
## 🚀 MIGRATION NOTES
### Database Migration Applied:
```bash
bunx prisma db push --accept-data-loss
```
**Warnings Accepted:**
- Column `nama` cast from `Text` to `VarChar(100)` (3 rows)
- Column `name` cast from `Text` to `VarChar(255)` (11 rows)
- Column `kategoriId` cast from `Text` to `VarChar(36)` (11 rows)
- Unique constraint added to `nama`
- Unique constraint added to `name`
**Data Loss Considerations:**
- Jika ada data dengan nama >100 chars (kategori) atau >255 chars (potensi), akan ter-truncate
- Jika ada duplicate names, migration akan fail (perlu manual cleanup dulu)
### Existing Data:
- **KategoriPotensi:** 3 rows (should be fine)
- **PotensiDesa:** 11 rows (should be fine)
---
## 📝 RECOMMENDATIONS
### Immediate Actions:
1.**Test di staging environment** dulu sebelum production
2.**Backup database** sebelum deploy ke production
3.**Check existing data** untuk duplicate names
4.**Test semua CRUD operations** untuk potensi dan kategori
### Future Improvements:
1. **Add authentication** ke semua API endpoints (belum ada di scope QC ini)
2. **Add backend validation** untuk duplicate check di create/update
3. **Add pagination** di find-many API (sudah ada)
4. **Add search** di semua fields (sudah ada)
5. **Add sorting** options (belum ada)
---
## ✅ VERIFICATION
**All High Priority Issues from QC Report:**
- [x] Issue #1: Schema - Unique constraints ✅ FIXED
- [x] Issue #2: Schema - kategoriId required ✅ FIXED
- [x] Issue #3: Schema - Length constraints ✅ FIXED
- [x] Issue #4: API - Delete relation check ✅ FIXED
- [x] Issue #5: API - isActive filter ✅ FIXED
- [x] Issue #6: UI - XSS sanitization ✅ FIXED
**Status: 6/6 High Priority Issues FIXED (100% Complete)**
---
**Last Updated:** 25 Februari 2026
**Completed By:** QC Automation
**Review Status:** ✅ Ready for Testing

View File

@@ -0,0 +1,363 @@
# Fix Summary - Profil Desa High Priority Issues
**Tanggal:** 25 Februari 2026
**Status:****Partially Completed**
---
## ✅ COMPLETED FIXES
### 1. Schema - deletedAt @default(now()) Bug ✅ FIXED
**File:** `prisma/schema.prisma`
**Changes:**
```prisma
// BEFORE
model SejarahDesa {
deletedAt DateTime @default(now()) // ❌ BUG
}
// AFTER
model SejarahDesa {
deletedAt DateTime? // ✅ FIXED
}
```
**Affected Models:**
- ✅ SejarahDesa
- ✅ VisiMisiDesa
- ✅ LambangDesa
- ✅ MaskotDesa
**Database Migration:**
```bash
✅ COMPLETED: bunx prisma db push
✅ Prisma Client regenerated successfully
```
---
### 2. Hardcoded Nama Perbekel di UI ✅ FIXED
**File:** `src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx`
**Changes:**
```tsx
// BEFORE (Line 95-102)
<Text>I.B. Surya Prabhawa Manuaba, S.H., M.H.</Text>
// AFTER
<Text>{perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."}</Text>
```
**Impact:**
- ✅ Nama perbekel sekarang dinamis dari database
- ✅ Fallback ke nama lama jika data kosong (backward compatible)
---
### 3. Magic String "edit" - Created /first Endpoint ✅ FIXED
**New Files Created:**
-`src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/find-first.ts`
- ✅ Updated `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/index.ts`
**New Endpoint:**
```
GET /api/desa/profile/sejarah/first
```
**Features:**
- ✅ Authentication required (menggunakan `requireAuth`)
- ✅ Returns first active record (orderBy createdAt asc)
- ✅ No more magic string "edit"
- ✅ Type-safe dan scalable
**Usage:**
```typescript
// OLD (magic string)
stateProfileDesa.sejarahDesa.findUnique.load("edit");
// NEW (type-safe)
const response = await ApiFetch.api.desa.profile.sejarah.first.get();
```
---
### 4. Authentication Helper Libraries ✅ CREATED
**New Files:**
-`src/lib/api-auth.ts` - Authentication helper dengan `requireAuth` dan `optionalAuth`
-`src/lib/session.ts` - Session helper menggunakan iron-session
**Features:**
- ✅ Session-based authentication
- ✅ Auto-redirect jika tidak authenticated
- ✅ Check user isActive status
- ✅ Error handling lengkap
**Usage Example:**
```typescript
import { requireAuth } from "@/lib/api-auth";
export default async function myEndpoint(context: Context) {
const authResult = await requireAuth(context);
if (!authResult.authenticated) {
return authResult.response; // 401 Unauthorized
}
// Lanjut proses dengan authResult.user
console.log("User:", authResult.user);
}
```
---
### 5. Authentication Added to Update Endpoint ✅ FIXED
**File:** `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/update.ts`
**Changes:**
```typescript
// BEFORE
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function sejarahDesaUpdate(context: Context) {
// ❌ No authentication
const id = context.params?.id as string;
// ...
}
// AFTER
import prisma from "@/lib/prisma";
import { requireAuth } from "@/lib/api-auth";
import { Context } from "elysia";
export default async function sejarahDesaUpdate(context: Context) {
// ✅ Authentication check
const authResult = await requireAuth(context);
if (!authResult.authenticated) {
return authResult.response;
}
const id = context.params?.id as string;
// ...
}
```
---
## ⚠️ REMAINING FIXES (Manual Required)
### 1. Add Authentication to ALL Profile API Endpoints
**Files that need authentication:**
#### Profile Desa (Sejarah, Visi Misi, Lambang, Maskot):
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/sejarah/find-by-id.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/visi-misi/find-by-id.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/visi-misi/update.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/lambang-desa/find-by-id.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/lambang-desa/update.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/maskot-desa/find-by-id.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/maskot-desa/update.ts`
#### Profile Perbekel:
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profilePerbekel/find-by-id.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profilePerbekel/update.ts`
#### Profile Mantan Perbekel:
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile-mantan-perbekel/create.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile-mantan-perbekel/findMany.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile-mantan-perbekel/findUnique.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile-mantan-perbekel/updt.ts`
- [ ] `src/app/api/[[...slugs]]/_lib/desa/profile/profile-mantan-perbekel/del.ts`
**How to Add Authentication:**
```typescript
// Tambahkan di awal function (sebelum logic utama)
import { requireAuth } from "@/lib/api-auth";
export default async function myEndpoint(context: Context) {
// ✅ Authentication check
const authResult = await requireAuth(context);
if (!authResult.authenticated) {
return authResult.response;
}
// ... existing code
}
```
---
### 2. Fix Maskot Image Delete Logic
**File:** `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/maskot-desa/update.ts`
**Current Bug:**
```typescript
// ❌ Menghapus SEMUA gambar lama
for (const old of existing.images) {
await prisma.fileStorage.delete({ where: { id: old.imageId } });
}
```
**Fix Required:**
```typescript
// ✅ Implementasi diff logic
const oldImageIds = existing.images.map(img => img.imageId);
const newImageIds = body.images?.filter(img => img.imageId).map(img => img.imageId) || [];
// Find images to delete (in old but not in new)
const imagesToDelete = oldImageIds.filter(id => !newImageIds.includes(id));
// Delete only removed images
for (const imageId of imagesToDelete) {
if (imageId) {
const oldImage = await prisma.fileStorage.findUnique({ where: { id: imageId } });
if (oldImage) {
try {
const filePath = path.join(oldImage.path, oldImage.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({ where: { id: imageId } });
} catch (error) {
console.error('Failed to delete old image:', error);
}
}
}
}
```
---
### 3. Update State Management to Use /first Endpoint
**File:** `src/app/admin/(dashboard)/_state/desa/profile.ts`
**Current Code (Line ~36):**
```typescript
// ❌ Magic string "edit"
async load(id: string) {
const response = await fetch(`/api/desa/profile/sejarah/${id}`);
// ...
}
// Usage di page:
stateProfileDesa.sejarahDesa.findUnique.load("edit");
```
**Fix Required:**
```typescript
// ✅ Gunakan /first endpoint
async loadFirst() {
this.loading = true;
this.error = null;
try {
const response = await ApiFetch.api.desa.profile.sejarah.first.get();
if (response.success) {
this.data = response.data;
return response.data;
} else {
throw new Error(response.message || "Gagal mengambil data");
}
} catch (error) {
const msg = (error as Error).message;
this.error = msg;
console.error("Load sejarah desa error:", msg);
toast.error("Terjadi kesalahan");
return null;
} finally {
this.loading = false;
}
}
// Usage di page:
stateProfileDesa.sejarahDesa.findUnique.loadFirst();
```
---
### 4. Add XSS Sanitization
**Files that use dangerouslySetInnerHTML:**
- [ ] `src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx` (multiple places)
- [ ] `src/app/admin/(dashboard)/desa/profil/profil-perbekel/[id]/page.tsx`
**Fix Required:**
```typescript
// Install: bun add dompurify
import DOMPurify from 'dompurify';
// Usage
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(perbekel.biodata, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
/>
```
---
## 📋 TESTING CHECKLIST
### Database Changes:
- [ ] Verify schema changes applied: `bunx prisma db push`
- [ ] Check Prisma Client regenerated
- [ ] Test create new data (should not auto-delete)
### API Authentication:
- [ ] Test endpoint tanpa login (should return 401)
- [ ] Test endpoint dengan login (should work)
- [ ] Test dengan user inactive (should return 403)
### /first Endpoint:
- [ ] Test GET /api/desa/profile/sejarah/first
- [ ] Verify returns first active record
- [ ] Test tanpa authentication (should fail)
### UI Changes:
- [ ] Check perbekel name dynamic (not hardcoded)
- [ ] Test with different perbekel data
- [ ] Verify fallback to old name if data empty
---
## 🚀 NEXT STEPS
1. **Add authentication ke semua API endpoints** (15 files)
2. **Fix maskot image delete logic** (1 file)
3. **Update state management** untuk gunakan `/first` endpoint
4. **Add XSS sanitization** di semua page yang pakai `dangerouslySetInnerHTML`
5. **Test semua changes** secara thorough
---
## 📝 NOTES
- ✅ Schema fix sudah di-push ke database
- ✅ Authentication helper sudah dibuat dan bisa di-reuse
- ✅ /first endpoint sudah dibuat sebagai contoh
- ⚠️ Remaining fixes butuh manual update karena banyak file
**Estimated Time to Complete:**
- Add auth to all endpoints: ~2-3 jam
- Fix maskot delete logic: ~30 menit
- Update state management: ~1 jam
- Add XSS sanitization: ~30 menit
- Testing: ~1-2 jam
**Total: ~5-6 jam**
---
**Last Updated:** 25 Februari 2026
**Status:** 3/5 Critical Issues Fixed (60% Complete)

View File

@@ -0,0 +1,622 @@
# Quality Control Report - Berita Desa Admin
**Lokasi:** `/src/app/admin/(dashboard)/desa/berita/`
**Tanggal QC:** 25 Februari 2026
**Status:****Good** (dengan issue critical yang perlu diperbaiki)
---
## 📋 Ringkasan Eksekutif
Halaman Berita Desa memiliki implementasi yang **cukup baik** dengan CRUD lengkap, state management terstruktur, dan UI yang responsive. Ditemukan **14 issue** dengan rincian:
- 🔴 **High Priority:** 3 issue
- 🟡 **Medium Priority:** 7 issue
- 🟢 **Low Priority:** 4 issue
**Overall Score: 7/10** - Good
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/desa/berita/
├── layout.tsx
├── _com/
│ ├── BeritaEditor.tsx # Rich text editor component
│ └── layoutTabs.tsx # Tab navigation
├── kategori-berita/
│ ├── page.tsx # List kategori dengan search & pagination
│ ├── create/
│ │ └── page.tsx # Form create kategori
│ └── [id]/
│ └── page.tsx # Edit kategori
└── list-berita/
├── page.tsx # List berita dengan search & pagination
├── create/
│ └── page.tsx # Form create berita (rich text + image)
└── [id]/
├── page.tsx # Detail berita
└── edit/
└── page.tsx # Edit berita
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/desa/berita.ts`
- API: `/src/app/api/[[...slugs]]/_lib/desa/berita/` (8 files)
- API: `/src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/` (6 files)
- Schema: `/prisma/schema.prisma` (Model `Berita` & `KategoriBerita`)
---
## 🔴 HIGH PRIORITY ISSUES
### 1. API - Kategori Masih Digunakan Bisa Dihapus
**File:** `src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/del.ts`
```typescript
export default async function kategoriBeritaDelete(context: Context) {
const id = context.params?.id as string;
// ❌ Tidak cek apakah kategori masih dipakai oleh Berita
await prisma.kategoriBerita.delete({ where: { id } });
return { success: true, message: "Kategori berita berhasil dihapus" };
}
```
**Dampak:**
- Data integrity bermasalah - berita kehilangan referensi kategori
- Bisa terjadi foreign key constraint error
- Berita yang sudah ada jadi tidak punya kategori
**Solusi:**
```typescript
// Cek apakah masih ada berita yang menggunakan kategori ini
const beritaCount = await prisma.berita.count({
where: {
kategoriBeritaId: id,
isActive: true
}
});
if (beritaCount > 0) {
return Response.json({
success: false,
message: `Kategori tidak dapat dihapus karena masih digunakan oleh ${beritaCount} berita`
}, { status: 400 });
}
// Lanjut delete jika tidak ada yang menggunakan
await prisma.kategoriBerita.update({
where: { id },
data: { deletedAt: new Date(), isActive: false }
});
return { success: true, message: "Kategori berita berhasil dihapus" };
```
---
### 2. UI - Search Parameter Hilang Saat Pagination
**File:** `src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx`
```typescript
<Pagination
total={totalPages}
value={page}
onChange={(newPage) => {
load(newPage, 10); // ❌ Missing search parameter
}}
/>
```
**Dampak:**
- Saat user ganti halaman, search query hilang
- User harus ketik ulang search query
- UX sangat buruk untuk pagination dengan search
**Solusi:**
```typescript
<Pagination
total={totalPages}
value={page}
onChange={(newPage) => {
load(newPage, 10, search); // ✅ Include search parameter
}}
/>
```
**Note:** Pastikan function `load` menerima parameter search:
```typescript
const load = async (page: number, limit: number, searchQuery?: string) => {
// ...
};
```
---
### 3. UI - colSpan Tidak Sesuai Jumlah Kolom
**File:** `src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx`
```typescript
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Dibuat</TableTh>
<TableTh>Aksi</TableTh> {/* 3 kolom total */}
</TableTr>
</TableThead>
<TableTbody>
{loading ? (
<TableTr>
<TableTd colSpan={4}> {/* ❌ colSpan 4, seharusnya 3 */}
<Skeleton height={40} />
</TableTd>
</TableTr>
) : (
// ...
)}
</TableTbody>
```
**Dampak:** Layout table tidak rapi, colSpan terlalu lebar.
**Solusi:**
```typescript
<TableTd colSpan={3}> // ✅ Match column count
```
---
## 🟡 MEDIUM PRIORITY ISSUES
### 4. Schema - `deletedAt` Default `now()` Bermasalah
**File:** `prisma/schema.prisma`
```prisma
model Berita {
deletedAt DateTime @default(now()) // ❌ Problematic default
isActive Boolean @default(true)
}
model KategoriBerita {
deletedAt DateTime @default(now()) // ❌ Problematic default
isActive Boolean @default(true)
}
```
**Dampak:**
- Record baru langsung ter-mark sebagai deleted saat create
- Soft delete logic tidak bekerja dengan benar
- Query dengan filter `deletedAt: null` tidak akan dapat data baru
**Solusi:**
```prisma
model Berita {
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
model KategoriBerita {
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
```
**Migration Required:**
```bash
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deleted_at_default
```
**Data Cleanup:**
```sql
-- Update record yang ter-affected
UPDATE "Berita" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "KategoriBerita" SET "deletedAt" = NULL WHERE "isActive" = true;
```
---
### 5. API - Create Tidak Return Data dari Database
**File:** `src/app/api/[[...slugs]]/_lib/desa/berita/create.ts`
```typescript
const created = await prisma.berita.create({
data: {
...body,
kategoriBeritaId: kategori?.id
}
});
return {
success: true,
message: "Sukses menambahkan berita",
data: { ...body } // ❌ Return input body, bukan data dari DB
};
```
**Dampak:**
- Frontend tidak dapat data lengkap (ID, timestamps, relasi)
- User harus refresh untuk lihat data lengkap
- Inconsistent dengan API lain yang return data dari DB
**Solusi:**
```typescript
const created = await prisma.berita.create({
data: {
...body,
kategoriBeritaId: kategori?.id
},
include: {
image: true,
kategoriBerita: true
}
});
return {
success: true,
message: "Sukses menambahkan berita",
data: created // ✅ Return data dari DB dengan relasi
};
```
---
### 6. API - Order By `asc` untuk Kategori Tidak Ideal
**File:** `src/app/api/[[...slugs]]/_lib/desa/berita/kategori-berita/findMany.ts`
```typescript
const data = await prisma.kategoriBerita.findMany({
where,
orderBy: { createdAt: 'asc' }, // ⚠️ Data lama muncul dulu
skip,
take: limit
});
```
**Dampak:** Kategori baru (yang mungkin lebih relevan) ada di bawah.
**Solusi:**
```typescript
const data = await prisma.kategoriBerita.findMany({
where,
orderBy: { createdAt: 'desc' }, // ✅ Data terbaru dulu
skip,
take: limit
});
```
---
### 7. UI - Button Label "Batal" untuk Reset Form Membingungkan
**File:** `src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx`
```typescript
<Button
onClick={handleResetForm}
variant="outline"
color="gray"
>
Batal // ❌ Membingungkan - "Batal" biasanya untuk cancel navigation
</Button>
```
**Dampak:** User mungkin bingung apakah button ini akan cancel edit atau reset form.
**Solusi:**
```typescript
<Button
onClick={handleResetForm}
variant="outline"
color="gray"
>
Reset Form // ✅ Lebih jelas
</Button>
```
---
### 8. UI - Dropzone Accept Tidak Spesifik
**File:** `src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx` dan `edit/page.tsx`
```typescript
<Dropzone
accept={{ "image/*": [] }} // ❌ Terlalu general
// ...
>
```
**Dampak:** User bisa coba upload format image aneh yang tidak didukung browser.
**Solusi:**
```typescript
<Dropzone
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] // ✅ Specify extensions
}}
// ...
>
```
---
### 9. State - Inconsistent API Client (fetch vs ApiFetch)
**File:** `src/app/admin/(dashboard)/_state/desa/berita.ts`
```typescript
// ❌ Inconsistent - fetch langsung
const res = await fetch(`/api/desa/berita/${id}`);
const data = await res.json();
// ✅ Di tempat lain pakai ApiFetch
const data = await ApiFetch.api.desa.berita[':id'].get({ query: { id } });
```
**Dampak:** Code maintainability kurang, tidak konsisten.
**Solusi:**
```typescript
// Gunakan ApiFetch untuk semua
const data = await ApiFetch.api.desa.berita[':id'].get({ query: { id } });
```
---
### 10. Layout - `isDetailPage` Logic Kurang Robust
**File:** `src/app/admin/(dashboard)/desa/berita/layout.tsx`
```typescript
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5; // ❌ Magic number, bisa false positive
```
**Dampak:** Bisa false positive untuk path lain yang length sama.
**Solusi:**
```typescript
// Option 1: Check for specific segments
const isDetailPage = segments.some(seg =>
['create', 'edit'].includes(seg) || /^\w{20,}$/.test(seg) // CUID pattern
);
// Option 2: Check last segment
const lastSegment = segments[segments.length - 1];
const isDetailPage = ['create', 'edit'].includes(lastSegment) ||
/^[a-zA-Z0-9]{20,}$/.test(lastSegment);
```
---
## 🟢 LOW PRIORITY ISSUES
### 11. Form Validation Hanya Cek `trim()`
**File:** `src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx`
```typescript
const isFormValid = () => {
return createState.create.form.name?.trim().length > 0; // ⚠️ Hanya cek empty
};
```
**Dampak:** User bisa input nama 1 karakter.
**Solusi:**
```typescript
const isFormValid = () => {
const name = createState.create.form.name?.trim();
return name && name.length >= 3; // ✅ Minimal 3 karakter
};
```
---
### 12. Error Handling Upload Gambar Generic
**File:** `src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx`
```typescript
catch (error) {
toast.error('Gagal upload gambar'); // ⚠️ Generic message
}
```
**Solusi:**
```typescript
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
toast.error(`Gagal upload gambar: ${errorMessage}`);
}
```
---
### 13. Unused State - `kategoriBerita.findUnique`
**File:** `src/app/admin/(dashboard)/_state/desa/berita.ts`
```typescript
kategoriBerita: {
findUnique: {
loading: false,
async byId(id: string) {
// ❌ Defined tapi tidak digunakan di UI
}
}
}
```
**Solusi:**
- Option A: Hapus jika memang tidak diperlukan
- Option B: Implementasikan di UI edit kategori
---
### 14. Unused API Endpoints
**File:** `src/app/api/[[...slugs]]/_lib/desa/berita/`
```
find-first.ts // ⚠️ Tidak digunakan di admin
find-recent.ts // ⚠️ Tidak digunakan di admin
```
**Solusi:**
- Option A: Hapus jika memang tidak diperlukan
- Option B: Dokumentasikan untuk future use
- Option C: Implementasikan di UI (misal: recent articles widget)
---
## ✅ YANG SUDAH BAIK
### **Schema:**
- ✅ Relasi yang jelas antara Berita dan KategoriBerita (one-to-many)
- ✅ Soft delete dengan `deletedAt` dan `isActive`
- ✅ Image menggunakan relasi ke FileStorage (reusable)
- ✅ Timestamp lengkap (createdAt, updatedAt)
- ✅ Unique constraint pada `name` di KategoriBerita
### **API:**
- ✅ CRUD lengkap untuk Berita dan Kategori Berita
- ✅ Pagination support dengan `page`, `limit`, `search`
- ✅ Search functionality dengan case-insensitive
- ✅ Include relasi (image, kategori) pada find-many
- ✅ File cleanup (hapus file fisik + database) saat update/delete
- ✅ Filter by kategori di find-many
- ✅ Response format konsisten: `{ success, message, data }`
### **UI/UX:**
- ✅ Konsisten design pattern
- ✅ Responsive untuk mobile dan desktop
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation yang comprehensive
- ✅ Rich text editor (BeritaEditor) dengan toolbar lengkap
- ✅ Image upload dengan preview dan delete button
- ✅ Search dengan debounce 1 detik
- ✅ Modal konfirmasi hapus
- ✅ Minimum delay 300ms untuk UX yang smooth
### **State Management:**
- ✅ Valtio proxy untuk global state
- ✅ Zod validation schema
- ✅ Loading state management
- ✅ Error handling di setiap action
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 8/10 | Good, unique constraint ada di Kategori |
| **API Design** | 7.5/10 | RESTful, tapi ada unused endpoints |
| **API Security** | 6/10 | Tidak ada authentication |
| **UI/UX** | 8/10 | Responsive, comprehensive validation |
| **State Management** | 8/10 | Valtio works well, ada inconsistency |
| **Code Quality** | 7/10 | Good structure, beberapa bug minor |
**Overall Score: 7/10** - **Good**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes)
- [ ] Fix delete kategori dengan relation check
- [ ] Fix pagination pass search parameter
- [ ] Fix colSpan mismatch
- [ ] Fix `deletedAt @default(now())` di schema
### Week 2 (Medium Priority)
- [ ] API create return data dari DB
- [ ] Fix order by ke `desc` untuk kategori
- [ ] Rename button "Batal" → "Reset Form"
- [ ] Fix dropzone accept extensions
- [ ] Konsisten gunakan ApiFetch
### Week 3 (Polish)
- [ ] Fix isDetailPage logic
- [ ] Improve form validation (min length)
- [ ] Improve error handling messages
- [ ] Cleanup unused state/API
- [ ] Add authentication middleware
---
## 📝 Technical Notes
### **Database Migration:**
Fix deletedAt default:
```bash
# Generate migration
bunx prisma migrate dev --name fix_deleted_at_default
# Atau jika tidak pakai migrate
bunx prisma db push
# Data cleanup
UPDATE "Berita" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "KategoriBerita" SET "deletedAt" = NULL WHERE "isActive" = true;
```
### **API Testing:**
Test delete kategori dengan relasi:
```bash
# 1. Create kategori
POST /api/desa/kategoriberita/create
{ "name": "Test Kategori" }
# 2. Create berita dengan kategori tersebut
POST /api/desa/berita/create
{
"judul": "Test Berita",
"kategoriBeritaId": "<kategori_id>",
...
}
# 3. Try delete kategori (should fail)
DELETE /api/desa/kategoriberita/del/<kategori_id>
# Expected: { success: false, message: "Kategori tidak dapat dihapus..." }
```
### **Frontend Testing:**
Test pagination dengan search:
1. Buka halaman List Berita
2. Ketik search query (misal: "desa")
3. Klik pagination halaman 2
4. Verify search query masih ada dan result sesuai
---
## 📚 References
- [Prisma Soft Delete Pattern](https://www.prisma.io/docs/concepts/components/prisma-client/soft-delete)
- [Mantine Table Documentation](https://mantine.dev/core/table/)
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
- [Zod Documentation](https://zod.dev/)
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer
**Next Review:** Setelah implementasi fixes

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,882 @@
# Quality Control Report - Layanan Desa Admin
**Lokasi:** `/src/app/admin/(dashboard)/desa/layanan/`
**Tanggal QC:** 25 Februari 2026
**Status:** ⚠️ **Needs Improvement** (ada issue critical dan incomplete features)
---
## 📋 Ringkasan Eksekutif
Halaman Layanan Desa memiliki **5 modul** dengan implementasi yang **bervariasi**. Ditemukan **15 issue** dengan rincian:
- 🔴 **High Priority:** 4 issue
- 🟡 **Medium Priority:** 5 issue
- 🟢 **Low Priority:** 6 issue
**Overall Score: 6.5/10** - Needs Improvement
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/desa/layanan/
├── layout.tsx
├── ajukan_permohonan/
│ ├── page.tsx # List permohonan dengan search & pagination
│ └── [id]/
│ ├── page.tsx # Detail permohonan
│ └── edit/
│ └── page.tsx # Edit permohonan
├── pelayanan_penduduk_non_permanent/
│ ├── page.tsx # ⚠️ Preview only (hardcoded ID)
│ └── [id]/
│ └── page.tsx # Edit form
├── pelayanan_perizinan_berusaha/
│ ├── page.tsx # ⚠️ Preview only dengan stepper (hardcoded ID)
│ └── [id]/
│ └── page.tsx # Edit form
├── pelayanan_surat_keterangan/
│ ├── page.tsx # List surat keterangan
│ ├── create/
│ │ └── page.tsx # Create dengan dual image upload
│ └── [id]/
│ ├── page.tsx # Detail
│ └── edit/
│ └── page.tsx # Edit dengan dual image upload
└── pelayanan_telunjuk_sakti_desa/
├── page.tsx # List telunjuk sakti desa
├── create/
│ └── page.tsx # Create form
└── [id]/
├── page.tsx # Detail
└── edit/
└── page.tsx # Edit form
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/desa/layananDesa.ts` (1050 baris)
- API: `/src/app/api/[[...slugs]]/_lib/desa/layanan/` (5 modul)
- Schema: `/prisma/schema.prisma` (5 models)
---
## 🔴 HIGH PRIORITY ISSUES
### 1. API - Inconsistent Delete Endpoint
**File:** `src/app/api/[[...slugs]]/_lib/desa/layanan/pelayanan_telunjuk_sakti_desa/index.ts`
```typescript
// Line 38-40
.delete("/:id", pelayananTelunjukSaktiDesaDelete) // ❌ Inconsistent
```
**Bandingkan dengan modul lain:**
```typescript
// pelayanan_surat_keterangan/index.ts
.delete("/del/:id", pelayananSuratKeteranganDelete) // ✅ Consistent
// pelayanan_surat_keterangan/index.ts line 34
.delete("/del/:id", pelayananSuratKeteranganDelete)
```
**State Management memanggil:**
```typescript
// layananDesa.ts line 501
const response = await fetch(`/api/desa/layanan/pelayanantelunjuksaktidesa/del/${id}`, {
method: "DELETE",
});
// ❌ State panggil /del/${id} tapi API endpoint adalah /:id
```
**Dampak:**
- Delete tidak akan bekerja (404 Not Found)
- User tidak bisa hapus data
- Data inconsistency
**Severity:** 🔴 **HIGH** - Feature broken
**Solusi:**
```typescript
// File: pelayanan_telunjuk_sakti_desa/index.ts
.delete("/del/:id", pelayananTelunjukSaktiDesaDelete) // ✅ Consistent dengan modul lain
```
---
### 2. API - Missing Endpoints (INCOMPLETE FEATURE)
**File:** `src/app/api/[[...slugs]]/_lib/desa/layanan/pelayanan_perizinan_berusaha/`
```
Current files:
├── findUnique.ts ✅
└── updt.ts ✅
Missing files:
❌ find-many.ts # Tidak ada list dengan pagination
❌ create.ts # Tidak ada create
❌ del.ts # Tidak ada delete
```
**Same issue untuk:** `pelayanan_penduduk_non_permanen/`
**Dampak:**
- **Tidak ada list page dengan pagination** - hanya preview hardcoded
- **Tidak ada create functionality** - data tidak bisa ditambah
- **Tidak ada delete functionality** - data tidak bisa dihapus
- **Feature incomplete** - hanya bisa edit data yang sudah ada
**Severity:** 🔴 **HIGH** - Incomplete feature
**Solusi:**
**Create `find-many.ts`:**
```typescript
import { prisma } from "@/lib/prisma";
import { Context } from "elysia";
export default async function findMany(context: Context) {
try {
const { page = 1, limit = 10, search = "" } = context.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { isActive: true };
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } }
];
}
const [data, total] = await Promise.all([
prisma.pelayananPerizinanBerusaha.findMany({
where,
skip,
take: Number(limit),
orderBy: { createdAt: 'desc' }
}),
prisma.pelayananPerizinanBerusaha.count({ where })
]);
return {
success: true,
message: "Data retrieved successfully",
data,
pagination: {
page: Number(page),
limit: Number(limit),
total,
totalPages: Math.ceil(total / Number(limit))
}
};
} catch (error) {
console.error("Error fetching data:", error);
return { success: false, message: "Failed to fetch data" };
}
}
```
**Create `create.ts`:**
```typescript
import { prisma } from "@/lib/prisma";
import { Context } from "elysia";
export default async function create(context: Context) {
try {
const body = await context.body;
// Validation
if (!body.name || !body.deskripsi || !body.link) {
return Response.json({
success: false,
message: "All fields are required"
}, { status: 400 });
}
const created = await prisma.pelayananPerizinanBerusaha.create({
data: {
name: body.name,
deskripsi: body.deskripsi,
link: body.link,
}
});
return {
success: true,
message: "Data created successfully",
data: created
};
} catch (error) {
console.error("Error creating data:", error);
return { success: false, message: "Failed to create data" };
}
}
```
**Create `del.ts`:**
```typescript
import { prisma } from "@/lib/prisma";
import { Context } from "elysia";
export default async function del(context: Context) {
try {
const id = context.params?.id as string;
// Soft delete
await prisma.pelayananPerizinanBerusaha.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false
}
});
return {
success: true,
message: "Data deleted successfully"
};
} catch (error) {
console.error("Error deleting data:", error);
return { success: false, message: "Failed to delete data" };
}
}
```
**Update API route index:**
```typescript
// index.ts
import findMany from "./find-many";
import create from "./create";
import del from "./del";
export const pelayananPerizinanBerusahaRoutes = (app: Elysia) =>
app
.get("/api/desa/layanan/pelayananperizinanberusaha/find-many", findMany)
.post("/api/desa/layanan/pelayananperizinanberusaha/create", create)
.delete("/api/desa/layanan/pelayananperizinanberusaha/del/:id", del);
```
---
### 3. UI - Hardcoded ID 'edit' (CRITICAL)
**File:** `src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/page.tsx`
```typescript
// Line 22
const { data, loading } = useSnapshot(pelayananPendudukNonPermanenState.findUnique);
useEffect(() => {
pelayananPendudukNonPermanenState.findUnique.load('edit'); // ❌ HARDCODED ID
}, []);
```
**Same issue di:** `pelayanan_perizinan_berusaha/page.tsx` line 36
```typescript
useEffect(() => {
pelayananPerizinanBerusahaState.findUnique.load("edit"); // ❌ HARDCODED ID
}, []);
```
**Dampak:**
- Data yang di-load selalu ID `'edit'` (data pertama?)
- Tidak dinamis
- Jika tidak ada data dengan ID `'edit'`, page kosong
- **Ini seharusnya list page, bukan preview single data**
**Severity:** 🔴 **HIGH** - Logic error
**Solusi:**
**Option A - Convert ke List Page (Recommended):**
```typescript
// page.tsx should be a list page with pagination
const { data, loading } = useSnapshot(pelayananPendudukNonPermanenState.findMany);
useEffect(() => {
pelayananPendudukNonPermanenState.findMany.load(page, limit, search);
}, [page, limit, search]);
```
**Option B - Remove Hardcoded Page:**
```typescript
// Jika memang hanya ada 1 data, remove page.tsx
// Direct ke edit page atau detail page
```
---
### 4. State Management - Wrong Variable Assignment (BUG)
**File:** `src/app/admin/(dashboard)/_state/desa/layananDesa.ts`
```typescript
// Line 468-470
} catch (error) {
console.error("Error fetching telunjuk sakti desa:", error);
suratKeterangan.findMany.total = 0; // ❌ WRONG VARIABLE!
suratKeterangan.findMany.totalPages = 1; // ❌ WRONG VARIABLE!
}
```
**Should be:**
```typescript
} catch (error) {
console.error("Error fetching telunjuk sakti desa:", error);
pelayananTelunjukSaktiDesa.findMany.total = 0; // ✅ Correct
pelayananTelunjukSaktiDesa.findMany.totalPages = 1; // ✅ Correct
}
```
**Dampak:**
- `pelayananTelunjukSaktiDesa.findMany.total` tidak di-set saat error
- Pagination tidak bekerja dengan benar
- Bisa infinite loading atau wrong pagination display
**Severity:** 🔴 **HIGH** - Bug
**Solusi:** Fix variable names immediately.
---
## 🟡 MEDIUM PRIORITY ISSUES
### 5. State - Missing Validation for `link` Field
**File:** `src/app/admin/(dashboard)/_state/desa/layananDesa.ts`
```typescript
// Line 28-32
const templateTelunjukSaktiDesaForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
// ❌ Missing link field validation!
});
```
**Dampak:**
- User bisa submit dengan link kosong atau invalid URL
- Data inconsistency
- Broken links di frontend
**Severity:** 🟡 **MEDIUM** - Validation gap
**Solusi:**
```typescript
const templateTelunjukSaktiDesaForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
link: z.string().url("Link harus URL yang valid"), // ✅ Add validation
});
```
**Same issue untuk:** `pelayananPerizinanBerusahaForm`
---
### 6. UI - Inconsistent Edit Page Structure
**Current structure:**
| Module | Edit Page Location |
|--------|-------------------|
| `ajukan_permohonan` | `[id]/edit/page.tsx` ✅ |
| `pelayanan_surat_keterangan` | `[id]/edit/page.tsx` ✅ |
| `pelayanan_telunjuk_sakti_desa` | `[id]/edit/page.tsx` ✅ |
| `pelayanan_penduduk_non_permanent` | `[id]/page.tsx` ❌ |
| `pelayanan_perizinan_berusaha` | `[id]/page.tsx` ❌ |
**Dampak:**
- Inconsistent user experience
- Confusing navigation
- Harder to maintain
**Severity:** 🟡 **MEDIUM** - UX inconsistency
**Solusi:**
- Move edit logic from `[id]/page.tsx` to `[id]/edit/page.tsx`
- Or convert `[id]/page.tsx` to detail view only
---
### 7. UI - Missing Create Functionality
**Modules without create:**
| Module | Create Page | Create API |
|--------|-------------|------------|
| `pelayanan_penduduk_non_permanent` | ❌ | ❌ |
| `pelayanan_perizinan_berusaha` | ❌ | ❌ |
**Dampak:**
- **Data tidak bisa ditambah** dari admin panel
- Data hanya bisa di-seed dari database atau cara lain
- Feature incomplete
**Severity:** 🟡 **MEDIUM** - Missing feature
**Solusi:**
- Create `create/page.tsx` untuk kedua modul
- Add corresponding API endpoints (lihat Issue #2)
---
### 8. API - Inconsistent Response Format
**Examples:**
```typescript
// pelayanan_surat_keterangan/create.ts
return {
success: true,
message: "Sukses menambahkan data",
data: created
};
// pelayanan_telunjuk_sakti_desa/create.ts
return new Response(
JSON.stringify({
status: 200,
message: "Sukses menambahkan data",
data: created
})
);
// ajukan_permohonan/del.ts
return {
status: 200,
message: "Sukses menghapus data"
};
```
**Dampak:**
- Frontend harus handle multiple response formats
- Confusing untuk developer
- Harder to maintain
**Severity:** 🟡 **MEDIUM** - Code quality
**Solusi:**
```typescript
// Standardize response format
return {
success: boolean,
message: string,
data?: any,
// Optional: status code if needed
};
```
---
### 9. UI - Client-Side Search Instead of Server-Side
**File:** `src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/page.tsx`
```typescript
// Line 50-57
const filteredData = useMemo(() => {
if (!search) return data || [];
return (data || []).filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.deskripsi.toLowerCase().includes(search.toLowerCase())
);
}, [data, search]);
```
**Dampak:**
- Semua data di-load dari server (no server-side filtering)
- Performance issue jika data banyak
- Pagination tidak bekerja dengan benar (filter setelah pagination)
**Severity:** 🟡 **MEDIUM** - Performance issue
**Solusi:**
```typescript
// Pass search to API
const load = async (page: number, limit: number, search: string) => {
pelayananSuratKeteranganState.findMany.loading = true;
try {
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan['find-many'].get({
query: { page, limit, search }
});
// ...
}
};
```
---
## 🟢 LOW PRIORITY ISSUES
### 10. UI - Table Fixed Layout Without Column Widths
**File:** Multiple list pages
```typescript
<Table layout="fixed">
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
```
**Dampak:** Column widths tidak konsisten, bisa break layout.
**Severity:** 🟢 **LOW** - UI polish
**Solusi:**
```typescript
<Table layout="fixed">
<TableThead>
<TableTr>
<TableTh w="30%">Nama</TableTh>
<TableTh w="50%">Deskripsi</TableTh>
<TableTh w="20%">Aksi</TableTh>
</TableTr>
</TableThead>
</Table>
```
---
### 11. State - Inconsistent Ordering
**File:** Multiple state files
```typescript
// ajukan_permohonan/findMany.ts
orderBy: { createdAt: 'asc' } // ❌ Ascending
// pelayanan_surat_keterangan/find-many.ts
orderBy: { createdAt: 'desc' } // ✅ Descending
```
**Dampak:** Inconsistent data display (oldest first vs newest first).
**Severity:** 🟢 **LOW** - UX consistency
**Solusi:** Standardize to `orderBy: { createdAt: 'desc' }` for all modules.
---
### 12. UI - Missing Loading States (Some Edit Pages)
**File:** Some edit pages
```typescript
useEffect(() => {
state.load(params.id);
}, [params.id]);
// ❌ No loading state check
return (
<form>
{/* Form fields */}
</form>
);
```
**Dampak:** Form bisa render dengan empty data saat loading.
**Severity:** 🟢 **LOW** - UX polish
**Solusi:**
```typescript
const [loading, setLoading] = useState(true);
useEffect(() => {
state.load(params.id).finally(() => setLoading(false));
}, [params.id]);
if (loading) {
return <Skeleton height={400} radius="md" />;
}
return (
<form>
{/* Form fields */}
</form>
);
```
---
### 13. UI - Memory Leak Potential (createObjectURL)
**File:** Multiple create/edit pages with image upload
```typescript
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreviewImage(url);
}
}, [file]);
// ❌ No cleanup
```
**Dampak:** Memory leak jika user upload banyak gambar.
**Severity:** 🟢 **LOW** - Performance
**Solusi:**
```typescript
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreviewImage(url);
return () => {
URL.revokeObjectURL(url); // ✅ Cleanup
};
}
}, [file]);
```
---
### 14. Schema - `deletedAt @default(now())` (SAME BUG AS OTHER MODULES)
**File:** `prisma/schema.prisma`
```prisma
model PelayananSuratKeterangan {
deletedAt DateTime @default(now()) // ❌ SAME BUG
}
model PelayananTelunjukSaktiDesa {
deletedAt DateTime @default(now()) // ❌ SAME BUG
}
model PelayananPerizinanBerusaha {
deletedAt DateTime @default(now()) // ❌ SAME BUG
}
model PelayananPendudukNonPermanen {
deletedAt DateTime @default(now()) // ❌ SAME BUG
}
model AjukanPermohonan {
deletedAt DateTime @default(now()) // ❌ SAME BUG
}
```
**Dampak:** Record baru langsung ter-mark deleted.
**Severity:** 🟢 **LOW** - (Actually MEDIUM, tapi sudah documented di QC lain)
**Solusi:**
```prisma
deletedAt DateTime? // Remove @default(now())
```
---
### 15. UI - No Error Boundary
**File:** No error boundary found
**Dampak:** Error di component bisa crash entire app.
**Severity:** 🟢 **LOW** - Code quality
**Solusi:**
```typescript
// Add Error Boundary di layout.tsx
'use client'
import { Component, ReactNode } from 'react'
class ErrorBoundary extends Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return <ErrorFallback />
}
return this.props.children
}
}
```
---
## ✅ YANG SUDAH BAIK
### **Schema:**
- ✅ Relasi yang jelas antara `AjukanPermohonan` dan `PelayananSuratKeterangan`
- ✅ Soft delete pattern dengan `deletedAt` dan `isActive`
- ✅ Audit trail dengan `createdAt` dan `updatedAt`
- ✅ Dual image support untuk `PelayananSuratKeterangan`
### **API:**
- ✅ CRUD lengkap untuk `pelayanan_surat_keterangan` dan `pelayanan_telunjuk_sakti_desa`
- ✅ Pagination support
- ✅ Search functionality
- ✅ Soft delete di-support via `isActive` flag
- ✅ Response format mostly consistent: `{ success, message, data }`
### **UI/UX:**
- ✅ Responsive design (desktop + mobile)
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation comprehensive
- ✅ Dual image upload dengan preview (surat keterangan)
- ✅ Rich text editor untuk deskripsi
- ✅ Search dengan debounce
- ✅ Modal konfirmasi hapus
- ✅ Interactive stepper (perizinan berusaha)
- ✅ Reset form functionality
### **State Management:**
- ✅ Valtio proxy untuk global state
- ✅ Zod validation schema
- ✅ Loading state management
- ✅ Auto-refresh after CRUD operations
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 7/10 | Good structure, tapi ada bug deletedAt |
| **API Completeness** | 5/10 | 2 modul incomplete (missing endpoints) |
| **API Security** | 5/10 | Tidak ada authentication |
| **UI/UX** | 7.5/10 | Responsive, good features |
| **State Management** | 6.5/10 | Good structure, ada bug |
| **Code Quality** | 6/10 | Inconsistent patterns, hardcoded values |
**Overall Score: 6.5/10** - **Needs Improvement**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes) 🔴
- [ ] **URGENT:** Fix delete endpoint inconsistency (`pelayanan_telunjuk_sakti_desa`)
- [ ] **URGENT:** Fix state management bug (wrong variable assignment)
- [ ] **URGENT:** Fix hardcoded ID 'edit' di list pages
- [ ] **URGENT:** Create missing API endpoints (`find-many`, `create`, `del`) untuk 2 modul
### Week 2 (Complete Features) 🟡
- [ ] Create `create/page.tsx` untuk 2 modul tanpa create
- [ ] Move edit logic to `[id]/edit/page.tsx` untuk consistency
- [ ] Add validation for `link` field di state
- [ ] Standardize response format di semua API
- [ ] Move client-side search to server-side
### Week 3 (Polish) 🟢
- [ ] Add column widths untuk fixed layout tables
- [ ] Standardize ordering (`createdAt: desc`)
- [ ] Add loading states di semua edit pages
- [ ] Fix memory leak (revoke Object URLs)
- [ ] Add Error Boundary di layout
- [ ] Fix `deletedAt @default(now())` di schema
---
## 📝 Technical Notes
### **Database Migration:**
Fix deletedAt default:
```bash
bunx prisma migrate dev --name fix_layanan_deleted_at
# atau
bunx prisma db push
# Data cleanup
UPDATE "PelayananSuratKeterangan" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "PelayananTelunjukSaktiDesa" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "PelayananPerizinanBerusaha" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "PelayananPendudukNonPermanen" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "AjukanPermohonan" SET "deletedAt" = NULL WHERE "isActive" = true;
```
### **API Endpoint Checklist:**
**pelayanan_perizinan_berusaha:**
- [ ] Create `find-many.ts`
- [ ] Create `create.ts`
- [ ] Create `del.ts`
- [ ] Update `index.ts` dengan routes baru
**pelayanan_penduduk_non_permanen:**
- [ ] Create `find-many.ts`
- [ ] Create `create.ts`
- [ ] Create `del.ts`
- [ ] Update `index.ts` dengan routes baru
### **Frontend Checklist:**
**pelayanan_perizinan_berusaha:**
- [ ] Convert `page.tsx` dari preview ke list page
- [ ] Create `create/page.tsx`
- [ ] Move edit logic ke `[id]/edit/page.tsx`
**pelayanan_penduduk_non_permanen:**
- [ ] Convert `page.tsx` dari preview ke list page
- [ ] Create `create/page.tsx`
- [ ] Move edit logic ke `[id]/edit/page.tsx`
---
## 📚 References
- [Prisma Soft Delete Pattern](https://www.prisma.io/docs/concepts/components/prisma-client/soft-delete)
- [Mantine Table Documentation](https://mantine.dev/core/table/)
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
- [Zod Documentation](https://zod.dev/)
- [URL.createObjectURL() Memory Management](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#memory_management)
---
## 📈 Comparison dengan QC Sebelumnya
| Aspek | Profil | Potensi | Berita | Pengumuman | Gallery | **Layanan** |
|-------|--------|---------|--------|------------|---------|-------------|
| Schema | 6/10 | 7/10 | 8/10 | 7/10 | 6/10 | **7/10** |
| API Completeness | 7/10 | 8/10 | 7.5/10 | 7/10 | 6/10 | **5/10** 🔴 |
| API Security | 4/10 | 6/10 | 6/10 | 6/10 | 4/10 | **5/10** |
| UI/UX | 8/10 | 8.5/10 | 8/10 | 7.5/10 | 7.5/10 | **7.5/10** |
| State Mgmt | 7/10 | 8/10 | 8/10 | 7/10 | 6.5/10 | **6.5/10** |
| Code Quality | 7/10 | 7.5/10 | 7/10 | 6.5/10 | 6/10 | **6/10** |
| **Overall** | **6.5/10** | **7.5/10** | **7/10** | **6.5/10** | **6/10** | **6.5/10** |
**Layanan** memiliki score sama dengan **Profil Desa** dan **Pengumuman** karena:
**Positif:**
- ✅ Schema design lebih baik (dual image support, relasi yang jelas)
- ✅ UI/UX bagus (responsive, interactive stepper)
- ✅ Most modules complete
**Negatif:**
-**2 modul incomplete** (missing API endpoints & create pages)
-**Hardcoded ID 'edit'** di production code
-**State management bug** (wrong variable assignment)
-**Inconsistent endpoint patterns** (delete endpoint beda)
- ❌ Missing authentication
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer
**Next Review:** Setelah implementasi fixes

View File

@@ -0,0 +1,774 @@
# Quality Control Report - Penghargaan Desa Admin
**Lokasi:** `/src/app/admin/(dashboard)/desa/penghargaan/`
**Tanggal QC:** 25 Februari 2026
**Status:****Good** (dengan beberapa issue security yang perlu diperbaiki)
---
## 📋 Ringkasan Eksekutif
Halaman Penghargaan Desa memiliki implementasi yang **cukup baik** dengan CRUD lengkap, upload gambar, dan state management terstruktur. Ditemukan **11 issue** dengan rincian:
- 🔴 **High Priority:** 2 issue
- 🟡 **Medium Priority:** 5 issue
- 🟢 **Low Priority:** 4 issue
**Overall Score: 7/10** - Good
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/desa/penghargaan/
├── page.tsx # List penghargaan dengan search & pagination
├── create/
│ └── page.tsx # Create penghargaan dengan upload gambar
└── [id]/
├── page.tsx # Detail penghargaan
└── edit/
└── page.tsx # Edit penghargaan dengan replace image
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/desa/penghargaan.ts`
- API: `/src/app/api/[[...slugs]]/_lib/desa/penghargaan/` (6 files)
- Schema: `/prisma/schema.prisma` (Model `Penghargaan`)
---
## 🔴 HIGH PRIORITY ISSUES
### 1. XSS Vulnerability via `dangerouslySetInnerHTML`
**File:** `src/app/admin/(dashboard)/desa/penghargaan/page.tsx`
```typescript
// Line 79
<TableTd
dangerouslySetInnerHTML={{
__html: item.deskripsi, // ❌ XSS VULNERABILITY
}}
/>
```
**Same issue di:** `src/app/admin/(dashboard)/desa/penghargaan/[id]/page.tsx` line 89
```typescript
<Box
dangerouslySetInnerHTML={{
__html: data.deskripsi, // ❌ XSS VULNERABILITY
}}
/>
```
**Dampak:**
- User bisa inject malicious script melalui rich text editor
- XSS attack bisa mencuri session, cookies, atau data sensitif
- Admin lain yang lihat data bisa terinfeksi
**Severity:** 🔴 **HIGH** - Security vulnerability
**Solusi:**
**Option A - Sanitize HTML (Recommended):**
```typescript
// Install: bun add dompurify
import DOMPurify from 'dompurify';
// Di component
<TableTd
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
/>
```
**Option B - Strip HTML Tags:**
```typescript
const stripHtml = (html: string) => {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
};
<TableTd>{stripHtml(item.deskripsi)}</TableTd>
```
**Option C - Server-Side Sanitization:**
```typescript
// Di API create.ts dan updt.ts
import sanitizeHtml from 'sanitize-html';
const sanitizedDeskripsi = sanitizeHtml(body.deskripsi, {
allowedTags: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
allowedAttributes: {}
});
```
---
### 2. Inconsistent Fetch Patterns (ApiFetch vs fetch)
**File:** `src/app/admin/(dashboard)/_state/desa/penghargaan.ts`
```typescript
// Line 45-53 (create) - Menggunakan ApiFetch ✅
const res = await ApiFetch.api.desa.penghargaan.create.post(penghargaan.create.form);
// Line 90-93 (findUnique) - Menggunakan fetch langsung ❌
const res = await fetch(`/api/desa/penghargaan/${id}`);
const data = await res.json();
// Line 108-120 (delete) - Menggunakan fetch langsung ❌
const response = await fetch(`/api/desa/penghargaan/del/${id}`, {
method: 'DELETE',
});
const result = await response.json();
// Line 147-165 (edit.load) - Menggunakan fetch langsung ❌
const response = await fetch(`/api/desa/penghargaan/${id}`);
const result = await response.json();
```
**Dampak:**
- Code maintainability kurang
- Tidak type-safe
- Inconsistent error handling
- Sulit refactor
**Severity:** 🔴 **HIGH** - Code quality issue
**Solusi:**
```typescript
// Gunakan ApiFetch untuk semua
// findUnique
const data = await ApiFetch.api.desa.penghargaan[':id'].get({ query: { id } });
// delete
const result = await ApiFetch.api.desa.penghargaan['del/:id'].delete({ params: { id } });
// edit.load
const data = await ApiFetch.api.desa.penghargaan[':id'].get({ query: { id } });
```
---
## 🟡 MEDIUM PRIORITY ISSUES
### 3. Tidak Ada Validasi Duplicate Name
**File:** `src/app/api/[[...slugs]]/_lib/desa/penghargaan/create.ts`
```typescript
// Line 13-23
const penghargaan = await prisma.penghargaan.create({
data: {
name: body.name, // ❌ Tidak cek duplicate
juara: body.juara,
deskripsi: body.deskripsi,
imageId: body.imageId,
},
});
```
**Same issue di:** `updt.ts` (update endpoint)
**Dampak:**
- User bisa buat penghargaan dengan nama sama
- Data redundancy
- Confusing saat search
**Severity:** 🟡 **MEDIUM** - Data integrity
**Solusi:**
```typescript
// Check duplicate sebelum create
const existing = await prisma.penghargaan.findFirst({
where: {
name: body.name,
isActive: true
}
});
if (existing) {
return Response.json({
success: false,
message: "Nama penghargaan sudah digunakan"
}, { status: 400 });
}
// Lanjut create
const penghargaan = await prisma.penghargaan.create({ ... });
```
**Alternative - Schema Level:**
```prisma
model Penghargaan {
name String @unique // Add unique constraint
// ...
}
```
---
### 4. Search Tidak Reset Pagination
**File:** `src/app/admin/(dashboard)/desa/penghargaan/page.tsx`
```typescript
// Line 35-38
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
```
**Dampak:**
- User di page 5, search untuk data yang hanya ada di page 1
- Result kosong, user bingung
- UX buruk
**Severity:** 🟡 **MEDIUM** - UX issue
**Solusi:**
```typescript
// Reset page saat search berubah
useShallowEffect(() => {
if (debouncedSearch !== search) {
setPage(1); // Reset to page 1
}
load(page, 10, debouncedSearch);
}, [page, debouncedSearch, search]);
```
**Better Solution:**
```typescript
// Watch search separately
useEffect(() => {
setPage(1); // Reset page saat search berubah
}, [debouncedSearch]);
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
```
---
### 5. Image Upload Hanya Saat Submit
**File:** `src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx`
```typescript
// Line 81-95
const handleSubmit = async () => {
// Validasi
// ...
// Upload image BARU saat submit
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar');
}
// Create penghargaan
await statePenghargaan.penghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.penghargaan.create();
};
```
**Dampak:**
- Jika create penghargaan gagal, file sudah ter-upload (orphaned file)
- User tidak bisa preview image yang sudah di-upload sebelumnya
- Tidak ada progress indicator saat upload
**Severity:** 🟡 **MEDIUM** - Data integrity & UX
**Solusi:**
**Option A - Upload Dulu, Baru Create:**
```typescript
// Upload immediately saat file selected
const handleFileChange = async (file: File) => {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (uploaded?.id) {
setFile(file);
setPreviewImage(URL.createObjectURL(file));
statePenghargaan.penghargaan.create.form.imageId = uploaded.id;
}
};
// Submit hanya create penghargaan
const handleSubmit = async () => {
await statePenghargaan.penghargaan.create();
};
```
**Option B - Transaction dengan Rollback:**
```typescript
const handleSubmit = async () => {
try {
// Upload file
const uploaded = await uploadFile(file);
// Create penghargaan
const result = await createPenghargaan({ imageId: uploaded.id });
if (!result.success) {
// Rollback: delete uploaded file
await deleteFile(uploaded.id);
throw new Error('Create failed');
}
} catch (error) {
toast.error('Gagal membuat penghargaan');
}
};
```
---
### 6. Dropzone Accept Format Typo
**File:** `src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx`
```typescript
// Line 140-143
<Dropzone
accept={{
'image/*': ['.jpeg', '.jpg', '.png', 'webp'] // ❌ Typo: "webp" seharusnya ".webp"
}}
// ...
>
```
**Same issue di:** `edit/page.tsx` line 180-183
**Dampak:**
- File `.webp` tidak akan di-accept oleh dropzone
- User confusion saat coba upload WebP
- Inconsistent dengan validasi lainnya
**Severity:** 🟡 **MEDIUM** - UX issue
**Solusi:**
```typescript
<Dropzone
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp'] // ✅ Fix typo
}}
// ...
>
```
---
### 7. Schema `deletedAt` Default Value (SAME BUG)
**File:** `prisma/schema.prisma`
```prisma
model Penghargaan {
id String @id @default(cuid())
name String
deletedAt DateTime @default(now()) // ❌ SAME BUG AS OTHER MODULES
isActive Boolean @default(true)
}
```
**Dampak:**
- Record baru langsung ter-mark deleted saat dibuat
- Soft delete logic tidak bekerja
- Query dengan `deletedAt: null` tidak dapat data baru
**Severity:** 🟡 **MEDIUM** - Data integrity bug
**Solusi:**
```prisma
model Penghargaan {
id String @id @default(cuid())
name String
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
```
**Migration:**
```bash
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_penghargaan_deleted_at
# Data cleanup
UPDATE "Penghargaan" SET "deletedAt" = NULL WHERE "isActive" = true;
```
---
## 🟢 LOW PRIORITY ISSUES
### 8. `isHtmlEmpty` Tidak Handle Edge Cases
**File:** `src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx`
```typescript
// Line 23-26
const isHtmlEmpty = (html: string) => {
const textContent = html.replace(/<[^>]*>/g, '').trim();
return textContent === '';
};
```
**Dampak:**
- HTML dengan hanya `&nbsp;` atau `<br>` akan dianggap empty
- User bisa submit content yang sebenarnya kosong
**Severity:** 🟢 **LOW** - Validation edge case
**Solusi:**
```typescript
const isHtmlEmpty = (html: string) => {
// Strip HTML tags
const tmp = document.createElement('div');
tmp.innerHTML = html;
// Get text content
const textContent = tmp.textContent || tmp.innerText || '';
// Check if empty or only whitespace
return textContent.trim().length === 0;
};
```
---
### 9. Duplicate Validation Check
**File:** `src/app/admin/(dashboard)/desa/penghargaan/create/page.tsx`
```typescript
// Line 58-73: Validasi pertama
const handleSubmit = async () => {
if (!statePenghargaan.penghargaan.create.form.name?.trim()) {
toast.error('Nama penghargaan wajib diisi');
return;
}
// ... validasi lainnya
// Line 81-84: Validasi diulang lagi (redundant)
if (
!statePenghargaan.penghargaan.create.form.name?.trim() ||
!statePenghargaan.penghargaan.create.form.juara?.trim() ||
isHtmlEmpty(statePenghargaan.penghargaan.create.form.deskripsi) ||
!file
) {
toast.error('Mohon lengkapi semua data');
return;
}
};
```
**Dampak:** Code redundancy, minor performance overhead.
**Severity:** 🟢 **LOW** - Code quality
**Solusi:**
```typescript
const handleSubmit = async () => {
// Single validation block
if (!statePenghargaan.penghargaan.create.form.name?.trim()) {
toast.error('Nama penghargaan wajib diisi');
return;
}
if (!statePenghargaan.penghargaan.create.form.juara?.trim()) {
toast.error('Juara wajib diisi');
return;
}
if (isHtmlEmpty(statePenghargaan.penghargaan.create.form.deskripsi)) {
toast.error('Deskripsi wajib diisi');
return;
}
if (!file) {
toast.error('Gambar wajib diunggah');
return;
}
// Submit logic
// ...
};
```
---
### 10. Inconsistent Button Labels (Reset vs Batal)
**File:** Create page vs Edit page
```typescript
// create/page.tsx line 109
<Button onClick={resetForm} variant="outline" color="gray">
Reset // ❌ Inconsistent
</Button>
// edit/page.tsx line 100
<Button onClick={handleResetForm} variant="outline" color="gray">
Batal // ❌ Inconsistent
</Button>
```
**Dampak:** Minor UX inconsistency.
**Severity:** 🟢 **LOW** - UX consistency
**Solusi:** Standardize to "Reset Form" untuk kedua page.
---
### 11. Tidak Ada Karakter Counter
**File:** Create & Edit pages
```typescript
<TextInput
label="Nama Penghargaan"
value={statePenghargaan.penghargaan.create.form.name}
onChange={(e) => {
statePenghargaan.penghargaan.create.form.name = e.target.value;
}}
// ❌ Tidak ada maxLength atau character counter
/>
```
**Dampak:** User tidak tahu ada limit atau tidak.
**Severity:** 🟢 **LOW** - UX polish
**Solusi:**
```typescript
<TextInput
label="Nama Penghargaan"
value={statePenghargaan.penghargaan.create.form.name}
onChange={(e) => {
statePenghargaan.penghargaan.create.form.name = e.target.value;
}}
maxLength={255} // Add max length
rightSection={
<Text size="sm" c="dimmed">
{statePenghargaan.penghargaan.create.form.name?.length || 0}/255
</Text>
}
/>
```
---
## ✅ YANG SUDAH BAIK
### **Schema:**
- ✅ Relasi ke FileStorage untuk gambar sudah benar
- ✅ Soft delete pattern dengan `deletedAt` dan `isActive`
- ✅ Audit trail dengan `createdAt` dan `updatedAt`
- ✅ Field yang diperlukan sudah lengkap
### **API:**
- ✅ CRUD lengkap untuk Penghargaan
- ✅ Pagination support dengan `page`, `limit`, `search`
- ✅ Search functionality dengan case-insensitive
- ✅ Include relasi image di response
-**File cleanup saat update** (hapus old image) ✅
-**File cleanup saat delete** (hapus image) ✅
- ✅ Parallel query untuk data & count (optimasi performa)
- ✅ Response format mostly konsisten: `{ success, message, data }`
### **UI/UX:**
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation comprehensive
- ✅ Image upload dengan dropzone & preview
- ✅ File size limit & format validation
- ✅ Rich text editor untuk deskripsi
- ✅ Search dengan debounce (1000ms)
- ✅ Modal konfirmasi hapus
- ✅ Empty state message
- ✅ Reset form functionality
- ✅ Button disabled saat invalid/submitting
### **State Management:**
- ✅ Valtio proxy untuk global state
- ✅ Zod validation schema
- ✅ Loading state management
- ✅ Auto-refresh after CRUD operations
- ✅ Error handling dengan toast
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 7/10 | Good, tapi ada bug deletedAt |
| **API Design** | 7.5/10 | RESTful, file cleanup implemented |
| **API Security** | 5/10 | Tidak ada auth, XSS vulnerability |
| **UI/UX** | 8/10 | Responsive, comprehensive features |
| **State Management** | 7/10 | Valtio works well, inconsistent fetch |
| **Code Quality** | 7/10 | Good structure, minor inconsistencies |
**Overall Score: 7/10** - **Good**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes) 🔴
- [ ] **URGENT:** Sanitize HTML content (DOMPurify) untuk XSS prevention
- [ ] **URGENT:** Konsistensi fetch pattern (gunakan ApiFetch untuk semua)
### Week 2 (Medium Priority) 🟡
- [ ] Tambahkan validasi duplicate name di API create/update
- [ ] Fix search reset pagination logic
- [ ] Fix image upload timing (upload dulu atau transaction)
- [ ] Fix dropzone accept format typo (`.webp`)
- [ ] Fix `deletedAt @default(now())` di schema
### Week 3 (Polish) 🟢
- [ ] Improve `isHtmlEmpty` function
- [ ] Remove duplicate validation
- [ ] Standardize button labels (Reset Form)
- [ ] Add character counter untuk text fields
- [ ] Add loading state saat load data di edit page
---
## 📝 Technical Notes
### **Database Migration:**
Fix deletedAt default:
```bash
bunx prisma migrate dev --name fix_penghargaan_deleted_at
# atau
bunx prisma db push
# Data cleanup
UPDATE "Penghargaan" SET "deletedAt" = NULL WHERE "isActive" = true;
```
### **XSS Prevention:**
Install DOMPurify:
```bash
bun add dompurify
bun add -D @types/dompurify
```
Usage:
```typescript
import DOMPurify from 'dompurify';
// Di component
<Box
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(data.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
/>
```
### **Duplicate Name Prevention:**
API validation:
```typescript
// Check existing name
const existing = await prisma.penghargaan.findFirst({
where: {
name: body.name,
isActive: true,
id: body.id ? { not: body.id } : undefined // Exclude current for update
}
});
if (existing) {
return Response.json({
success: false,
message: "Nama penghargaan sudah digunakan"
}, { status: 400 });
}
```
### **Search Reset Pagination:**
```typescript
// Watch search separately
useEffect(() => {
setPage(1); // Reset page saat search berubah
}, [debouncedSearch]);
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
```
---
## 📚 References
- [Prisma Soft Delete Pattern](https://www.prisma.io/docs/concepts/components/prisma-client/soft-delete)
- [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
- [Mantine Dropzone Documentation](https://mantine.dev/x/dropzone/)
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
- [Zod Documentation](https://zod.dev/)
---
## 📈 Comparison dengan QC Sebelumnya
| Aspek | Profil | Potensi | Berita | Pengumuman | Gallery | Layanan | **Penghargaan** |
|-------|--------|---------|--------|------------|---------|---------|-----------------|
| Schema | 6/10 | 7/10 | 8/10 | 7/10 | 6/10 | 7/10 | **7/10** |
| API Design | 7/10 | 8/10 | 7.5/10 | 7/10 | 6/10 | 5/10 | **7.5/10** ✅ |
| API Security | 4/10 | 6/10 | 6/10 | 6/10 | 4/10 | 5/10 | **5/10** |
| UI/UX | 8/10 | 8.5/10 | 8/10 | 7.5/10 | 7.5/10 | 7.5/10 | **8/10** ✅ |
| State Mgmt | 7/10 | 8/10 | 8/10 | 7/10 | 6.5/10 | 6.5/10 | **7/10** |
| Code Quality | 7/10 | 7.5/10 | 7/10 | 6.5/10 | 6/10 | 6/10 | **7/10** |
| **Overall** | **6.5/10** | **7.5/10** | **7/10** | **6.5/10** | **6/10** | **6.5/10** | **7/10** |
**Penghargaan** memiliki score **tertinggi kedua** (setelah Potensi Desa) karena:
**Positif:**
- ✅ CRUD lengkap & berfungsi dengan baik
- ✅ File cleanup implemented (update & delete) ✅
- ✅ Responsive design bagus
- ✅ Comprehensive validation
- ✅ Parallel query untuk performa
- ✅ Tidak ada incomplete features (seperti Layanan)
- ✅ Tidak ada critical data loss bugs (seperti Gallery)
**Yang Perlu Diperbaiki:**
- ❌ XSS vulnerability (dangerouslySetInnerHTML)
- ❌ Inconsistent fetch patterns
- ❌ Duplicate name validation tidak ada
-`deletedAt @default(now())` bug
- ❌ Search tidak reset pagination
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer
**Next Review:** Setelah implementasi fixes

View File

@@ -0,0 +1,809 @@
# Quality Control Report - Pengumuman Desa Admin
**Lokasi:** `/src/app/admin/(dashboard)/desa/pengumuman/`
**Tanggal QC:** 25 Februari 2026
**Status:** ⚠️ **Needs Improvement** (ada issue critical yang perlu segera diperbaiki)
---
## 📋 Ringkasan Eksekutif
Halaman Pengumuman Desa memiliki implementasi yang **cukup baik** dengan CRUD lengkap dan state management terstruktur. Namun ditemukan **15 issue** dengan rincian:
- 🔴 **High Priority:** 2 issue
- 🟡 **Medium Priority:** 7 issue
- 🟢 **Low Priority:** 6 issue
**Overall Score: 6.5/10** - Needs Improvement
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/desa/pengumuman/
├── layout.tsx
├── _com/
│ └── layoutTabs.tsx # Tab navigation component
├── kategori-pengumuman/
│ ├── page.tsx # List kategori dengan search & pagination
│ ├── create/
│ │ └── page.tsx # Form create kategori
│ └── [id]/
│ └── page.tsx # Edit kategori
└── list-pengumuman/
├── page.tsx # List pengumuman dengan search & pagination
├── create/
│ └── page.tsx # Form create pengumuman (rich text)
└── [id]/
├── page.tsx # Detail pengumuman
└── edit/
└── page.tsx # Edit pengumuman
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/desa/pengumuman.ts`
- API: `/src/app/api/[[...slugs]]/_lib/desa/pengumuman/` (9 files)
- API: `/src/app/api/[[...slugs]]/_lib/desa/pengumuman/kategori-pengumuman/` (6 files)
- Schema: `/prisma/schema.prisma` (Model `Pengumuman` & `CategoryPengumuman`)
---
## 🔴 HIGH PRIORITY ISSUES
### 1. API - Hard Delete vs Soft Delete Mismatch (DATA LOSS RISK)
**File:** `src/app/api/[[...slugs]]/_lib/desa/pengumuman/del.ts`
```typescript
export default async function pengumumanDelete(context: Context) {
const id = context.params?.id as string;
// ❌ HARD DELETE - Data benar-benar terhapus dari database
await prisma.pengumuman.delete({ where: { id } });
return { success: true, message: "Pengumuman berhasil dihapus" };
}
```
**Schema yang Diharapkan:**
```prisma
model Pengumuman {
deletedAt DateTime? @default(null) // Soft delete field
isActive Boolean @default(true)
}
```
**Dampak:**
- **DATA LOSS** - Data pengumuman terhapus permanen, tidak bisa direcover
- Audit trail hilang (riwayat pengumuman tidak ada lagi)
- Inconsistent dengan schema design yang sudah ada soft delete fields
- Bisa melanggar compliance requirements untuk data retention
**Solusi:**
```typescript
// Ganti hard delete dengan soft delete
export default async function pengumumanDelete(context: Context) {
const id = context.params?.id as string;
// ✅ SOFT DELETE - Update deletedAt dan isActive
await prisma.pengumuman.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false
}
});
return { success: true, message: "Pengumuman berhasil dihapus" };
}
```
**File yang Perlu Diperbaiki:**
- `src/app/api/[[...slugs]]/_lib/desa/pengumuman/del.ts`
- `src/app/api/[[...slugs]]/_lib/desa/pengumuman/kategori-pengumuman/del.ts`
---
### 2. Schema - `deletedAt` Default Value `now()` Bermasalah
**File:** `prisma/schema.prisma`
```prisma
model Pengumuman {
id String @id @default(cuid())
judul String
deletedAt DateTime @default(now()) // ❌ PROBLEMATIC DEFAULT
isActive Boolean @default(true)
}
model CategoryPengumuman {
id String @id @default(cuid())
name String @unique
deletedAt DateTime @default(now()) // ❌ PROBLEMATIC DEFAULT
isActive Boolean @default(true)
}
```
**Dampak:**
- Setiap record **baru langsung ter-mark sebagai deleted** saat dibuat
- Query dengan filter `deletedAt: null` tidak akan dapat data baru
- Soft delete logic tidak bekerja dengan benar
- Data inconsistency antara `deletedAt` (set) dan `isActive` (true)
**Solusi:**
```prisma
model Pengumuman {
id String @id @default(cuid())
judul String
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
model CategoryPengumuman {
id String @id @default(cuid())
name String @unique
deletedAt DateTime? // ✅ Nullable, tanpa default
isActive Boolean @default(true)
}
```
**Migration Required:**
```bash
# Generate migration
bunx prisma migrate dev --name fix_deleted_at_default
# Atau jika tidak pakai migrate
bunx prisma db push
# Data cleanup untuk record yang sudah ter-affected
UPDATE "Pengumuman" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "CategoryPengumuman" SET "deletedAt" = NULL WHERE "isActive" = true;
```
---
## 🟡 MEDIUM PRIORITY ISSUES
### 3. UI - Search Parameter Hilang Saat Pagination
**File:** `src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx`
```typescript
<Pagination
total={totalPages}
value={page}
onChange={(newPage) => {
load(newPage, 10); // ❌ Missing search parameter
}}
/>
```
**Dampak:**
- Saat user ganti halaman, search query hilang
- User harus ketik ulang search query
- UX sangat buruk untuk pagination dengan search
- Inconsistent dengan page lain (berita, potensi)
**Solusi:**
```typescript
<Pagination
total={totalPages}
value={page}
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search parameter
}}
/>
```
**Note:** Pastikan function `load` menerima parameter search:
```typescript
const load = async (page: number, limit: number, searchQuery?: string) => {
// ...
};
```
---
### 4. UI - Duplicate State Management
**File:** `src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/edit/page.tsx`
```typescript
// Local state
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
content: '',
categoryPengumumanId: '',
});
const [originalData, setOriginalData] = useState({...formData});
// Global state (Valtio)
editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form,
...formData, // ❌ Duplicate data
};
```
**Dampak:**
- Data inconsistency antara local state dan global state
- Sulit debug karena data ada di 2 tempat
- Memory overhead
- Potential bugs saat reset form
**Solusi:**
**Option A - Gunakan hanya global state:**
```typescript
// Hapus local state, gunakan langsung global state
const formData = editState.pengumuman.edit.form;
const handleResetForm = () => {
editState.pengumuman.edit.form = { ...originalData };
};
```
**Option B - Sinkronisasi dengan useEffect:**
```typescript
useEffect(() => {
// Sync local state ke global state
editState.pengumuman.edit.form = { ...formData };
}, [formData]);
```
---
### 5. UI - Error Handling Silent Failures
**File:** `src/app/admin/(dashboard)/_state/desa/pengumuman.ts`
```typescript
// Line 266-268
catch (error) {
console.log((error as Error).message);
// ❌ Error tidak ditampilkan ke user, silent failure
}
```
**Dampak:**
- User tidak tahu ada error
- Sulit debug production issues
- User experience buruk (loading forever tanpa feedback)
**Solusi:**
```typescript
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('Failed to load pengumuman:', errorMessage);
toast.error(`Gagal memuat data: ${errorMessage}`);
}
```
---
### 6. UI - ColSpan Mismatch
**File:** `src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/page.tsx`
```typescript
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Dibuat</TableTh>
<TableTh>Aksi</TableTh> {/* 3 kolom total */}
</TableTr>
</TableThead>
<TableTbody>
{loading ? (
<TableTr>
<TableTd colSpan={4}> {/* ❌ colSpan 4, seharusnya 3 */}
<Skeleton height={40} />
</TableTd>
</TableTr>
) : (
// ...
)}
</TableTbody>
```
**Dampak:** Layout table tidak rapi, colSpan terlalu lebar.
**Solusi:**
```typescript
<TableTd colSpan={3}> // ✅ Match column count
```
---
### 7. State Management - Copy-Paste Error Message
**File:** `src/app/admin/(dashboard)/_state/desa/pengumuman.ts`
```typescript
// Line 68-70
kategoriPengumuman: {
findMany: {
loading: false,
async load(page = 1, limit = 10, search = '') {
try {
// ...
} catch (error) {
console.error("Failed to load potensi desa:", res.data?.message);
// ❌ Copy-paste error dari file potensi! Seharusnya "kategori pengumuman"
}
}
}
}
```
**Dampak:**
- Membingungkan saat debug
- Tidak profesional
- Menunjukkan kurangnya attention to detail
**Solusi:**
```typescript
console.error("Failed to load kategori pengumuman:", res.data?.message);
```
---
### 8. UI - Button Text "Batal" Membingungkan
**File:** `src/app/admin/(dashboard)/desa/pengumuman/kategori-pengumuman/[id]/page.tsx`
```typescript
<Button
onClick={handleResetForm}
variant="outline"
color="gray"
>
Batal // ❌ Membingungkan - "Batal" biasanya untuk cancel navigation
</Button>
```
**Dampak:** User mungkin bingung apakah button ini akan cancel edit atau reset form.
**Solusi:**
```typescript
<Button
onClick={handleResetForm}
variant="outline"
color="gray"
>
Reset Form // ✅ Lebih jelas
</Button>
```
---
### 9. UI - Button Order Tidak Mengikuti UX Best Practice
**File:** `src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/[id]/page.tsx`
```typescript
<Group gap="sm">
<Button color="red"> {/* Delete button first */}
<Button color="green"> {/* Edit button second */}
</Group>
```
**Dampak:** Destructive action (delete) lebih prominent daripada primary action (edit).
**Solusi:**
```typescript
<Group gap="sm">
<Button color="green"> {/* Edit button first */}
<Button color="red"> {/* Delete button second */}
</Group>
```
**UX Best Practice:** Primary action (edit) seharusnya lebih prominent, destructive action (delete) kurang prominent dan lebih sulit diakses.
---
## 🟢 LOW PRIORITY ISSUES
### 10. UI - Inline Styles yang Panjang
**File:** `src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx`
```typescript
<TabsList
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
border: "1px solid #d1d5db",
padding: "0.5rem",
borderRadius: "12px",
display: "flex",
gap: "0.5rem",
// ... 10+ baris inline styles
}}
>
```
**Dampak:**
- Sulit maintain
- Tidak reusable
- Code readability buruk
**Solusi:**
```typescript
// Option A: CSS module
// layoutTabs.module.css
.tabsList {
background: linear-gradient(135deg, #e7ebf7, #f9faff);
boxShadow: 0 2px 8px rgba(0,0,0,0.08);
// ...
}
// Component
<TabsList className={styles.tabsList}>
```
**Option B: Mantine theme**
```typescript
// theme.ts
const theme = createTheme({
components: {
TabsList: {
styles: {
root: {
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
// ...
}
}
}
}
});
```
---
### 11. UI - Hardcoded Paths
**File:** `src/app/admin/(dashboard)/desa/pengumuman/_com/layoutTabs.tsx`
```typescript
const tabs = [
{ href: "/admin/desa/pengumuman/list-pengumuman" },
{ href: "/admin/desa/pengumuman/kategori-pengumuman" },
];
```
**Dampak:** Sulit refactor, jika ada perubahan struktur URL harus update di banyak tempat.
**Solusi:**
```typescript
// constants/routes.ts
export const ROUTES = {
PENGUMUMAN_LIST: '/admin/desa/pengumuman/list-pengumuman',
PENGUMUMAN_CREATE: '/admin/desa/pengumuman/list-pengumuman/create',
PENGUMUMAN_EDIT: (id: string) => `/admin/desa/pengumuman/list-pengumuman/${id}/edit`,
KATEGORI_PENGUMUMAN_LIST: '/admin/desa/pengumuman/kategori-pengumuman',
KATEGORI_PENGUMUMAN_CREATE: '/admin/desa/pengumuman/kategori-pengumuman/create',
KATEGORI_PENGUMUMAN_EDIT: (id: string) => `/admin/desa/pengumuman/kategori-pengumuman/${id}/edit`,
};
// Usage
const tabs = [
{ href: ROUTES.PENGUMUMAN_LIST },
{ href: ROUTES.KATEGORI_PENGUMUMAN_LIST },
];
```
---
### 12. UI - HTML Validation Function Bisa False Positive
**File:** `src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/create/page.tsx`
```typescript
const isHtmlEmpty = (html: string) => {
const textContent = html.replace(/<[^>]*>/g, '').trim();
return textContent === '';
};
```
**Dampak:**
- Konten dengan hanya `<br>` atau `<p> </p>` akan dianggap empty
- User bisa submit content yang sebenarnya kosong
**Solusi:**
```typescript
const isHtmlEmpty = (html: string) => {
// Strip HTML tags
const tmp = document.createElement('div');
tmp.innerHTML = html;
// Get text content and check if empty
const textContent = tmp.textContent || tmp.innerText || '';
return textContent.trim().length === 0;
};
```
---
### 13. State - Inconsistent API Client Usage
**File:** `src/app/admin/(dashboard)/_state/desa/pengumuman.ts`
```typescript
// ❌ Direct fetch
const res = await fetch(`/api/desa/kategoripengumuman/${id}`);
const data = await res.json();
// ✅ Di tempat lain pakai ApiFetch
const data = await ApiFetch.api.desa.kategoripengumuman[':id'].get({ query: { id } });
```
**Dampak:** Code maintainability kurang, tidak konsisten.
**Solusi:**
```typescript
// Gunakan ApiFetch untuk semua
const data = await ApiFetch.api.desa.kategoripengumuman[':id'].get({ query: { id } });
```
---
### 14. Layout - `isDetailPage` Logic Kurang Robust
**File:** `src/app/admin/(dashboard)/desa/pengumuman/layout.tsx`
```typescript
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5; // ❌ Magic number, bisa false positive
```
**Dampak:** Bisa false positive untuk path lain yang length sama.
**Contoh False Positive:**
```
/admin/desa/pengumuman/list-pengumuman/create // 6 segments, dianggap detail page ❌
```
**Solusi:**
```typescript
// Check last segment
const lastSegment = segments[segments.length - 1];
const isDetailPage = ['create', 'edit'].includes(lastSegment) ||
/^[a-zA-Z0-9]{20,}$/.test(lastSegment); // CUID pattern
```
---
### 15. API - Missing Validation
**File:** `src/app/api/[[...slugs]]/_lib/desa/pengumuman/create.ts`
```typescript
const body = await context.body;
// ❌ Tidak ada validasi uniqueness untuk judul
// ❌ Tidak ada validasi panjang maksimal
await prisma.pengumuman.create({
data: {
judul: body.judul, // Bisa sangat panjang
// ...
}
});
```
**Dampak:**
- User bisa buat pengumuman dengan judul sama
- User bisa input judul/deskripsi sangat panjang
- Database bisa penuh dengan data tidak valid
**Solusi:**
```typescript
// Validasi di API
const body = await context.body;
// Check uniqueness
const existing = await prisma.pengumuman.findFirst({
where: {
judul: body.judul,
isActive: true
}
});
if (existing) {
return new Response(
JSON.stringify({
success: false,
message: "Judul pengumuman sudah digunakan"
}),
{ status: 400 }
);
}
// Validate length
if (body.judul.length > 255) {
return new Response(
JSON.stringify({
success: false,
message: "Judul maksimal 255 karakter"
}),
{ status: 400 }
);
}
```
---
## ✅ YANG SUDAH BAIK
### **Schema:**
- ✅ Relasi yang jelas antara Pengumuman dan CategoryPengumuman (one-to-many)
- ✅ Soft delete pattern dengan `deletedAt` dan `isActive` (tapi ada bug di default value)
- ✅ Audit trail dengan `createdAt` dan `updatedAt`
- ✅ Unique constraint pada `name` di CategoryPengumuman
### **API:**
- ✅ CRUD lengkap untuk Pengumuman dan Kategori Pengumuman
- ✅ Pagination support dengan `page`, `limit`, `search`
- ✅ Search functionality dengan case-insensitive
- ✅ Include relasi (CategoryPengumuman) di response
- ✅ Validation input menggunakan Elysia `t.Object`
- ✅ Filter by kategori di find-many
### **UI/UX:**
- ✅ Konsisten design pattern
- ✅ Responsive untuk mobile dan desktop
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation yang comprehensive
- ✅ Rich text editor (TipTap) untuk content
- ✅ Search dengan debounce (500ms-1000ms)
- ✅ Modal konfirmasi hapus
- ✅ Empty state message
### **State Management:**
- ✅ Valtio proxy untuk global state
- ✅ Zod validation schema
- ✅ Loading state management
- ✅ Auto-refresh after CRUD operations
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 7/10 | Good, tapi ada bug di deletedAt default |
| **API Design** | 7/10 | RESTful, validation ada, tapi hard delete issue |
| **API Security** | 6/10 | Tidak ada authentication |
| **UI/UX** | 7.5/10 | Responsive, comprehensive validation |
| **State Management** | 7/10 | Valtio works well, ada inconsistency |
| **Code Quality** | 6.5/10 | Good structure, copy-paste errors, inline styles |
**Overall Score: 6.5/10** - **Needs Improvement**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes) 🔴
- [ ] **URGENT:** Fix hard delete → soft delete di API del.ts
- [ ] **URGENT:** Fix `deletedAt @default(now())` di schema
- [ ] Fix pagination pass search parameter
- [ ] Fix colSpan mismatch
### Week 2 (Medium Priority) 🟡
- [ ] Consolidate state management (local vs global)
- [ ] Improve error handling (no silent failures)
- [ ] Fix error message typo ("potensi desa" → "kategori pengumuman")
- [ ] Rename button "Batal" → "Reset Form"
- [ ] Fix button order (edit before delete)
### Week 3 (Polish) 🟢
- [ ] Move inline styles to CSS module/theme
- [ ] Extract hardcoded paths to constants
- [ ] Fix HTML validation function
- [ ] Konsisten gunakan ApiFetch
- [ ] Fix isDetailPage logic
- [ ] Add uniqueness validation di API create
---
## 📝 Technical Notes
### **Database Migration:**
Fix deletedAt default dan cleanup data:
```bash
# Generate migration
bunx prisma migrate dev --name fix_deleted_at_default
# Atau jika tidak pakai migrate
bunx prisma db push
# Data cleanup
UPDATE "Pengumuman" SET "deletedAt" = NULL WHERE "isActive" = true;
UPDATE "CategoryPengumuman" SET "deletedAt" = NULL WHERE "isActive" = true;
```
### **Soft Delete Implementation:**
Update semua delete endpoint:
```typescript
// Before (hard delete)
await prisma.pengumuman.delete({ where: { id } });
// After (soft delete)
await prisma.pengumuman.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false
}
});
```
### **API Testing:**
Test soft delete:
```bash
# 1. Create pengumuman
POST /api/desa/pengumuman/create
{
"judul": "Test Pengumuman",
"deskripsi": "Test",
"content": "Test content",
"categoryPengumumanId": "<id>"
}
# 2. Delete pengumuman
DELETE /api/desa/pengumuman/del/<id>
# 3. Verify soft delete (data masih ada tapi isActive = false)
GET /api/desa/pengumuman/<id>
# Expected: isActive = false, deletedAt != null
```
Test pagination dengan search:
1. Buka halaman List Pengumuman
2. Ketik search query (misal: "desa")
3. Klik pagination halaman 2
4. Verify search query masih ada dan result sesuai
---
## 📚 References
- [Prisma Soft Delete Pattern](https://www.prisma.io/docs/concepts/components/prisma-client/soft-delete)
- [Mantine Table Documentation](https://mantine.dev/core/table/)
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
- [Zod Documentation](https://zod.dev/)
- [TipTap Documentation](https://tiptap.dev/)
---
## 📈 Comparison dengan QC Sebelumnya
| Aspek | Profil Desa | Potensi Desa | Berita Desa | **Pengumuman** |
|-------|-------------|--------------|-------------|----------------|
| Schema | 6/10 | 7/10 | 8/10 | **7/10** |
| API Security | 4/10 | 6/10 | 6/10 | **6/10** |
| API Design | 7/10 | 8/10 | 7.5/10 | **7/10** |
| UI/UX | 8/10 | 8.5/10 | 8/10 | **7.5/10** |
| State Mgmt | 7/10 | 8/10 | 8/10 | **7/10** |
| Code Quality | 7/10 | 7.5/10 | 7/10 | **6.5/10** |
| **Overall** | **6.5/10** | **7.5/10** | **7/10** | **6.5/10** |
**Pengumuman** memiliki score yang sama dengan **Profil Desa** karena:
- ✅ Unique constraint pada `name` (CategoryPengumuman)
- ✅ Validation input di API
- ❌ Hard delete vs soft delete mismatch (critical)
- ❌ Copy-paste error messages
- ❌ Inline styles yang berlebihan
- ❌ Duplicate state management
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer
**Next Review:** Setelah implementasi fixes

View File

@@ -0,0 +1,658 @@
# Quality Control Report - Potensi Desa Admin
**Lokasi:** `/src/app/admin/(dashboard)/desa/potensi/`
**Tanggal QC:** 25 Februari 2026
**Status:****Good** (dengan area untuk improvement)
---
## 📋 Ringkasan Eksekutif
Halaman Potensi Desa memiliki implementasi yang **cukup baik** dengan CRUD lengkap, UI yang responsive, dan state management yang terstruktur. Ditemukan **15 issue** dengan rincian:
- 🔴 **High Priority:** 6 issue
- 🟡 **Medium Priority:** 6 issue
- 🟢 **Low Priority:** 3 issue
**Overall Score: 7.5/10** - Good
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/desa/potensi/
├── layout.tsx
├── _lib/
│ └── layoutTabs.tsx
├── kategori-potensi/
│ ├── page.tsx # List kategori dengan search & pagination
│ ├── create/
│ │ └── page.tsx # Form create kategori
│ └── [id]/
│ └── page.tsx # Edit kategori
└── list-potensi/
├── page.tsx # List potensi dengan search & pagination
├── create/
│ └── page.tsx # Form create potensi (rich text + image)
└── [id]/
├── page.tsx # Detail potensi
└── edit/
└── page.tsx # Edit potensi
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/desa/potensi.ts`
- API: `/src/app/api/[[...slugs]]/_lib/desa/potensi/` (10 files)
- API: `/src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/` (5 files)
- Schema: `/prisma/schema.prisma` (Model `PotensiDesa` & `KategoriPotensi`)
---
## 🔴 HIGH PRIORITY ISSUES
### 1. Schema - Tidak Ada Unique Constraint pada `name` dan `nama`
**File:** `prisma/schema.prisma`
```prisma
model PotensiDesa {
name String // ❌ Tidak ada @unique
deskripsi String
// ...
}
model KategoriPotensi {
nama String // ❌ Tidak ada @unique
// ...
}
```
**Dampak:**
- Bisa ada duplikasi nama kategori potensi (misal: "Pariwisata" muncul 2x)
- Bisa ada duplikasi judul potensi desa
- Menyulitkan user saat mencari data
**Solusi:**
```prisma
model PotensiDesa {
name String @unique // ✅ Add unique constraint
// ...
}
model KategoriPotensi {
nama String @unique // ✅ Add unique constraint
// ...
}
```
**Migration Required:**
```bash
bunx prisma db push
# atau
bunx prisma migrate dev --name add_unique_constraints
```
---
### 2. Schema - `kategoriId` Nullable Seharusnya Required
**File:** `prisma/schema.prisma`
```prisma
model PotensiDesa {
kategoriId String? // ❌ Nullable, seharusnya required
// ...
}
```
**Dampak:** Potensi desa bisa dibuat tanpa kategori, tidak masuk akal secara bisnis.
**Solusi:**
```prisma
model PotensiDesa {
kategoriId String // ✅ Remove ? (required)
// ...
}
```
**Note:** Perlu update form create/edit untuk validasi kategori wajib dipilih.
---
### 3. Schema - Tidak Ada Length Constraints
**File:** `prisma/schema.prisma`
```prisma
model PotensiDesa {
name String // ❌ Tidak ada max length
deskripsi String @db.Text
// ...
}
model KategoriPotensi {
nama String // ❌ Tidak ada max length
// ...
}
```
**Dampak:** User bisa input nama sangat panjang, bisa break UI atau database.
**Solusi:**
```prisma
model PotensiDesa {
name String @db.VarChar(255) // ✅ Max 255 chars
deskripsi String @db.Text
// ...
}
model KategoriPotensi {
nama String @db.VarChar(100) // ✅ Max 100 chars
// ...
}
```
---
### 4. API - Delete Kategori Tanpa Cek Relasi
**File:** `src/app/api/[[...slugs]]/_lib/desa/potensi/kategori-potensi/del.ts`
```typescript
export default async function kategoriPotensiDelete(context: Context) {
const id = context.params?.id as string;
// ❌ Tidak cek apakah kategori masih dipakai oleh PotensiDesa
await prisma.kategoriPotensi.update({
where: { id },
data: { deletedAt: new Date(), isActive: false }
});
return { success: true, message: "Kategori potensi berhasil dihapus" };
}
```
**Dampak:**
- Bisa terjadi foreign key constraint error
- Data inconsistency jika kategori masih dipakai
**Solusi:**
```typescript
// Cek apakah masih ada potensi yang menggunakan kategori ini
const existingPotensi = await prisma.potensiDesa.findFirst({
where: {
kategoriId: id,
isActive: true
}
});
if (existingPotensi) {
return Response.json({
success: false,
message: "Kategori masih digunakan oleh potensi desa. Tidak dapat dihapus."
}, { status: 400 });
}
```
---
### 5. API - `find-unique.ts` Tidak Filter `isActive`
**File:** `src/app/api/[[...slugs]]/_lib/desa/potensi/find-unique.ts`
```typescript
const data = await prisma.potensiDesa.findUnique({
where: { id }, // ❌ Tidak cek isActive
include: {
image: true,
kategori: true
}
});
```
**Dampak:** Bisa load data yang sudah di-soft delete.
**Solusi:**
```typescript
const data = await prisma.potensiDesa.findUnique({
where: {
id,
isActive: true // ✅ Add filter
},
include: {
image: true,
kategori: true
}
});
```
---
### 6. UI - HTML Injection Risk (XSS Vulnerability)
**File:** Multiple pages
**`kategori-potensi/page.tsx`:**
```typescript
<TableTd dangerouslySetInnerHTML={{ __html: item.nama }} />
```
**`list-potensi/page.tsx`:**
```typescript
<TableTd dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
```
**Dampak:**
- User bisa inject malicious script melalui rich text editor
- XSS attack bisa mencuri session atau data sensitif
**Solusi:**
```typescript
// Install: bun add dompurify
import DOMPurify from 'dompurify';
// Sanitize sebelum render
<TableTd
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.deskripsi, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}}
/>
```
**Alternatif (tanpa library):**
```typescript
// Strip HTML tags completely
const stripHtml = (html: string) => {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
};
<TableTd>{stripHtml(item.deskripsi)}</TableTd>
```
---
## 🟡 MEDIUM PRIORITY ISSUES
### 7. API - Inconsistent Naming Convention
**File:** API routes
```
potensi/
├── find-many.ts // ❌ kebab-case
└── kategori-potensi/
└── findMany.ts // ❌ camelCase
```
**Dampak:** Membingungkan developer, tidak konsisten.
**Solusi:** Standardize ke **kebab-case** (konsisten dengan endpoint lain):
```bash
mv findMany.ts find-many.ts
mv findUnique.ts find-unique.ts
mv updt.ts update.ts
mv del.ts delete.ts
```
Update semua import di frontend.
---
### 8. UI - Pagination Tidak Pass Search Parameter
**File:** `src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx`
```typescript
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ❌ Tidak ada search parameter
}}
/>
```
**Dampak:** Saat ganti halaman, search query hilang.
**Solusi:**
```typescript
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search); // ✅ Include search
}}
/>
```
---
### 9. UI - colSpan Mismatch
**File:** `src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx`
```typescript
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Dibuat</TableTh>
<TableTh>Aksi</TableTh> {/* 3 kolom */}
</TableTr>
</TableThead>
<TableTbody>
{loading ? (
<TableTr>
<TableTd colSpan={4}> {/* ❌ colSpan 4, seharusnya 3 */}
<Skeleton height={40} />
</TableTd>
</TableTr>
) : (
// ...
)}
</TableTbody>
```
**Solusi:**
```typescript
<TableTd colSpan={3}> // ✅ Match column count
```
---
### 10. UI - Alert Instead of Toast
**File:** `src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx`
```typescript
if (!nama.trim()) {
alert('Nama kategori potensi wajib diisi'); // ❌ Browser alert
return;
}
```
**Dampak:** Browser alert blocking, UX buruk, tidak konsisten dengan page lain.
**Solusi:**
```typescript
import { toast } from 'react-toastify';
if (!nama.trim()) {
toast.error('Nama kategori potensi wajib diisi'); // ✅ Toast notification
return;
}
```
---
### 11. UI - Missing useEffect Dependencies
**File:** `src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx`
```typescript
useEffect(() => {
potensiState.kategoriPotensi.findMany.load();
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]); // ❌ Missing potensiState
```
**Dampak:** ESLint warning, potential stale closure.
**Solusi:**
```typescript
useEffect(() => {
potensiState.kategoriPotensi.findMany.load();
load(page, 10, debouncedSearch);
}, [page, debouncedSearch, potensiState]); // ✅ Add missing dep
```
**Note:** Atau gunakan `useCallback` untuk `load` function.
---
### 12. UI - Dropzone Accept Tidak Specify Extensions
**File:** `src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx`
```typescript
<Dropzone
accept={{ "image/*": [] }} // ❌ Terlalu general
// ...
>
```
**Dampak:** User bisa upload format image aneh yang tidak didukung browser.
**Solusi:**
```typescript
<Dropzone
accept={{
"image/*": ['.jpeg', '.jpg', '.png', '.webp'] // ✅ Specify extensions
}}
// ...
>
```
---
## 🟢 LOW PRIORITY ISSUES
### 13. UI - Magic Number untuk Detail Page Detection
**File:** `src/app/admin/(dashboard)/desa/potensi/layout.tsx`
```typescript
const isDetailPage = segments.length >= 5; // ❌ Magic number
```
**Dampak:** Tidak jelas maksudnya, brittle jika ada perubahan route structure.
**Solusi:**
```typescript
const isDetailPage = segments.includes('[id]') ||
segments.some(s => !['create', 'edit'].includes(s) && s.match(/^\w+$/));
// Atau lebih baik lagi:
const isDetailPage = segments.some(s => s.match(/^[a-zA-Z0-9]{20,}$/)); // CUID pattern
```
---
### 14. API - Inconsistent Error Handling
**File:** Multiple API handlers
**Contoh inconsistency:**
```typescript
// File A - Return object
return { success: false, message: "Error" };
// File B - Throw error
throw new Error("Something went wrong");
// File C - Return Response
return Response.json({ success: false }, { status: 500 });
```
**Solusi:** Standardize ke satu format:
```typescript
// Always return Response.json dengan format konsisten
return Response.json({
success: false,
message: "Error message",
data: null
}, { status: 500 });
```
---
### 15. State - Inconsistent Loading State
**File:** `src/app/admin/(dashboard)/_state/desa/potensi.ts`
```typescript
delete: {
loading: false,
async byId(id: string) {
try {
// ❌ Loading di-set di dalam async function
potensiDesa.delete.loading = true;
// ...
} finally {
potensiDesa.delete.loading = false;
}
}
}
```
**Solusi:** Konsisten set loading di awal dan reset di finally untuk semua operation.
---
## ✅ YANG SUDAH BAIK
### **Schema:**
- ✅ Soft delete dengan `deletedAt` dan `isActive`
- ✅ Relasi yang jelas antara PotensiDesa dan KategoriPotensi
- ✅ Relasi ke FileStorage untuk gambar
- ✅ Timestamp lengkap (createdAt, updatedAt)
### **API:**
- ✅ CRUD lengkap untuk kedua entitas
- ✅ Pagination support dengan `page`, `limit`, `search`
- ✅ Search functionality dengan case-insensitive
- ✅ Include relasi (image, kategori) pada find-many dan find-unique
- ✅ File cleanup (hapus file fisik + database) saat update/delete
- ✅ Response format konsisten: `{ success, message, data }`
### **UI/UX:**
- ✅ Konsisten design pattern
- ✅ Responsive untuk mobile dan desktop
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation yang comprehensive
- ✅ Rich text editor dengan toolbar lengkap
- ✅ Image upload dengan preview dan delete button
- ✅ Search dengan debounce
- ✅ Modal konfirmasi hapus
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 7/10 | Good, tapi perlu unique constraints |
| **API Design** | 8/10 | RESTful, konsisten, perlu standardisasi naming |
| **API Security** | 6/10 | Tidak ada auth, XSS vulnerability |
| **UI/UX** | 8.5/10 | Responsive, comprehensive validation |
| **State Management** | 8/10 | Valtio works well, minor inconsistency |
| **Code Quality** | 7.5/10 | Good structure, beberapa bug minor |
**Overall Score: 7.5/10** - **Good**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes)
- [ ] Add unique constraint pada `name` dan `nama` di schema
- [ ] Make `kategoriId` required di schema
- [ ] Add length constraints (@db.VarChar)
- [ ] Fix delete kategori dengan relation check
- [ ] Add `isActive` filter di find-unique API
- [ ] Add HTML sanitization (DOMPurify)
### Week 2 (Medium Priority)
- [ ] Standardize API naming (kebab-case)
- [ ] Fix pagination pass search parameter
- [ ] Fix colSpan mismatch
- [ ] Replace alert dengan toast
- [ ] Fix useEffect dependencies
- [ ] Specify dropzone extensions
### Week 3 (Polish)
- [ ] Remove magic number di layout
- [ ] Standardize error handling di API
- [ ] Fix loading state consistency
- [ ] Add authentication middleware
- [ ] Add unit tests untuk critical functions
---
## 📝 Technical Notes
### **Database Migration:**
Setelah update schema:
```bash
# Generate migration
bunx prisma migrate dev --name add_unique_and_length_constraints
# Atau jika tidak pakai migrate
bunx prisma db push
# Handle duplicate data (jika ada)
# Query manual untuk merge/delete duplicates
```
### **HTML Sanitization:**
Install DOMPurify:
```bash
bun add dompurify
bun add -D @types/dompurify
```
Usage:
```typescript
import DOMPurify from 'dompurify';
// Di component
const sanitizedContent = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'],
ALLOWED_ATTR: []
});
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
```
### **API Testing:**
Test delete kategori dengan relasi:
```bash
# 1. Create kategori
POST /api/desa/kategoripotensi/create
{ "nama": "Test Kategori" }
# 2. Create potensi dengan kategori tersebut
POST /api/desa/potensi/create
{
"name": "Test Potensi",
"kategoriId": "<kategori_id>",
...
}
# 3. Try delete kategori (should fail)
DELETE /api/desa/kategoripotensi/del/<kategori_id>
# Expected: { success: false, message: "Kategori masih digunakan..." }
```
---
## 📚 References
- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
- [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
- [Mantine Table Documentation](https://mantine.dev/core/table/)
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer
**Next Review:** Setelah implementasi fixes

View File

@@ -0,0 +1,371 @@
# Quality Control Report - Profil Desa Admin
**Lokasi:** `/src/app/admin/(dashboard)/desa/profil/`
**Tanggal QC:** 25 Februari 2026
**Status:** ⚠️ **Needs Improvement**
---
## 📋 Ringkasan Eksekutif
Halaman Profil Desa sudah memiliki struktur yang baik dengan separation of concerns yang jelas antara UI, State Management, dan API. Namun ditemukan **16 issue** dengan rincian:
- 🔴 **High Priority:** 5 issue
- 🟡 **Medium Priority:** 5 issue
- 🟢 **Low Priority:** 6 issue
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/desa/profil/
├── layout.tsx
├── _lib/
│ ├── layoutTabsDetail.tsx
│ └── layoutTabsEdit.tsx
├── profil-desa/
│ ├── page.tsx
│ └── [id]/
│ ├── sejarah_desa/page.tsx
│ ├── visi_misi_desa/page.tsx
│ ├── lambang_desa/page.tsx
│ └── maskot_desa/page.tsx
├── profil-perbekel/
│ ├── page.tsx
│ └── [id]/page.tsx
└── profil-perbekel-dari-masa-ke-masa/
├── page.tsx
├── create/page.tsx
└── [id]/
├── page.tsx
└── edit/page.tsx
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/desa/profile.ts` (1058 baris)
- API: `/src/app/api/[[...slugs]]/_lib/desa/profile/` (15+ files)
- Schema: `/prisma/schema.prisma`
---
## 🔴 HIGH PRIORITY ISSUES
### 1. Schema Bug - `deletedAt` Default Value Salah
**File:** `prisma/schema.prisma`
```prisma
model SejarahDesa {
deletedAt DateTime @default(now()) // ❌ BUG: Record langsung ter-delete!
isActive Boolean @default(true)
}
```
**Dampak:** Setiap record baru langsung ter-mark sebagai deleted karena `deletedAt` di-set ke `now()` saat create.
**Solusi:**
```prisma
deletedAt DateTime? // ✅ Nullable, tanpa default
```
**Affected Models:** `SejarahDesa`, `VisiMisiDesa`, `LambangDesa`, `MaskotDesa`
---
### 2. API Tidak Ada Authentication
**File:** Semua file di `/src/app/api/[[...slugs]]/_lib/desa/profile/`
```typescript
export default async function sejarahDesaUpdate(context: Context) {
// ❌ Tidak ada validasi session/user
const id = context.params?.id as string;
// Langsung proses update...
}
```
**Dampak:** Siapa saja yang tahu endpoint bisa update/delete data tanpa login.
**Solusi:** Tambahkan middleware authentication di route handler atau di setiap endpoint.
---
### 3. Hardcoded Nama Perbekel di UI
**File:** `src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx`
```typescript
<Text>
I.B. Surya Prabhawa Manuaba, S.H., M.H. // ❌ Hardcoded!
</Text>
```
**Dampak:** UI tidak update otomatis jika ada perbekel baru.
**Solusi:** Ambil data dari database `ProfilPerbekel` dengan filter `isActive: true`.
---
### 4. Maskot Image Delete Logic Bug
**File:** `src/app/api/[[...slugs]]/_lib/desa/profile/profile_desa/maskot-desa/update.ts`
```typescript
// Hapus semua gambar lama
for (const old of existing.images) {
await prisma.fileStorage.delete({ where: { id: old.imageId } });
}
```
**Dampak:** Semua gambar lama **selalu dihapus**, bahkan jika user ingin mempertahankan beberapa gambar.
**Solusi:** Implementasi diff logic untuk membandingkan gambar yang dipertahankan vs dihapus.
---
### 5. Magic String "edit" sebagai ID
**File:** Multiple files di state dan API
```typescript
stateProfileDesa.sejarahDesa.findUnique.load("edit"); // ❌ Magic string
```
**Dampak:** Tidak type-safe, rentan typo, tidak scalable.
**Solusi:** Buat endpoint khusus `/first` atau `/active` untuk get record pertama yang aktif.
---
## 🟡 MEDIUM PRIORITY ISSUES
### 6. ProfileDesaImage Tanpa Soft Delete
**File:** `prisma/schema.prisma`
```prisma
model ProfileDesaImage {
// ❌ Tidak ada deletedAt, isActive, createdAt, updatedAt
id String @id @default(cuid())
label String
imageId String?
}
```
**Solusi:** Tambahkan audit fields:
```prisma
deletedAt DateTime?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
```
---
### 7. HTML Validation dengan Regex
**File:** `src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx`
```typescript
const isHtmlEmpty = (html: string) => {
const textContent = html.replace(/<[^>]*>/g, '').trim(); // ❌ Tidak robust
return textContent === '';
};
```
**Dampak:** Validasi bisa gagal untuk edge cases (nested tags, comments, script tags).
**Solusi:** Gunakan library `sanitize-html` atau DOMParser untuk extract text content.
---
### 8. Image Label Tidak Divvalidasi
**File:** `src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx`
**Dampak:** User bisa submit dengan label kosong atau sangat panjang.
**Solusi:** Tambahkan validation:
```typescript
z.object({
label: z.string().min(1, "Label wajib diisi").max(100, "Maksimal 100 karakter")
})
```
---
### 9. Typo Variable Name
**File:** `src/app/api/[[...slugs]]/_lib/desa/profile/profilePerbekel/update.ts`
```typescript
if (exisitng.imageId !== imageId) { // ❌ Typo: "exisitng"
```
**Solusi:** Fix menjadi `existing`.
---
### 10. Tidak Ada Error Boundary
**Dampak:** Jika ada error di component tree, seluruh halaman bisa crash.
**Solusi:** Tambahkan React Error Boundary di layout.tsx:
```typescript
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<ErrorFallback />}>
{children}
</ErrorBoundary>
```
---
## 🟢 LOW PRIORITY ISSUES
### 11. Image Loading Tanpa Skeleton
**File:** `src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx`
**Dampak:** Layout shift saat image load, UX kurang smooth.
**Solusi:** Tambahkan Skeleton component:
```typescript
{loading ? (
<Skeleton height={200} circle />
) : (
<Image src={imageUrl} alt="..." />
)}
```
---
### 12. Reset Form Tanpa Konfirmasi
**File:** `src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx`
**Dampak:** User bisa tidak sengaja reset form dan kehilangan perubahan.
**Solusi:** Tambahkan modal konfirmasi sebelum reset.
---
### 13. Sequential API Calls Tanpa Promise.all
**File:** `src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx`
```typescript
useEffect(() => {
stateProfileDesa.sejarahDesa.findUnique.load("edit");
stateProfileDesa.visiMisiDesa.findUnique.load("edit"); // ❌ Sequential
stateProfileDesa.lambangDesa.findUnique.load("edit");
stateProfileDesa.maskotDesa.findUnique.load("edit");
}, []);
```
**Solusi:** Gunakan `Promise.all` untuk parallel loading.
---
### 14. FileStorage Validation di Server
**Dampak:** User bisa upload file dengan tipe yang tidak diinginkan.
**Solusi:** Tambahkan MIME type check di server-side upload handler.
---
### 15. Mantan Perbekel Create Tidak Return ID
**File:** `src/app/api/[[...slugs]]/_lib/desa/profile/profile-mantan-perbekel/create.ts`
```typescript
return {
success: true,
data: { ...body }, // ❌ Tidak return ID
};
```
**Solusi:** Return ID record yang baru dibuat untuk referensi.
---
### 16. Tidak Ada Unique Constraint
**Dampak:** Bisa ada multiple record aktif untuk model yang seharusnya single-record.
**Solusi:** Tambahkan unique constraint atau validasi di API layer.
---
## ✅ Yang Sudah Baik
1.**Struktur folder terorganisir** dengan separation of concerns
2.**Responsive design** untuk mobile dan desktop
3.**Loading states** dan error handling dasar
4.**Form validation** client-side dengan Valtio
5.**Preview image** sebelum upload
6.**Toast notifications** untuk feedback user
7.**File cleanup** (hapus file fisik + database) di API
8.**Consistent response format** di semua API endpoint
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 6/10 | Ada bug critical di deletedAt |
| **API Security** | 4/10 | Tidak ada authentication |
| **API Design** | 7/10 | RESTful, tapi ada magic string |
| **UI/UX** | 8/10 | Responsive, tapi ada hardcoded data |
| **State Management** | 7/10 | Valtio works, tapi tidak type-safe |
| **Code Quality** | 7/10 | Ada typo, tidak ada error boundary |
**Overall Score: 6.5/10** - **Needs Improvement**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes)
- [ ] Fix `deletedAt @default(now())` di schema
- [ ] Tambahkan authentication middleware di API
- [ ] Fix hardcoded nama perbekel
- [ ] Fix maskot image delete logic
### Week 2 (Medium Priority)
- [ ] Tambahkan audit fields di ProfileDesaImage
- [ ] Fix HTML validation dengan library
- [ ] Tambahkan validasi image label
- [ ] Fix typo dan tambahkan error boundary
### Week 3 (Polish)
- [ ] Tambahkan skeleton loading untuk images
- [ ] Tambahkan konfirmasi reset form
- [ ] Optimasi dengan Promise.all
- [ ] Tambahkan server-side file validation
---
## 📝 Notes
1. **Database Migration Required:** Setelah fix schema, jalankan:
```bash
bunx prisma db push
```
2. **Data Migration:** Record yang sudah ter-create dengan `deletedAt` set perlu di-update:
```sql
UPDATE "SejarahDesa" SET "deletedAt" = NULL WHERE "isActive" = true;
```
3. **Testing:** Setelah fix authentication, test semua endpoint dengan:
- User belum login (should redirect)
- User login dengan role berbeda (should respect permissions)
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer

View File

@@ -0,0 +1,904 @@
# Quality Control Report - Posyandu Kesehatan Admin
**Lokasi:** `/src/app/admin/(dashboard)/kesehatan/posyandu/`
**Tanggal QC:** 25 Februari 2026
**Status:** ⚠️ **Needs Improvement** (ada issue critical data loss & validation)
---
## 📋 Ringkasan Eksekutif
Halaman Posyandu Kesehatan memiliki implementasi yang **cukup baik** dengan CRUD lengkap, upload gambar, dan state management terstruktur. Namun ditemukan **15 issue** dengan rincian:
- 🔴 **High Priority:** 5 issue
- 🟡 **Medium Priority:** 5 issue
- 🟢 **Low Priority:** 5 issue
**Overall Score: 6.5/10** - Needs Improvement
---
## 📁 Struktur File yang Diperiksa
```
/src/app/admin/(dashboard)/kesehatan/posyandu/
├── page.tsx # List posyandu dengan search & pagination
├── create/
│ └── page.tsx # Create posyandu dengan upload gambar
└── [id]/
├── page.tsx # Detail posyandu
└── edit/
└── page.tsx # Edit posyandu dengan replace image
```
**File Terkait:**
- State: `/src/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu.ts`
- API: `/src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/` (6 files)
- Schema: `/prisma/schema.prisma` (Model `Posyandu`)
- UI Components: `/src/app/admin/(dashboard)/_com/` (createEditor, editEditor, modalKonfirmasiHapus)
---
## 🔴 HIGH PRIORITY ISSUES
### 1. Delete Operation Hard Delete (DATA LOSS RISK)
**File:** `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/del.ts`
```typescript
// Line 28-37
// Hapus file gambar dari filesystem
const filePath = path.join(posyandu.image.path, posyandu.image.name);
await fs.unlink(filePath);
// Hapus dari database FileStorage
await prisma.fileStorage.delete({ where: { id: posyandu.image.id } });
// Hapus posyandu (HARD DELETE!) ❌
await prisma.posyandu.delete({ where: { id } });
```
**Schema yang Diharapkan:**
```prisma
model Posyandu {
deletedAt DateTime? @default(null) // Soft delete field
isActive Boolean @default(true)
}
```
**Dampak:**
- **DATA LOSS** - Data posyandu terhapus permanen, tidak bisa direcover
- Audit trail hilang (riwayat posyandu tidak ada lagi)
- **Inconsistent dengan schema design** yang sudah ada soft delete fields
- Bisa melanggar compliance requirements untuk data retention
**Severity:** 🔴 **HIGH** - Data loss risk
**Solusi:**
```typescript
// Ganti hard delete dengan soft delete
export default async function posyanduDelete(context: Context) {
const id = context.params?.id as string;
try {
// SOFT DELETE - Update deletedAt dan isActive
await prisma.posyandu.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false
}
});
return {
success: true,
message: "Posyandu berhasil dihapus"
};
} catch (error) {
console.error("Error deleting posyandu:", error);
return { success: false, message: "Gagal menghapus posyandu" };
}
}
```
**Note:** File cleanup sebaiknya tidak dilakukan saat soft delete, atau dipindah ke background job untuk hard delete data yang sudah lama ter-delete.
---
### 2. Tidak Ada Validasi Duplicate Name/Nomor
**File:** `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/create.ts`
```typescript
// Line 13-23
const posyandu = await prisma.posyandu.create({
data: {
name: body.name, // ❌ Tidak cek duplicate
nomor: body.nomor, // ❌ Tidak cek duplicate
deskripsi: body.deskripsi,
imageId: body.imageId,
jadwalPelayanan: body.jadwalPelayanan,
},
});
```
**Same issue di:** `updt.ts` (update endpoint)
**Dampak:**
- User bisa buat posyandu dengan nama/nomor sama
- Data redundancy
- Confusing saat search dan reporting
- Bisa terjadi data inconsistency
**Severity:** 🔴 **HIGH** - Data integrity
**Solusi:**
```typescript
// Validasi duplicate sebelum create
const existing = await prisma.posyandu.findFirst({
where: {
OR: [
{ name: body.name },
{ nomor: body.nomor }
],
isActive: true
}
});
if (existing) {
return Response.json({
success: false,
message: "Nama atau nomor posyandu sudah digunakan"
}, { status: 400 });
}
// Lanjut create
const posyandu = await prisma.posyandu.create({ ... });
```
**Alternative - Schema Level:**
```prisma
model Posyandu {
name String @unique @db.VarChar(255) // Add unique constraint
nomor String @unique @db.VarChar(50) // Add unique constraint
// ...
}
```
---
### 3. Tidak Ada Validasi imageId Existence
**File:** `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/create.ts`
```typescript
// Line 13-23
const posyandu = await prisma.posyandu.create({
data: {
imageId: body.imageId, // ❌ Tidak cek apakah FileStorage benar ada
// ...
},
});
```
**Dampak:**
- User bisa create posyandu dengan `imageId` yang tidak valid
- Orphaned records (posyandu dengan gambar yang tidak ada)
- Bisa error saat fetch data dengan include image
**Severity:** 🔴 **HIGH** - Data integrity
**Solusi:**
```typescript
// Validasi imageId existence
if (body.imageId) {
const imageExists = await prisma.fileStorage.findUnique({
where: { id: body.imageId }
});
if (!imageExists) {
return Response.json({
success: false,
message: "Gambar tidak valid atau tidak ditemukan"
}, { status: 404 });
}
}
// Lanjut create
const posyandu = await prisma.posyandu.create({ ... });
```
---
### 4. Race Condition di Edit Page
**File:** `src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx`
```typescript
// Line 53-59: Local state
const [formData, setFormData] = useState({
name: '',
nomor: '',
deskripsi: '',
jadwalPelayanan: '',
imageId: '',
});
// Line 79-95: Load data ke local state
useEffect(() => {
const loadPosyandu = async () => {
const data = await statePosyandu.edit.load(params?.id as string);
if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
// ...
});
}
};
loadPosyandu();
}, [params?.id]);
// Line 100-113: Reset form
const handleResetForm = () => {
setFormData({
name: originalData.name,
nomor: originalData.nomor,
// ...
});
// ❌ statePosyandu.edit.form tidak di-reset
};
// Line 133-140: Sync ke global state sebelum submit
useEffect(() => {
statePosyandu.edit.form = {
...statePosyandu.edit.form,
...formData,
};
}, [formData]);
```
**Dampak:**
- **Dual source of truth** - formData lokal dan statePosyandu.edit.form bisa tidak sinkron
- User bisa submit data yang tidak sesuai dengan yang ditampilkan di form
- Sulit debug karena data ada di 2 tempat
**Severity:** 🔴 **HIGH** - Data consistency
**Solusi:**
**Option A - Gunakan hanya global state (Recommended):**
```typescript
// Hapus local state, gunakan langsung global state
const formData = statePosyandu.edit.form;
const handleResetForm = () => {
statePosyandu.edit.form = { ...originalData };
};
// Submit langsung
const handleSubmit = async () => {
// Validasi
await statePosyandu.edit.update();
};
```
**Option B - Sinkronisasi dengan proper effect:**
```typescript
// Sync global state ke local state saat load
useEffect(() => {
const loadPosyandu = async () => {
const data = await statePosyandu.edit.load(params?.id as string);
if (data) {
statePosyandu.edit.form = {
name: data.name || '',
nomor: data.nomor || '',
// ...
};
setFormData(statePosyandu.edit.form);
}
};
loadPosyandu();
}, [params?.id]);
// Update global state saat formData berubah
useEffect(() => {
statePosyandu.edit.form = { ...formData };
}, [formData]);
```
---
### 5. Inconsistent API Client Usage
**File:** `src/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu.ts`
```typescript
// Line 45-53 (create) - Menggunakan ApiFetch ✅
const res = await ApiFetch.api.kesehatan.posyandu.create.post(posyandu.create.form);
// Line 90-93 (findUnique) - Menggunakan fetch langsung ❌
const res = await fetch(`/api/kesehatan/posyandu/${id}`);
const data = await res.json();
// Line 108-120 (delete) - Menggunakan fetch langsung ❌
const response = await fetch(`/api/kesehatan/posyandu/del/${id}`, {
method: 'DELETE',
});
const result = await response.json();
// Line 147-165 (edit.load) - Menggunakan fetch langsung ❌
const response = await fetch(`/api/kesehatan/posyandu/${id}`);
const result = await response.json();
```
**Dampak:**
- Code maintainability kurang
- Tidak type-safe
- Inconsistent error handling
- Sulit refactor
**Severity:** 🔴 **HIGH** - Code quality
**Solusi:**
```typescript
// Gunakan ApiFetch untuk semua
// findUnique
const data = await ApiFetch.api.kesehatan.posyandu[':id'].get({ query: { id } });
// delete
const result = await ApiFetch.api.kesehatan.posyandu['del/:id'].delete({ params: { id } });
// edit.load
const data = await ApiFetch.api.kesehatan.posyandu[':id'].get({ query: { id } });
```
---
## 🟡 MEDIUM PRIORITY ISSUES
### 6. Search Tidak Reset Pagination
**File:** `src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx`
```typescript
// Line 35-38
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
```
**Dampak:**
- User di page 5, search untuk data yang hanya ada di page 1
- Result kosong atau page error
- UX buruk
**Severity:** 🟡 **MEDIUM** - UX issue
**Solusi:**
```typescript
// Watch search separately
useEffect(() => {
setPage(1); // Reset page saat search berubah
}, [debouncedSearch]);
useEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
```
---
### 7. Find By ID Tidak Filter isActive
**File:** `src/app/api/[[...slugs]]/_lib/kesehatan/posyandu/find-by-id.ts`
```typescript
// Line 13-19
const data = await prisma.posyandu.findUnique({
where: { id }, // ❌ Tidak filter isActive
include: { image: true }
});
```
**Dampak:**
- Bisa fetch data yang sudah di-soft delete
- Data inconsistency
- Bisa tampil di UI padahal sudah dihapus
**Severity:** 🟡 **MEDIUM** - Data consistency
**Solusi:**
```typescript
const data = await prisma.posyandu.findFirst({
where: {
id,
isActive: true,
deletedAt: null // ✅ Filter soft-deleted data
},
include: { image: true }
});
```
---
### 8. Error Handling Upload Gambar Hanya console.log
**File:** `src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx`
```typescript
// Line 81-95
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error('Gagal mengunggah gambar'); // ❌ Generic error
console.error('Gagal upload gambar'); // ❌ Hanya console.log
return;
}
```
**Dampak:**
- User tidak tahu penyebab error
- Sulit debug production issues
- Error detail hilang
**Severity:** 🟡 **MEDIUM** - UX & debugging
**Solusi:**
```typescript
const uploaded = res.data?.data;
if (!uploaded?.id) {
const errorMessage = res.data?.message || 'Unknown error';
console.error('Gagal upload gambar:', errorMessage);
toast.error(`Gagal upload gambar: ${errorMessage}`);
return;
}
```
---
### 9. Tidak Ada Progress Indicator Upload
**File:** Create & Edit pages
**Dampak:**
- User tidak tahu upload sedang berjalan
- User bisa klik submit berkali-kali (duplicate upload)
- UX buruk untuk file besar
**Severity:** 🟡 **MEDIUM** - UX
**Solusi:**
```typescript
// Tambah loading state untuk upload
const [uploading, setUploading] = useState(false);
const handleUpload = async (file: File) => {
setUploading(true);
try {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
// ...
} finally {
setUploading(false);
}
};
// Disable button saat uploading
<Button type="submit" loading={submitting || uploading}>
Simpan
</Button>
```
---
### 10. Validasi Form Hanya di Frontend
**File:** Create & Edit pages
**Dampak:**
- User bisa bypass validation via API call langsung
- Data invalid bisa masuk ke database
- Security risk
**Severity:** 🟡 **MEDIUM** - Security & data integrity
**Solusi:**
```typescript
// Tambah validasi di API create.ts
const { name, nomor, deskripsi, jadwalPelayanan } = await context.body;
// Validasi required fields
if (!name || !nomor || !deskripsi || !jadwalPelayanan) {
return Response.json({
success: false,
message: "Semua field wajib diisi"
}, { status: 400 });
}
// Validasi length
if (name.length > 255) {
return Response.json({
success: false,
message: "Nama maksimal 255 karakter"
}, { status: 400 });
}
// Validasi nomor format (jika perlu)
if (!/^\d+$/.test(nomor)) {
return Response.json({
success: false,
message: "Nomor harus angka"
}, { status: 400 });
}
```
---
## 🟢 LOW PRIORITY ISSUES
### 11. Schema Field `name` Tidak Unique
**File:** `prisma/schema.prisma`
```prisma
model Posyandu {
name String // ❌ Tidak ada @unique (berbeda dengan Berita, KategoriBerita, dll)
nomor String // ❌ Tidak ada @unique
// ...
}
```
**Dampak:** Tidak ada constraint di database level untuk mencegah duplikasi.
**Severity:** 🟢 **LOW** - Schema design
**Solusi:**
```prisma
model Posyandu {
name String @unique @db.VarChar(255)
nomor String @unique @db.VarChar(50)
// ...
}
```
---
### 12. Tidak Ada Constraint Panjang untuk Field Text
**File:** `prisma/schema.prisma`
```prisma
model Posyandu {
name String // ❌ Tidak ada max length
nomor String // ❌ Tidak ada max length
deskripsi String @db.Text
jadwalPelayanan String // ❌ Tidak ada max length
// ...
}
```
**Dampak:** User bisa input text sangat panjang, bisa break UI atau database.
**Severity:** 🟢 **LOW** - Schema design
**Solusi:**
```prisma
model Posyandu {
name String @db.VarChar(255)
nomor String @db.VarChar(50)
deskripsi String @db.Text
jadwalPelayanan String @db.VarChar(500)
// ...
}
```
---
### 13. Empty State Tanpa Illustration
**File:** `src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx`
```typescript
// Line 67-69
{filteredData.length === 0 && (
<Box py="xl" ta="center">
<Text c="dimmed">Tidak ada data posyandu</Text>
</Box>
)}
```
**Dampak:** Empty state kurang informatif dan kurang visually appealing.
**Severity:** 🟢 **LOW** - UX polish
**Solusi:**
```typescript
{filteredData.length === 0 && (
<Box py="xl" ta="center">
<Image
src="/empty-state.svg"
alt="No data"
w={200}
mx="auto"
mb="md"
/>
<Text fw={600} mb="xs">Tidak ada data posyandu</Text>
<Text c="dimmed" size="sm">
{search ? 'Coba kata kunci lain' : 'Mulai dengan menambahkan posyandu baru'}
</Text>
{!search && (
<Button mt="md" onClick={() => router.push('/kesehatan/posyandu/create')}>
Tambah Posyandu
</Button>
)}
</Box>
)}
```
---
### 14. Tidak Ada Sorting Option
**File:** `find-many.ts` dan `page.tsx`
```typescript
// find-many.ts
orderBy: { createdAt: 'desc' } // ❌ Hardcoded, tidak ada option sorting
```
**Dampak:** User tidak bisa sort by name, nomor, atau jadwal.
**Severity:** 🟢 **LOW** - UX
**Solusi:**
```typescript
// API find-many.ts
const { page = 1, limit = 10, search = '', sortBy = 'createdAt', sortOrder = 'desc' } = context.query;
orderBy: {
[sortBy as string]: sortOrder === 'asc' ? 'asc' : 'desc'
}
```
---
### 15. Toast Error Tidak Spesifik
**File:** `posyandu.ts` state
```typescript
// Line 45-53
if (res.status === 200) {
toast.success("Posyandu berhasil disimpan!");
} else {
toast.error("Gagal menyimpan posyandu"); // ❌ Generic error
}
```
**Dampak:** User tidak tahu penyebab error.
**Severity:** 🟢 **LOW** - UX
**Solusi:**
```typescript
if (res.status === 200) {
toast.success("Posyandu berhasil disimpan!");
} else {
const errorMessage = res.data?.message || 'Terjadi kesalahan';
toast.error(`Gagal menyimpan posyandu: ${errorMessage}`);
}
```
---
## ✅ YANG SUDAH BAIK
### **Schema:**
- ✅ Relasi ke FileStorage untuk gambar sudah benar
- ✅ Soft delete pattern dengan `deletedAt` dan `isActive` (tapi tidak dipakai di delete)
- ✅ Audit trail dengan `createdAt` dan `updatedAt`
- ✅ Field yang diperlukan sudah lengkap (name, nomor, deskripsi, jadwal, image)
### **API:**
- ✅ CRUD lengkap untuk Posyandu
- ✅ Pagination support dengan `page`, `limit`, `search`
- ✅ Search functionality dengan case-insensitive (include semua field)
- ✅ Include relasi image di response
- ✅ File cleanup saat delete (hapus file fisik + database)
- ✅ Error handling ada di semua endpoints
- ✅ Response format konsisten: `{ success, message, data }`
### **UI/UX:**
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dan skeleton
- ✅ Toast notifications untuk feedback
- ✅ Form validation comprehensive (name, nomor, deskripsi, jadwal, image)
- ✅ Image upload dengan dropzone & preview
- ✅ File size limit & format validation
- ✅ Rich text editor untuk deskripsi dan jadwal
- ✅ Search dengan debounce (1000ms)
- ✅ Modal konfirmasi hapus
- ✅ Empty state message
- ✅ Reset form functionality
- ✅ Button disabled saat invalid/submitting
### **State Management:**
- ✅ Valtio proxy untuk global state
- ✅ Zod validation schema
- ✅ Loading state management
- ✅ Auto-refresh after CRUD operations
- ✅ Separate state untuk create, findMany, findUnique, edit, delete
---
## 📊 Metrics
| Aspek | Score | Keterangan |
|-------|-------|------------|
| **Schema Design** | 6.5/10 | Good structure, tapi tidak ada unique constraints |
| **API Design** | 6.5/10 | RESTful, file cleanup implemented, tapi tidak ada validation |
| **API Security** | 5/10 | Tidak ada auth, tidak ada backend validation |
| **UI/UX** | 7.5/10 | Responsive, comprehensive features |
| **State Management** | 6.5/10 | Valtio works well, inconsistent fetch patterns |
| **Code Quality** | 6.5/10 | Good structure, race condition potential |
**Overall Score: 6.5/10** - **Needs Improvement**
---
## 🎯 Action Plan
### Week 1 (Critical Fixes) 🔴
- [ ] **URGENT:** Fix delete operation (hard delete → soft delete)
- [ ] **URGENT:** Tambahkan validasi duplicate name/nomor di API
- [ ] **URGENT:** Tambahkan validasi imageId existence di API
- [ ] **URGENT:** Fix race condition di edit page (dual state)
- [ ] **URGENT:** Konsistensi fetch pattern (gunakan ApiFetch)
### Week 2 (Medium Priority) 🟡
- [ ] Fix search reset pagination logic
- [ ] Tambahkan filter isActive di find-by-id API
- [ ] Improve error handling upload gambar
- [ ] Tambahkan progress indicator untuk upload
- [ ] Tambahkan backend validation untuk semua field
### Week 3 (Polish) 🟢
- [ ] Tambahkan unique constraint di schema
- [ ] Tambahkan length constraints di schema
- [ ] Improve empty state dengan illustration
- [ ] Tambahkan sorting option
- [ ] Improve toast error messages
---
## 📝 Technical Notes
### **Database Migration:**
Fix deletedAt default dan add unique constraints:
```bash
# Generate migration
bunx prisma migrate dev --name fix_posyandu_deleted_at_and_unique
# Atau jika tidak pakai migrate
bunx prisma db push
# Data cleanup
UPDATE "Posyandu" SET "deletedAt" = NULL WHERE "isActive" = true;
```
### **Soft Delete Implementation:**
Update delete endpoint:
```typescript
// del.ts - Before (hard delete)
await prisma.posyandu.delete({ where: { id } });
// After (soft delete)
await prisma.posyandu.update({
where: { id },
data: {
deletedAt: new Date(),
isActive: false
}
});
```
### **Duplicate Validation:**
```typescript
// Check existing name/nomor
const existing = await prisma.posyandu.findFirst({
where: {
OR: [
{ name: body.name },
{ nomor: body.nomor }
],
isActive: true,
id: body.id ? { not: body.id } : undefined // Exclude current for update
}
});
if (existing) {
return Response.json({
success: false,
message: "Nama atau nomor posyandu sudah digunakan"
}, { status: 400 });
}
```
### **Race Condition Fix:**
```typescript
// Option A: Use only global state
const formData = statePosyandu.edit.form;
const handleResetForm = () => {
statePosyandu.edit.form = { ...originalData };
};
// Submit directly
const handleSubmit = async () => {
// Validation
await statePosyandu.edit.update();
};
```
---
## 📚 References
- [Prisma Soft Delete Pattern](https://www.prisma.io/docs/concepts/components/prisma-client/soft-delete)
- [Prisma Unique Constraints](https://www.prisma.io/docs/concepts/components/prisma-schema/relations)
- [Mantine Dropzone Documentation](https://mantine.dev/x/dropzone/)
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
- [Zod Documentation](https://zod.dev/)
- [Valtio Documentation](https://docs.pmnd.rs/valtio)
---
## 📈 Comparison dengan QC Sebelumnya
| Aspek | Profil | Potensi | Berita | Pengumuman | Gallery | Layanan | Penghargaan | **Posyandu** |
|-------|--------|---------|--------|------------|---------|---------|-------------|--------------|
| Schema | 6/10 | 7/10 | 8/10 | 7/10 | 6/10 | 7/10 | 7/10 | **6.5/10** |
| API Design | 7/10 | 8/10 | 7.5/10 | 7/10 | 6/10 | 5/10 | 7.5/10 | **6.5/10** |
| API Security | 4/10 | 6/10 | 6/10 | 6/10 | 4/10 | 5/10 | 5/10 | **5/10** |
| UI/UX | 8/10 | 8.5/10 | 8/10 | 7.5/10 | 7.5/10 | 7.5/10 | 8/10 | **7.5/10** ✅ |
| State Mgmt | 7/10 | 8/10 | 8/10 | 7/10 | 6.5/10 | 6.5/10 | 7/10 | **6.5/10** |
| Code Quality | 7/10 | 7.5/10 | 7/10 | 6.5/10 | 6/10 | 6/10 | 7/10 | **6.5/10** |
| **Overall** | **6.5/10** | **7.5/10** | **7/10** | **6.5/10** | **6/10** | **6.5/10** | **7/10** | **6.5/10** |
**Posyandu** memiliki score sama dengan **Profil Desa** dan **Pengumuman** karena:
**Positif:**
- ✅ CRUD lengkap & berfungsi dengan baik
- ✅ File cleanup implemented (delete) ✅
- ✅ Responsive design bagus
- ✅ Comprehensive validation di frontend
- ✅ Rich text editor untuk 2 field (deskripsi & jadwal)
- ✅ Search include semua field
**Negatif:**
-**Hard delete** vs soft delete mismatch (data loss risk)
-**Tidak ada validasi backend** (duplicate, imageId, required fields)
-**Race condition** di edit page (dual state)
-**Inconsistent fetch patterns** (ApiFetch vs fetch)
-**Tidak ada unique constraints** di schema
-**Tidak ada authentication** di API
---
**Dibuat oleh:** QC Automation
**Review Status:** ⏳ Menunggu Review Developer
**Next Review:** Setelah implementasi fixes

View File

@@ -0,0 +1,763 @@
# QC Summary - APBDes Module
**Scope:** List APBDes, Create, Edit, Detail
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa critical issues yang perlu diperbaiki
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| APBDes | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Consistency**
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dengan Skeleton
- ✅ Search dengan debounce (1000ms)
- ✅ Pagination konsisten
- ✅ Empty state handling yang informatif
- ✅ Modal konfirmasi hapus
### **2. File Upload Handling**
- ✅ Dual upload: Gambar + Dokumen
- ✅ Dropzone dengan preview (image + iframe untuk dokumen)
- ✅ Validasi format (gambar: JPEG/PNG/WEBP, dokumen: PDF/DOC/DOCX)
- ✅ Validasi ukuran file (max 5MB untuk gambar, 10MB untuk dokumen di edit)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
### **3. Form Validation**
- ✅ Zod schema untuk validasi typed
- ✅ isFormValid() check sebelum submit
- ✅ Error toast dengan pesan spesifik
- ✅ Button disabled saat invalid/loading
- ✅ Type number input untuk tahun
### **4. Complex Feature - APBDes Items**
- ✅ Hierarchical items dengan level (1, 2, 3)
- ✅ Tipe classification (pendapatan, belanja, pembiayaan)
- ✅ Auto-calculation: selisih & persentase
- ✅ Add/remove items dynamic
- ✅ Table preview dengan badge color coding
- ✅ Indentasi visual berdasarkan level
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Preview image & dokumen dari data lama
- ✅ Reset form mengembalikan ke data original
- ✅ File replacement logic (upload baru jika ada perubahan)
**Code Example (✅ GOOD):**
```typescript
// Line ~95-130 - Load data & save original
const data = await apbdesState.edit.load(id);
setOriginalData({
tahun: data.tahun || new Date().getFullYear(),
imageId: data.imageId || '',
fileId: data.fileId || '',
imageUrl: data.image?.link || '',
fileUrl: data.file?.link || '',
});
// Set form dengan data lama (termasuk imageId dan fileId)
apbdesState.edit.form = {
tahun: data.tahun || new Date().getFullYear(),
imageId: data.imageId || '', // ✅ Preserve old ID
fileId: data.fileId || '', // ✅ Preserve old ID
items: (data.items || []).map(...),
};
// Line ~270 - Handle reset
const handleReset = () => {
apbdesState.edit.form = {
tahun: originalData.tahun,
imageId: originalData.imageId, // ✅ Restore old ID
fileId: originalData.fileId, // ✅ Restore old ID
items: [...apbdesState.edit.form.items],
};
setPreviewImage(originalData.imageUrl || null);
setPreviewDoc(originalData.fileUrl || null);
setImageFile(null);
setDocFile(null);
toast.info('Form dikembalikan ke data awal');
};
```
**Verdict:****SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
---
### **6. Schema Design**
- ✅ Proper relations: APBDes ↔ FileStorage (image & file)
- ✅ Self-relation untuk hierarchical items (parentId → children)
- ✅ Indexing untuk performa (kode, level, apbdesId)
- ✅ Soft delete support (deletedAt, isActive)
- ✅ Nullable deletedAt yang benar (`DateTime? @default(null)`)
**Schema Example (✅ GOOD):**
```prisma
model APBDes {
id String @id @default(cuid())
tahun Int?
name String?
deskripsi String?
jumlah String?
items APBDesItem[]
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
imageId String?
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
fileId String?
deletedAt DateTime? // ✅ Nullable, no default
isActive Boolean @default(true)
}
model APBDesItem {
id String @id @default(cuid())
kode String
uraian String
anggaran Float
realisasi Float
selisih Float // ✅ Formula di komentar
persentase Float
tipe String? // ✅ Nullable untuk level 1
level Int
parentId String?
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
children APBDesItem[] @relation("APBDesItemParent")
apbdesId String
apbdes APBDes @relation(fields: [apbdesId], references: [id])
@@index([kode])
@@index([level])
@@index([apbdesId])
}
```
**Verdict:****SUDAH BENAR** - Schema design sudah solid.
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Formula Selisih - SALAH di State, BENAR di Schema/API**
**Lokasi:**
- `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts` (line 36)
- Schema komentar di `prisma/schema.prisma` (line 210)
**Masalah:**
```typescript
// ❌ SALAH di state (line 36)
function normalizeItem(item: Partial<...>): z.infer<typeof ApbdesItemSchema> {
const anggaran = item.anggaran ?? 0;
const realisasi = item.realisasi ?? 0;
// ❌ WRONG FORMULA
const selisih = anggaran - realisasi; // positif = sisa anggaran
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
return { ... };
}
```
```prisma
// ✅ BENAR di schema komentar (line 210)
model APBDesItem {
// ...
realisasi Float
selisih Float // ✅ realisasi - anggaran (komentar benar)
// ...
}
```
**Dampak:**
- **Data salah!** Selisih positif/negatif terbalik
- Jika realisasi > anggaran (over budget), seharusnya **negatif** tapi jadi **positif**
- Jika realisasi < anggaran (under budget/sisa), seharusnya **positif** tapi jadi **negatif**
- Color coding di UI (green/red) juga terbalik!
**Contoh:**
```
Anggaran: Rp 100.000.000
Realisasi: Rp 120.000.000 (over budget!)
❌ Formula sekarang: selisih = 100M - 120M = -20M (negatif)
UI show: merah (over budget) ✅ TAPI karena negatif
✅ Seharusnya: selisih = 120M - 100M = +20M (positif)
UI show: merah (over budget) ✅ Karena positif
```
**Rekomendasi:** Fix formula di state:
```typescript
// ✅ CORRECT FORMULA
const selisih = realisasi - anggaran; // positif = over budget, negatif = under budget
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Low (1 line fix)
**Impact:** **HIGH** (data integrity issue)
---
#### **2. State Management - Inconsistency Fetch Pattern**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
**Masalah:** Ada 3 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany, delete, edit.load, edit.update)
const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
const res = await ApiFetch.api.landingpage.apbdes["findMany"].get({ query });
const res = await (ApiFetch.api.landingpage.apbdes as any)["del"][id].delete();
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
// ❌ Pattern 2: fetch manual (findUnique)
const response = await fetch(`/api/landingpage/apbdes/${id}`);
const res = await response.json();
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
- Console.log debugging tertinggal di production
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
this.loading = true;
const res = await ApiFetch.api.landingpage.apbdes[id].get();
if (res.data?.success) {
this.data = res.data.data;
} else {
this.data = null;
this.error = res.data?.message || "Gagal memuat detail APBDes";
toast.error(this.error);
}
} catch (error) {
console.error("FindUnique error:", error);
this.data = null;
this.error = "Gagal memuat detail APBDes";
toast.error(this.error);
} finally {
this.loading = false;
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di findUnique)
---
#### **3. Console.log Debugging di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
**Masalah:**
```typescript
// Line ~175-177
const url = `/api/landingpage/apbdes/${id}`;
console.log("🌐 Fetching:", url); // ❌ Debug log
const response = await fetch(url);
const res = await response.json();
console.log("📦 Response:", res); // ❌ Debug log
```
**Dampak:**
- Performance impact (I/O operation)
- Security risk (expose API structure)
- Log pollution di production
- Unprofessional
**Rekomendasi:** Remove atau gunakan conditional logging:
```typescript
// ✅ Remove completely (recommended)
// Atau gunakan conditional logging
if (process.env.NODE_ENV === 'development') {
console.log("🌐 Fetching:", url);
console.log("📦 Response:", res);
}
```
**Priority:** 🔴 Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Type Safety - Any Usage di Edit Methods**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
**Masalah:**
```typescript
// Line ~215
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Line ~245
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
```
**Dampak:**
- Type safety hilang
- Autocomplete tidak bekerja
- Runtime errors tidak terdeteksi di compile time
- Refactoring sulit
**Rekomendasi:** Define typed API client:
```typescript
// Define proper types
interface APBDesAPI {
[id: string]: {
get: () => Promise<ApiResponse<APBDesData>>;
put: (data: APBDesForm) => Promise<ApiResponse<APBDesData>>;
};
del: {
[id: string]: {
delete: () => Promise<ApiResponse<void>>;
};
};
}
// Use typed client
const res = await ApiFetch.api.landingpage.apbdes[id].get();
// No more `as any`
```
**Priority:** 🟡 Medium
**Effort:** Medium (perlu setup types)
---
#### **5. Edit Form - Items Tidak Di-Restore Saat Reset**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx`
**Masalah:**
```typescript
// Line ~270-285
const handleReset = () => {
apbdesState.edit.form = {
tahun: originalData.tahun,
imageId: originalData.imageId,
fileId: originalData.fileId,
items: [...apbdesState.edit.form.items], // ⚠️ Keep MODIFIED items
};
// ...
};
```
**Issue:** Saat reset, items yang sudah di-modified (added/removed) tidak di-restore ke original. User expect reset = kembali ke data awal sepenuhnya.
**Rekomendasi:** Save original items dan restore saat reset:
```typescript
// Add to originalData state
const [originalData, setOriginalData] = useState({
tahun: 0,
imageId: '',
fileId: '',
imageUrl: '',
fileUrl: '',
items: [] as ItemForm[], // ✅ Save original items
});
// Load data
setOriginalData({
tahun: data.tahun || new Date().getFullYear(),
imageId: data.imageId || '',
fileId: data.fileId || '',
imageUrl: data.image?.link || '',
fileUrl: data.file?.link || '',
items: (data.items || []).map((item: any) => ({...})), // ✅ Save
});
// Reset
const handleReset = () => {
apbdesState.edit.form = {
tahun: originalData.tahun,
imageId: originalData.imageId,
fileId: originalData.fileId,
items: [...originalData.items], // ✅ Restore original items
};
// ...
};
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Zod Schema - Error Message Tidak Akurat**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
**Masalah:**
```typescript
// Line ~10
const ApbdesItemSchema = z.object({
kode: z.string().min(1, "Kode wajib diisi"), // ✅ OK
uraian: z.string().min(1, "Uraian wajib diisi"), // ✅ OK
anggaran: z.number().min(0), // ⚠️ No custom message
realisasi: z.number().min(0), // ⚠️ No custom message
// ...
});
// Line ~17
const ApbdesFormSchema = z.object({
tahun: z.number().int().min(2000, "Tahun tidak valid"), // ⚠️ Generic
imageId: z.string().min(1, "Gambar wajib diunggah"), // ✅ OK
fileId: z.string().min(1, "File wajib diunggah"), // ✅ OK
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"), // ✅ OK
});
```
**Dampak:** Error messages tidak konsisten, beberapa generic beberapa spesifik.
**Rekomendasi:** Standardisasi error messages:
```typescript
const ApbdesItemSchema = z.object({
kode: z.string().min(1, "Kode wajib diisi"),
uraian: z.string().min(1, "Uraian wajib diisi"),
anggaran: z.number().min(0, "Anggaran tidak boleh negatif"),
realisasi: z.number().min(0, "Realisasi tidak boleh negatif"),
selisih: z.number(),
persentase: z.number(),
level: z.number().int().min(1).max(3, "Level harus antara 1-3"),
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
});
const ApbdesFormSchema = z.object({
tahun: z.number().int().min(2000, "Tahun minimal 2000").max(2100, "Tahun maksimal 2100"),
imageId: z.string().min(1, "Gambar wajib diunggah"),
fileId: z.string().min(1, "Dokumen wajib diunggah"),
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"),
});
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **7. Console.log di Production (UI Components)**
**Lokasi:** Multiple UI files
**Masalah:**
```typescript
// edit/page.tsx - Line ~220
console.error('Update error:', err);
// create/page.tsx - Line ~120
console.error("Gagal submit:", error);
// detail/page.tsx - Line ~40
console.error('Error loading APBDes:', error);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error('Update error:', err);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Mobile Layout - Title Order Inconsistency**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~170 (Mobile)
<Title order={2} size="lg" lh={1.2}>
Daftar APBDes
</Title>
// Line ~70 (Desktop - inside Paper)
<Title order={4} size="lg" lh={1.2}>
Daftar APBDes
</Title>
```
**Issue:** Mobile pakai `order={2}` (heading besar), desktop `order={4}`. Seharusnya konsisten.
**Rekomendasi:** Samakan:
```typescript
<Title order={4} size="lg" lh={1.2}>
Daftar APBDes
</Title>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Search Placeholder Tidak Spesifik**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~30
<HeaderSearch
title="APBDes"
placeholder="Cari APBDes..." // ⚠️ Generic
// ...
/>
```
**Rekomendasi:** Lebih spesifik:
```typescript
placeholder='Cari nama atau tahun APBDes...'
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Duplicate Comment**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
**Masalah:**
```typescript
// Line ~28-29
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
// ^ Duplicate line
```
**Priority:** 🟢 Low
**Effort:** Low (remove duplicate)
---
#### **11. Inconsistent Button Label**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// create/page.tsx - Line ~270
<Button ...>Simpan</Button>
// edit/page.tsx - Line ~340
<Button ...>Simpan Perubahan</Button>
// Should be consistent: "Simpan" atau "Simpan Perubahan"
```
**Rekomendasi:** Standardisasi:
```typescript
// Create: "Simpan"
// Edit: "Simpan Perubahan" (lebih descriptive untuk edit)
// OR both: "Simpan"
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Missing Search Feature in Pagination**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~250
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ⚠️ Missing search parameter
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang.
**Rekomendasi:** Include search:
```typescript
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **13. Edit Page - Document Max Size Inconsistency**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~230 (Image)
maxSize={5 * 1024 ** 2} // 5MB
// Line ~250 (Document)
maxSize={10 * 1024 ** 2} // 10MB
```
**Issue:** Create page maksimal 5MB untuk semua file, edit page 10MB untuk dokumen. Inconsistent.
**Rekomendasi:** Samakan (prefer 5MB untuk consistency):
```typescript
maxSize={5 * 1024 ** 2} // 5MB for both
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Formula selisih SALAH** | State | **CRITICAL** | Low | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P1 | Console.log debugging in production | State | Medium | Low | Should fix |
| 🟡 M | Type safety (any usage) | State | Low | Medium | Optional |
| 🟡 M | Items tidak di-restore saat reset | Edit UI | Medium | Low | Should fix |
| 🟡 M | Zod schema error messages | State | Low | Low | Optional |
| 🟢 L | Console.log in UI components | UI | Low | Low | Optional |
| 🟢 L | Mobile title order inconsistency | List UI | Low | Low | Optional |
| 🟢 L | Search placeholder tidak spesifik | List UI | Low | Low | Optional |
| 🟢 L | Duplicate comment | State | Low | Low | Optional |
| 🟢 L | Inconsistent button label | UI | Low | Low | Optional |
| 🟢 L | Missing search in pagination | List UI | Low | Low | Should fix |
| 🟢 L | Document max size inconsistency | Edit UI | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (7/10)**
**Strengths:**
1. UI/UX konsisten & responsive
2. File upload handling solid (dual upload: image + document)
3. Form validation dengan Zod schema
4. State management terstruktur (Valtio)
5. **Edit form reset sudah benar** (original data tracking untuk files)
6. Complex feature: hierarchical items dengan level & tipe
7. Schema design solid (proper relations, indexing, soft delete)
8. Modal konfirmasi hapus untuk user safety
**Critical Issues:**
1. **FORMULA SELISIH SALAH** - Data integrity issue (CRITICAL)
2. Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. Console.log debugging tertinggal di production
**Areas for Improvement:**
1. **Fix formula selisih** (realisasi - anggaran, bukan anggaran - realisasi)
2. **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. **Remove console.log** debugging dari production code
4. **Save & restore original items** saat reset form di edit page
5. **Improve type safety** dengan remove `as any` usage
6. **Standardisasi error messages** di Zod schema
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix formula selisih** di state (line 36) - 5 menit fix
2. **🔴 HIGH:** Refactor findUnique ke ApiFetch - 30 menit
3. **🔴 HIGH:** Remove console.log debugging - 10 menit
4. **🟡 MEDIUM:** Save & restore original items - 30 menit
5. **🟡 MEDIUM:** Improve type safety - 1-2 jam
6. **🟢 LOW:** Polish minor issues - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | APBDes | Notes |
|--------|--------|-------------------|-----------|--------|-------|
| Fetch Pattern | Mixed | Mixed | Mixed | Mixed | All perlu refactor |
| Loading State | Some missing | Some missing | Missing | Good | APBDes paling baik |
| Edit Form Reset | Good | Good | Good | Good | All consistent |
| Type Safety | Some `any` | Some `any` | Some `any` | Some `any` | Same issue |
| File Upload | Images | Documents | Images | **Dual** | APBDes paling complex |
| Error Handling | Good | Good (better) | Good | Good | Consistent |
| Schema Design | Good | deletedAt issue | deletedAt issue | **Best** | APBDes paling solid |
| **Data Integrity** | Good | Good | Good | **Formula WRONG** | **APBDes CRITICAL issue** |
| Complexity | Low | Medium | Low | **High** | APBDes items hierarchy |
---
## 🎯 UNIQUE FEATURES OF APBDes MODULE
**Most Complex Module So Far:**
1. **Dual file upload** (gambar + dokumen) - unique to APBDes
2. **Hierarchical items** dengan 3 level - unique to APBDes
3. **Auto-calculation** (selisih & persentase) - unique to APBDes
4. **Type classification** (pendapatan, belanja, pembiayaan) - unique to APBDes
5. **Dynamic item management** (add/remove) - unique to APBDes
**Best Practices:**
1. Schema design paling solid (deletedAt nullable, proper indexing)
2. Edit form reset paling comprehensive (preserve files & items)
3. Validation paling thorough (Zod schema untuk items)
**Biggest Issue:**
1. **Formula selisih SALAH** - critical data integrity issue yang tidak ada di modul lain
---
**Catatan:** Secara keseluruhan, modul APBDes adalah **paling complex dan paling solid** dibanding modul lain yang sudah di-QC. Namun, ada **1 CRITICAL BUG** (formula selisih) yang harus **SEGERA DIPERBAIKI** karena menyangkut integritas data. Setelah fix critical issue, module ini production-ready dengan beberapa improvement minor yang bisa dilakukan secara incremental.
**Priority Action:**
```
🔴 FIX INI SEKARANG JUGA (5 MENIT):
File: src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
Line: 36
Change: const selisih = anggaran - realisasi;
To: const selisih = realisasi - anggaran;
```

View File

@@ -0,0 +1,639 @@
# QC Summary - Desa Anti Korupsi Module
**Scope:** List Desa Anti Korupsi, Kategori Desa Anti Korupsi
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Module | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| List Desa Anti Korupsi | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
| Kategori Desa Anti Korupsi | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK (COMMON)
### **1. UI/UX Consistency**
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dengan Skeleton
- ✅ Search dengan debounce (1000ms)
- ✅ Pagination konsisten
- ✅ Empty state handling yang informatif
- ✅ Modal konfirmasi hapus
### **2. File Upload Handling** (Desa Anti Korupsi)
- ✅ Dropzone dengan preview iframe untuk dokumen
- ✅ Validasi format dokumen (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
- ✅ Validasi ukuran file (max 5MB)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
### **3. Form Validation**
- ✅ Zod schema untuk validasi typed
- ✅ isFormValid() check sebelum submit
- ✅ Error toast dengan pesan spesifik
- ✅ Button disabled saat invalid/loading
### **4. CRUD Operations**
- ✅ Create dengan upload file
- ✅ FindMany dengan pagination & search
- ✅ FindUnique untuk detail
- ✅ Delete dengan soft delete
- ✅ Update dengan file replacement
### **5. Error Handling**
- ✅ Try-catch di semua async operation
- ✅ Toast error dengan pesan user-friendly
- ✅ Console.error untuk debugging
- ✅ Response cloning untuk error handling yang lebih baik (di kategori update)
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Edit Form - File Lama Tidak Tersimpan Saat Reset**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx`
**Masalah:**
```typescript
// Line ~70 - Load data
const data = await desaAntiKorupsiState.edit.load(id);
setFormData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId, // ✅ Sudah benar
});
setOriginalData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId,
fileUrl: data.file?.link || "", // ✅ Sudah benar
});
// Line ~130 - Handle reset
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
kategoriId: originalData.kategoriId,
fileId: originalData.fileId, // ✅ Sudah benar
});
setPreviewFile(originalData.fileUrl || null); // ✅ Sudah benar
setFile(null); // ✅ Sudah benar
};
```
**Status:****SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
**Verdict:** Tidak ada action needed.
---
#### **2. State Management - Inconsistency Fetch Pattern**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create operations)
const res = await ApiFetch.api.landingpage.desaantikorupsi["create"].post({...});
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
const response = await fetch(`/api/landingpage/desaantikorupsi/del/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
const res = await ApiFetch.api.landingpage.desaantikorupsi["create"].post(data);
const res = await ApiFetch.api.landingpage.desaantikorupsi[id].get();
const res = await ApiFetch.api.landingpage.desaantikorupsi[id].put(data);
const res = await ApiFetch.api.landingpage.desaantikorupsi["del"][id].delete();
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di semua state methods)
---
#### **3. findUnique State - Tidak Ada Loading State Management**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
**Masalah:**
```typescript
// Line ~97 - desaAntikorupsi.findUnique.load()
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
if (res.ok) {
const data = await res.json();
desaAntikorupsi.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
desaAntikorupsi.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
desaAntikorupsi.findUnique.data = null;
}
// ❌ MISSING: finally block untuk stop loading
}
```
**Dampak:** UI mungkin stuck di loading state jika ada error.
**Rekomendasi:** Tambahkan loading state dan finally block:
```typescript
async load(id: string) {
try {
desaAntikorupsi.findUnique.loading = true; // ✅ Start loading
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
if (res.ok) {
const data = await res.json();
desaAntikorupsi.findUnique.data = data.data ?? null;
}
} catch (error) {
console.error("Error:", error);
} finally {
desaAntikorupsi.findUnique.loading = false; // ✅ Stop loading
}
}
```
**Priority:** 🔴 Medium
**Effort:** Low
---
#### **4. Kategori Edit - Response Cloning Overkill**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
**Masalah:**
```typescript
// Line ~370 - kategoriDesaAntiKorupsi.edit.update()
async update() {
// ...
const response = await fetch(...);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
// ...
} catch (error) {
// If JSON parsing fails, try to get the response text
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
// ...
}
}
}
```
**Analysis:**
-**GOOD:** Error handling sangat thorough
- ⚠️ **OVERKILL:** Untuk production API yang stable, ini berlebihan
- ⚠️ **INCONSISTENT:** Module lain tidak punya error handling se-detail ini
**Rekomendasi:** Simplify untuk consistency:
```typescript
async update() {
try {
kategoriDesaAntiKorupsi.edit.loading = true;
const response = await fetch(`/api/landingpage/kategoridak/${this.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: this.form.name }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result?.message || `HTTP ${response.status}`);
}
if (result.success) {
toast.success(result.message || "Berhasil update");
await kategoriDesaAntiKorupsi.findMany.load();
return true;
}
throw new Error(result.message || "Gagal update");
} catch (error) {
console.error("Error updating:", error);
toast.error(error instanceof Error ? error.message : "Gagal update");
return false;
} finally {
kategoriDesaAntiKorupsi.edit.loading = false;
}
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟡 MEDIUM**
#### **5. HTML Injection Risk - dangerouslySetInnerHTML**
**Lokasi:**
- `list-desa-anti-korupsi/[id]/page.tsx` (line ~105)
- `list-desa-anti-korupsi/create/page.tsx` (CreateEditor component)
- `list-desa-anti-korupsi/[id]/edit/page.tsx` (EditEditor component)
**Masalah:**
```typescript
// ❌ Direct HTML render tanpa sanitization
<Box
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.6 }}
/>
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
- Security vulnerability
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedHtml = DOMPurify.sanitize(data.deskripsi);
<Box
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
// ...
/>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
**Priority:** 🟡 Medium (**Security concern**)
**Effort:** Low
---
#### **6. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
**Masalah:**
```typescript
// Line ~60
data: null as any[] | null, // ❌ Using 'any'
// Line ~280
data: null as any[] | null, // ❌ Using 'any'
// Line ~97
data: null as Prisma.DesaAntiKorupsiGetPayload<{...}> | null, // ✅ Typed
// Line ~310
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{...}> | null, // ✅ Typed
```
**Rekomendasi:** Gunakan typed data consistently:
```typescript
// desaAntikorupsi.findMany
data: null as Prisma.DesaAntiKorupsiGetPayload<{
include: { kategori: true; file: true };
}>[] | null,
// kategoriDesaAntiKorupsi.findMany
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{}>[] | null,
```
**Priority:** 🟡 Medium
**Effort:** Medium (perlu update semua reference)
---
#### **7. Console.log di Production**
**Lokasi:** Multiple places di state file
**Masalah:**
```typescript
// Line ~50
console.log(error);
toast.error("Gagal menambahkan data");
// Line ~85
console.error("Failed to load media sosial:", res.data?.message);
// Line ~91
console.error("Error loading media sosial:", error);
// Line ~110
console.error("Failed to fetch data", res.status, res.statusText);
// Line ~114
console.error("Error fetching data:", error);
// ... dan banyak lagi
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
Atau gunakan logging library (winston, pino, dll) dengan levels yang jelas.
**Priority:** 🟡 Low
**Effort:** Low
---
#### **8. Error Message Tidak Konsisten**
**Lokasi:** Multiple places
**Masalah:**
```typescript
// Create - Line ~40
return toast.error("Gagal menambahkan data");
// Create - Line ~42
toast.error("Gagal menambahkan data");
// Delete - Line ~140
toast.error("Terjadi kesalahan saat menghapus desa anti korupsi");
// Edit - Line ~190
toast.error("Gagal memuat data");
// Edit update - Line ~240
toast.error("Gagal mengupdate desa anti korupsi");
```
**Rekomendasi:** Standardisasi error messages:
```typescript
// Pattern: "[Action] [resource] gagal"
toast.error("Menambahkan data gagal");
toast.error("Menghapus data gagal");
toast.error("Memuat data gagal");
toast.error("Memperbarui data gagal");
// Atau lebih spesifik dengan context
toast.error("Gagal menambahkan data Desa Anti Korupsi");
toast.error("Gagal menghapus Kategori Desa Anti Korupsi");
```
**Priority:** 🟢 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **9. Placeholder Search Tidak Spesifik**
**Lokasi:**
- `list-desa-anti-korupsi/page.tsx`: `placeholder="Cari nama program atau kategori..."` ✅ Spesifik
- `kategori-desa-anti-korupsi/page.tsx`: `placeholder='pencarian'` ❌ Terlalu generic
**Rekomendasi:**
```typescript
// Kategori page
placeholder="Cari nama kategori..."
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Alert vs Toast**
**Lokasi:** `kategori-desa-anti-korupsi/create/page.tsx`
**Masalah:**
```typescript
// Line ~37
if (!stateKategori.create.form.name) {
return alert('Nama kategori harus diisi'); // ❌ Using alert()
}
```
**Rekomendasi:** Gunakan toast untuk consistency:
```typescript
if (!stateKategori.create.form.name) {
return toast.warn('Nama kategori harus diisi'); // ✅ Using toast
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Component Name Mismatch**
**Lokasi:** `list-desa-anti-korupsi/[id]/page.tsx`
**Masalah:**
```typescript
// Line ~17
export default function DetailKegiatanDesa() { // ❌ Wrong name
// ...
}
```
**Rekomendasi:** Rename ke yang sesuai:
```typescript
export default function DetailDesaAntiKorupsi() { // ✅ Correct name
// ...
}
```
**Priority:** 🟢 Low
**Effort:** Low (hanya rename)
---
#### **12. Duplicate Error Logging**
**Lokasi:** `list-desa-anti-korupsi/[id]/edit/page.tsx`
**Masalah:**
```typescript
// Line ~87
} catch (err) {
console.error(err); // ❌ Duplicate logging
toast.error('Gagal memuat data Desa Anti Korupsi');
}
```
**Rekomendasi:** Cukup satu logging yang informatif:
```typescript
} catch (err) {
console.error('Failed to load Desa Anti Korupsi:', err);
toast.error('Gagal memuat data Desa Anti Korupsi');
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **13. Comment Typo**
**Lokasi:** `kategori-desa-anti-korupsi/[id]/edit/page.tsx`
**Masalah:**
```typescript
// Line ~20
// 🧠 Ambil proxy asli (bisa ditulis) & snapshot (buat render)
const stateKategori = korupsiState.kategoriDesaAntiKorupsi;
const snapshotKategori = useProxy(stateKategori);
// ❌ snapshotKategori declared but never used
```
**Rekomendasi:** Remove unused variable:
```typescript
const stateKategori = korupsiState.kategoriDesaAntiKorupsi;
// const snapshotKategori = useProxy(stateKategori); // ❌ Remove
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **14. Schema - deletedAt Default Value**
**Lokasi:** `prisma/schema.prisma`
**Masalah:**
```prisma
model DesaAntiKorupsi {
// ...
deletedAt DateTime @default(now()) // ❌ Always has default value
isActive Boolean @default(true)
}
```
**Issue:** `deletedAt @default(now())` berarti setiap record baru langsung punya `deletedAt` value, yang bisa membingungkan untuk soft delete logic.
**Rekomendasi:**
```prisma
model DesaAntiKorupsi {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🟢 Medium (potential logic issue)
**Effort:** Medium (perlu migration)
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P0 | Missing loading state in findUnique | State | Medium | Low | Perlu fix |
| 🟡 M | HTML injection risk | UI | **High (Security)** | Low | **Should fix** |
| 🟡 M | Type safety (any usage) | State | Low | Medium | Optional |
| 🟡 M | Response cloning overkill | State (Kategori) | Low | Low | Optional |
| 🟢 L | Console.log in production | State | Low | Low | Optional |
| 🟢 L | Error message inconsistency | State | Low | Low | Optional |
| 🟢 L | Placeholder tidak spesifik | Kategori UI | Low | Low | Optional |
| 🟢 L | Alert vs Toast | Kategori Create | Low | Low | Optional |
| 🟢 L | Component name mismatch | Detail page | Low | Low | Optional |
| 🟢 L | Duplicate error logging | Edit page | Low | Low | Optional |
| 🟢 L | Unused variable | Kategori Edit | Low | Low | Optional |
| 🟢 M | deletedAt default value | Schema | Medium | Medium | Should fix |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (7.5/10)**
**Strengths:**
1. ✅ UI/UX konsisten & responsive
2. ✅ File upload handling solid (iframe preview untuk dokumen)
3. ✅ Form validation dengan Zod schema
4. ✅ State management terstruktur (Valtio)
5. ✅ Error handling comprehensive (terutama di kategori update)
6.**Edit form reset sudah benar** (original data tracking)
7. ✅ Modal konfirmasi hapus untuk user safety
**Areas for Improvement:**
1. ⚠️ **Security:** HTML injection di deskripsi (prioritas)
2. ⚠️ **Consistency:** Fetch method pattern (ApiFetch vs fetch manual)
3. ⚠️ **Loading States:** findUnique tidak ada loading state management
4. ⚠️ **Type Safety:** Reduce `any` usage, gunakan Prisma types
5. ⚠️ **Schema:** deletedAt default value bisa menyebabkan logic issue
**Recommended Next Steps:**
1. **Fix HTML injection** dengan DOMPurify atau backend validation
2. **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. **Add loading state** di findUnique operations
4. **Fix deletedAt schema** untuk soft delete yang benar
5. **Optional:** Improve type safety dengan remove `any`
---
## 📈 COMPARISON WITH OTHER MODULES
| Aspect | Profil Module | Desa Anti Korupsi | Notes |
|--------|--------------|-------------------|-------|
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | Both perlu refactor |
| Loading State | ⚠️ Some missing | ⚠️ Some missing | Same issue |
| Edit Form Reset | ✅ Good | ✅ Good | Consistent |
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
| HTML Injection | ⚠️ Present | ⚠️ Present | Both need fix |
| File Upload | ✅ Images | ✅ Documents | Different use case |
| Error Handling | ✅ Good | ✅ Good (better) | DAK more thorough |
---
**Catatan:** Secara keseluruhan, modul Desa Anti Korupsi sudah **production-ready** dengan beberapa improvements yang bisa dilakukan secara incremental. Module ini memiliki error handling yang lebih thorough dibanding module Profil, terutama di kategori update operation.

View File

@@ -0,0 +1,875 @@
# QC Summary - Prestasi Desa Module
**Scope:** List Prestasi Desa, Kategori Prestasi Desa, Create, Edit, Detail
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Prestasi Desa | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
| Kategori Prestasi | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Consistency**
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dengan Skeleton
- ✅ Search dengan debounce (1000ms)
- ✅ Pagination konsisten
- ✅ Empty state handling yang informatif
- ✅ Modal konfirmasi hapus
### **2. File Upload Handling**
- ✅ Dropzone dengan preview image
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
- ✅ Validasi ukuran file (max 5MB)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
- ✅ Preview dengan max height yang proper
### **3. Form Validation**
- ✅ Zod schema untuk validasi typed
- ✅ isFormValid() check sebelum submit
- ✅ Error toast dengan pesan spesifik
- ✅ Button disabled saat invalid/loading
### **4. CRUD Operations**
- ✅ Create dengan upload file
- ✅ FindMany dengan pagination & search
- ✅ FindUnique untuk detail
- ✅ Delete dengan hard delete (via Prisma)
- ✅ Update dengan file replacement
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Preview image dari data lama
- ✅ Reset form mengembalikan ke data original
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~70-95
const data = await editState.edit.load(id);
setOriginalData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
imageId: data.imageId,
imageUrl: data.image?.link || "",
});
setFormData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
imageId: data.imageId,
});
if (data.image?.link) setPreviewFile(data.image.link);
// Line ~105 - Handle reset
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
kategoriId: originalData.kategoriId,
imageId: originalData.imageId,
});
setPreviewFile(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
```
**Verdict:****SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
---
### **6. State Management - Good Practices**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
- ✅ Reset function untuk cleanup
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~70-95
load: async (page = 1, limit = 10, search = "") => {
prestasiDesa.findMany.loading = true; // ✅ Start loading
prestasiDesa.findMany.page = page;
prestasiDesa.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
prestasiDesa.findMany.data = res.data.data ?? [];
prestasiDesa.findMany.totalPages = res.data.totalPages ?? 1;
} else {
prestasiDesa.findMany.data = [];
prestasiDesa.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch prestasi desa paginated:", err);
prestasiDesa.findMany.data = [];
prestasiDesa.findMany.totalPages = 1;
} finally {
prestasiDesa.findMany.loading = false; // ✅ Stop loading
}
};
```
**Verdict:****SUDAH BENAR** - Loading state management sudah proper.
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH**
**Lokasi:** `prisma/schema.prisma` (line 239-240)
**Masalah:**
```prisma
model PrestasiDesa {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
model KategoriPrestasiDesa {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
**Contoh Issue:**
```prisma
// Record baru dibuat
CREATE PrestasiDesa {
name: "Prestasi 1",
// deletedAt otomatis ter-set ke now() ❌
// isActive: true ✅
}
// Query untuk data aktif (seharusnya return data ini)
prisma.prestasiDesa.findMany({
where: { deletedAt: null, isActive: true }
})
// ❌ Return kosong! Karena deletedAt sudah ter-set
```
**Rekomendasi:** Fix schema:
```prisma
model PrestasiDesa {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
model KategoriPrestasiDesa {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. State Management - Inconsistency Fetch Pattern**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany)
const res = await ApiFetch.api.landingpage.prestasidesa["create"].post({...});
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({query});
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
const response = await fetch(`/api/landingpage/prestasidesa/del/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
prestasiDesa.edit.loading = true;
const res = await ApiFetch.api.landingpage.prestasidesa[id].get();
if (res.data?.success) {
const data = res.data.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading prestasi desa:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
} finally {
prestasiDesa.edit.loading = false;
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di findUnique, edit, delete methods)
---
#### **3. findUnique State - Tidak Ada Loading State Management**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
**Masalah:**
```typescript
// Line ~110 - prestasiDesa.findUnique.load()
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
if (res.ok) {
const data = await res.json();
prestasiDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
prestasiDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
prestasiDesa.findUnique.data = null;
}
// ❌ MISSING: finally block untuk stop loading
// ❌ MISSING: loading state initialization
}
```
**Dampak:** UI mungkin stuck di loading state jika ada error.
**Rekomendasi:** Tambahkan loading state dan finally block:
```typescript
async load(id: string) {
try {
prestasiDesa.findUnique.loading = true; // ✅ Start loading
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
if (res.ok) {
const data = await res.json();
prestasiDesa.findUnique.data = data.data ?? null;
}
} catch (error) {
console.error("Error:", error);
} finally {
prestasiDesa.findUnique.loading = false; // ✅ Stop loading
}
}
```
**Priority:** 🔴 Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. HTML Injection Risk - dangerouslySetInnerHTML**
**Lokasi:**
- `list-prestasi-desa/page.tsx` (line ~90, 145)
- `list-prestasi-desa/[id]/page.tsx` (line ~85)
- `list-prestasi-desa/create/page.tsx` (CreateEditor component)
- `list-prestasi-desa/[id]/edit/page.tsx` (EditEditor component)
**Masalah:**
```typescript
// ❌ Direct HTML render tanpa sanitization
<Text
lineClamp={1}
fz="md"
c="dimmed"
lh={1.5}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
- Security vulnerability
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedHtml = DOMPurify.sanitize(item.deskripsi);
<Text
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
// ...
/>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
**Priority:** 🟡 Medium (**Security concern**)
**Effort:** Low
---
#### **5. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
**Masalah:**
```typescript
// Line ~73
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;
// Line ~270
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;
```
**Rekomendasi:** Gunakan typed query:
```typescript
// Define type
interface FindManyQuery {
page: number | string;
limit: number | string;
search?: string;
}
// Use typed query
const query: FindManyQuery = { page, limit };
if (search) query.search = search;
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Console.log di Production**
**Lokasi:** Multiple places di state file
**Masalah:**
```typescript
// Line ~48
console.log(error);
toast.error("Gagal menambahkan data");
// Line ~120
console.error("Failed to fetch data", res.status, res.statusText);
// Line ~124
console.error("Error fetching data:", error);
// Line ~300
console.log(error);
toast.error("Gagal menambahkan data");
// ... dan banyak lagi
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **7. Error Message Tidak Konsisten**
**Lokasi:** Multiple places
**Masalah:**
```typescript
// Create - Line ~46
return toast.error("Gagal menambahkan data");
// Create - Line ~48
toast.error("Gagal menambahkan data");
// Delete - Line ~150
toast.error("Terjadi kesalahan saat menghapus prestasi desa");
// Edit - Line ~200
toast.error("Gagal memuat data");
// Edit update - Line ~240
toast.error("Gagal mengupdate prestasi desa");
// Toast success - Line ~235
toast.success("Berhasil update prestasi desa");
```
**Issue:**
- Inconsistent capitalization
- Mixed patterns ("Gagal menambahkan" vs "Terjadi kesalahan")
- Generic messages
**Rekomendasi:** Standardisasi error messages:
```typescript
// Pattern: "[Action] [resource] gagal" dengan proper casing
toast.error("Menambahkan data Prestasi Desa gagal");
toast.error("Menghapus data Prestasi Desa gagal");
toast.error("Memuat data Prestasi Desa gagal");
toast.error("Memperbarui data Prestasi Desa gagal");
// Atau lebih spesifik dengan context
toast.error("Gagal menambahkan data Prestasi Desa");
toast.error("Gagal menghapus Prestasi Desa");
toast.success("Berhasil memperbarui Prestasi Desa");
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **8. Zod Schema - Error Message Tidak Akurat**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
**Masalah:**
```typescript
// Line ~8
const templateprestasiDesaForm = z.object({
name: z.string().min(1, "Judul minimal 1 karakter"), // ⚠️ "Judul" instead of "Nama"
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), // ✅ OK
imageId: z.string().min(1, "File minimal 1"), // ⚠️ Generic
kategoriId: z.string().min(1, "Kategori minimal 1 karakter"), // ⚠️ "Kategori" instead of "Kategori Prestasi"
});
```
**Dampak:** User confusion saat validasi error muncul.
**Rekomendasi:** Fix error messages:
```typescript
const templateprestasiDesaForm = z.object({
name: z.string().min(1, "Nama prestasi wajib diisi"),
deskripsi: z.string().min(1, "Deskripsi prestasi wajib diisi"),
imageId: z.string().min(1, "Gambar prestasi wajib diunggah"),
kategoriId: z.string().min(1, "Kategori prestasi wajib dipilih"),
});
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **9. Component Name Mismatch**
**Lokasi:** `list-prestasi-desa/page.tsx`
**Masalah:**
```typescript
// Line ~11
function ListPrestasiDesa() {
// ...
}
// Line ~27
function ListPrestasi({ search }: { search: string }) {
// ...
}
// ⚠️ Function name tidak konsisten dengan file name
```
**Rekomendasi:** Rename ke yang lebih descriptive:
```typescript
function ListPrestasiDesaPage() {
// ...
}
function ListPrestasiDesaTable({ search }: { search: string }) {
// ...
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Pagination onChange Tidak Include Search**
**Lokasi:** `list-prestasi-desa/page.tsx`
**Masalah:**
```typescript
// Line ~170
<Pagination
value={page}
onChange={load} // ⚠️ Hanya pass page number
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang karena `load` dipanggil hanya dengan page number.
**Rekomendasi:** Include search dan limit:
```typescript
<Pagination
value={page}
onChange={(newPage) => load(newPage, 10, debouncedSearch)} // ✅ Include all params
total={totalPages}
// ...
/>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Mobile Pagination - load Function Tidak Lengkap**
**Lokasi:** `kategori-prestasi-desa/page.tsx`
**Masalah:**
```typescript
// Line ~170 (Desktop)
onChange={(newPage) => load(newPage)} // ⚠️ Missing limit & search
// Line ~200 (Mobile)
onChange={(newPage) => load(newPage)} // ⚠️ Missing limit & search
```
**Rekomendasi:** Include all params:
```typescript
onChange={(newPage) => load(newPage, 10, debouncedSearch)}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Duplicate Error Logging**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// edit/page.tsx - Line ~100
} catch (error) {
console.error('Error loading prestasi desa:', error); // ❌ Duplicate
toast.error('Gagal memuat data prestasi desa');
}
// edit/page.tsx - Line ~130
} catch (error) {
console.error('Error updating prestasi desa:', error); // ❌ Duplicate
toast.error('Terjadi kesalahan saat memperbarui prestasi desa');
}
```
**Rekomendasi:** Cukup satu logging yang informatif:
```typescript
} catch (error) {
console.error('Failed to load Prestasi Desa:', err);
toast.error('Gagal memuat data Prestasi Desa');
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **13. Inconsistent Button Label**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// create/page.tsx - Line ~200
<Button ...>Reset</Button>
// edit/page.tsx - Line ~180
<Button ...>Batal</Button>
// Should be consistent: "Reset" atau "Batal"
```
**Rekomendasi:** Standardisasi:
```typescript
// Create: "Reset"
// Edit: "Batal" (lebih descriptive untuk cancel changes)
// OR both: "Reset" / "Batal"
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **14. Search Placeholder Tidak Spesifik**
**Lokasi:**
- `list-prestasi-desa/page.tsx`: `placeholder='Cari nama prestasi...'` ✅ OK
- `kategori-prestasi-desa/page.tsx`: `placeholder='Cari kategori prestasi...'` ✅ OK
**Verdict:****SUDAH BENAR** - Placeholder sudah spesifik.
**Priority:** 🟢 None
**Effort:** None
---
#### **15. Response Clone Overkill di Kategori Edit**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
**Masalah:**
```typescript
// Line ~370 - kategoriPrestasi.edit.update()
const response = await fetch(...);
const responseClone = response.clone();
try {
const result = await response.json();
// ...
} catch (error) {
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
```
**Analysis:**
-**GOOD:** Error handling sangat thorough
- ⚠️ **OVERKILL:** Untuk production API yang stable, ini berlebihan
- ⚠️ **INCONSISTENT:** Module lain tidak punya error handling se-detail ini
**Rekomendasi:** Simplify untuk consistency:
```typescript
async update() {
try {
kategoriPrestasi.edit.loading = true;
const response = await fetch(`/api/landingpage/kategoriprestasi/${this.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: this.form.name }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result?.message || `HTTP ${response.status}`);
}
if (result.success) {
toast.success(result.message || "Berhasil update");
await kategoriPrestasi.findMany.load();
return true;
}
throw new Error(result.message || "Gagal update");
} catch (error) {
console.error("Error updating:", error);
toast.error(error instanceof Error ? error.message : "Gagal update");
return false;
} finally {
kategoriPrestasi.edit.loading = false;
}
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P1 | Missing loading state in findUnique | State | Medium | Low | Perlu fix |
| 🟡 M | HTML injection risk | UI | **High (Security)** | Low | **Should fix** |
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Error message inconsistency | State/UI | Low | Low | Optional |
| 🟡 M | Zod schema error messages | State | Low | Low | Should fix |
| 🟢 L | Component name mismatch | List UI | Low | Low | Optional |
| 🟢 L | Pagination missing search param | List UI | Low | Low | Should fix |
| 🟢 L | Duplicate error logging | UI | Low | Low | Optional |
| 🟢 L | Inconsistent button label | UI | Low | Low | Optional |
| 🟢 L | Response clone overkill | State (Kategori) | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (7/10)**
**Strengths:**
1. ✅ UI/UX konsisten & responsive
2. ✅ File upload handling solid
3. ✅ Form validation dengan Zod schema
4. ✅ State management terstruktur (Valtio)
5.**Edit form reset sudah benar** (original data tracking)
6. ✅ Loading state management di findMany (dengan finally block)
7. ✅ Modal konfirmasi hapus untuk user safety
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. ⚠️ findUnique tidak ada loading state management
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. ⚠️ **Add loading state** di findUnique operations
4. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
5. ⚠️ **Improve type safety** dengan remove `any` usage
6. ⚠️ **Standardisasi error messages** di Zod schema dan toast
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH:** Refactor findUnique, edit, delete ke ApiFetch - 1 jam
3. **🔴 HIGH:** Add loading state di findUnique - 15 menit
4. **🟡 MEDIUM:** Fix HTML injection dengan DOMPurify - 30 menit
5. **🟡 MEDIUM:** Improve type safety - 30 menit
6. **🟢 LOW:** Polish minor issues - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | APBDes | Prestasi Desa | Notes |
|--------|--------|-------------------|-----------|--------|---------------|-------|
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | ✅ Good | ⚠️ findUnique missing | Similar issue |
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | ✅ Good | ✅ Good | All consistent |
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
| File Upload | ✅ Images | ✅ Documents | ✅ Images | ✅ Dual | ✅ Images | APBDes paling complex |
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | ✅ Good | ✅ Good | Consistent |
| **Schema deletedAt** | ⚠️ Issue | ⚠️ Issue | ⚠️ Issue | ✅ Good | ❌ **WRONG** | **Prestasi CRITICAL** |
| HTML Injection | ⚠️ Present | ⚠️ Present | N/A | N/A | ⚠️ Present | Security concern |
| Complexity | Low | Medium | Low | **High** | Medium | APBDes paling complex |
---
## 🎯 UNIQUE FEATURES OF PRESTASI DESA MODULE
**Standard Complexity:**
1. **Single file upload** (gambar) - similar to SDGs, Profil
2. **Kategori relation** - similar to Desa Anti Korupsi
3. **Rich text editor** (deskripsi) - similar to Desa Anti Korupsi
**Best Practices:**
1. ✅ Loading state management di findMany (dengan finally block) - better than SDGs
2. ✅ Edit form reset comprehensive (preserve all fields)
3. ✅ Proper typing di findMany (Prisma types)
**Critical Issues:**
1.**Schema deletedAt SALAH** - sama seperti SDGs & Desa Anti Korupsi, tapi APBDes sudah benar
---
**Catatan:** Secara keseluruhan, modul Prestasi Desa sudah **production-ready** dengan beberapa improvements yang bisa dilakukan secara incremental. Module ini memiliki struktur yang mirip dengan modul Desa Anti Korupsi (kategori relation, rich text editor, file upload).
**Unique Issues:**
1. Schema deletedAt default value yang salah (sama seperti SDGs & Desa Anti Korupsi)
2. HTML injection risk di deskripsi (sama seperti Desa Anti Korupsi)
3. Fetch pattern inconsistency (sama seperti semua modul lain)
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 239-240, 248-249
model PrestasiDesa {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model KategoriPrestasiDesa {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_default
```
Setelah fix critical schema issue, module ini production-ready! 🎉

View File

@@ -0,0 +1,488 @@
# QC Summary - Profil Landing Page Module
**Scope:** Media Sosial, Pejabat Desa, Program Inovasi
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement minor
---
## 📊 OVERVIEW
| Module | Schema | API | UI Admin | Public Page | Overall |
|--------|--------|-----|----------|-------------|---------|
| Media Sosial | ✅ Baik | ✅ Baik | ✅ Baik | N/A | 🟢 Baik |
| Pejabat Desa | ✅ Baik | ⚠️ Ada issue | ✅ Baik | N/A | 🟡 Perlu fix |
| Program Inovasi | ✅ Baik | ✅ Baik | ✅ Baik | N/A | 🟢 Baik |
---
## ✅ YANG SUDAH BAIK (COMMON)
### **1. Konsistensi UI/UX**
- ✅ Semua halaman menggunakan pattern yang sama (list → detail → edit)
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dengan Skeleton
- ✅ Empty state handling yang informatif
- ✅ Search dengan debounce (1000ms)
- ✅ Pagination konsisten di semua modul
### **2. File Upload Handling**
- ✅ Dropzone dengan preview image
- ✅ Validasi format & ukuran file (max 5MB)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
- ✅ Cleanup file state saat reset form
### **3. Form Validation**
- ✅ Zod schema untuk validasi typed
- ✅ isFormValid() check sebelum submit
- ✅ Error toast dengan pesan spesifik
- ✅ Button disabled saat invalid/loading
### **4. State Management (Valtio)**
- ✅ Proxy state untuk reaktivitas
- ✅ Separate state per modul (programInovasi, pejabatDesa, mediaSosial)
- ✅ Reset form function di setiap create/edit
- ✅ Original data tracking untuk reset
### **5. Error Handling**
- ✅ Try-catch di semua async operation
- ✅ Toast error dengan pesan user-friendly
- ✅ Console.error untuk debugging
- ✅ Modal konfirmasi hapus
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Pejabat Desa - Edit Form Tidak Reset imageId ke Original**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/[id]/edit/page.tsx`
**Masalah:**
```typescript
// Line ~100 - Load data
setFormData({
name: profileData.name || "",
position: profileData.position || "",
imageId: profileData.imageId || "", // ✅ Sudah benar
});
// Line ~170 - Handle reset
setFormData({
name: originalData.name,
position: originalData.position,
imageId: originalData.imageId, // ✅ Sudah benar
});
```
**Status:****SUDAH BENAR** - Tidak ada issue di sini
**Verdict:** Tidak ada action needed.
---
#### **2. Media Sosial - Edit Form Sudah Benar**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx`
**Verdict:****SUDAH BENAR** - Original data tracking sudah implementasi dengan baik:
```typescript
const [originalData, setOriginalData] = useState({
name: '',
icon: '',
iconUrl: '',
imageId: '',
imageUrl: '',
});
// Load data
setOriginalData({
...newForm,
imageUrl: data.image?.link || '',
});
// Reset form
setFormData({
name: originalData.name,
icon: originalData.icon,
iconUrl: originalData.iconUrl,
imageId: originalData.imageId,
});
```
**Verdict:** Tidak ada action needed.
---
#### **3. Program Inovasi - Edit Form Sudah Benar**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/edit/page.tsx`
**Verdict:****SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
**Verdict:** Tidak ada action needed.
---
### **🟡 MEDIUM**
#### **4. Inconsistency: Fetch Method di State**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/profile.ts`
**Masalah:** Ada 3 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (programInovasi.create)
const res = await ApiFetch.api.landingpage.programinovasi["create"].post(formData);
// ❌ Pattern 2: fetch manual (programInovasi.findUnique)
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
// ❌ Pattern 3: fetch dengan headers (programInovasi.update)
const response = await fetch(`/api/landingpage/programinovasi/${this.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({...}),
});
// ❌ Pattern 4: fetch dengan delete (programInovasi.delete)
const response = await fetch(`/api/landingpage/programinovasi/del/${id}`, {
method: "DELETE",
...
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅统一 pattern
const res = await ApiFetch.api.landingpage.programinovasi["create"].post(formData);
const res = await ApiFetch.api.landingpage.programinovasi[id].get();
const res = await ApiFetch.api.landingpage.programinovasi[id].put(data);
const res = await ApiFetch.api.landingpage.programinovasi["del"][id].delete();
```
**Priority:** 🟡 Medium
**Effort:** Low (refactor saja, tidak ada logic change)
---
#### **5. Media Sosial - Validasi IconUrl Tidak Selalu Relevan**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/media-sosial/create/page.tsx`
**Masalah:**
```typescript
// Line ~67
const isFormValid = () => {
const isNameValid = stateMediaSosial.create.form.name?.trim() !== '';
const isIconUrlValid = stateMediaSosial.create.form.iconUrl?.trim() !== ''; // ❌ Selalu required
const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
return isNameValid && isIconUrlValid && isCustomIconValid;
};
```
**Scenario:**
- User pilih icon "telephone" → iconUrl **seharusnya** required (nomor telepon)
- User pilih icon "facebook" → iconUrl **seharusnya** required (URL profile)
- Tapi jika user hanya mau tampil icon tanpa link → **tidak bisa**
**Rekomendasi:** Jadikan optional atau berikan default value:
```typescript
const isFormValid = () => {
const isNameValid = stateMediaSosial.create.form.name?.trim() !== '';
// IconUrl optional, atau validasi berdasarkan selectedSosmed
const isIconUrlValid = true; // atau validasi spesifik
const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
return isNameValid && isCustomIconValid;
};
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Pejabat Desa - Hanya Ada 1 Data (Hardcoded ID "edit")**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/page.tsx`
**Masalah:**
```typescript
// Line ~17
useShallowEffect(() => {
allList.findUnique.load("edit"); // ❌ Hardcoded ID
}, []);
```
**Dampak:**
- Tidak scalable jika nanti ada multiple pejabat desa
- Pattern berbeda dari modul lain (yang pakai findMany)
- Confusing untuk developer baru
**Rekomendasi:**
- Jika memang hanya 1 data, tambahkan komentar:
```typescript
// Note: "edit" adalah special ID untuk single pejabat desa record
// Backend akan return data pertama jika ID tidak ditemukan
allList.findUnique.load("edit");
```
- Atau gunakan pattern yang lebih clear:
```typescript
allList.findUnique.load("single"); // atau "default"
```
**Priority:** 🟡 Low-Medium
**Effort:** Low
---
#### **7. Program Inovasi - HTML Injection Risk di Deskripsi**
**Lokasi:**
- `src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx` (line ~107)
- `src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx` (line ~105)
**Masalah:**
```typescript
// ❌ Direct HTML render tanpa sanitization
<Text dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedHtml = DOMPurify.sanitize(item.description);
<Text dangerouslySetInnerHTML={{ __html: sanitizedHtml }}></Text>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, dll).
**Priority:** 🟡 Medium (security concern)
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Inconsistency: Button Size & Styling**
**Lokasi:** Multiple files
**Masalah:** Button styling tidak konsisten:
```typescript
// Media Sosial create
<Button size="md" ...>Simpan</Button>
// Program Inovasi create
<Button size="md" ...>Simpan</Button>
// Pejabat Desa edit
<Button size="md" ...>Simpan</Button>
// Media Sosial edit
<Button size="md" ...>Simpan</Button>
```
Tapi di detail page:
```typescript
// Semua detail page
<Button size="md" ...> // ✅ Konsisten
```
**Rekomendasi:** Buat konstanta untuk button size:
```typescript
const BUTTON_SIZE = "md";
const BUTTON_VARIANT = "light";
const BUTTON_RADIUS = "md";
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Search Placeholder Tidak Spesifik**
**Lokasi:** Multiple list pages
**Masalah:**
```typescript
// Media Sosial
placeholder='Cari nama media sosial atau kontak...' // ✅ Spesifik
// Program Inovasi
placeholder="Cari program inovasi..." // ✅ Oke
// Pejabat Desa
// ❌ Tidak ada search feature
```
**Rekomendasi:** Tambahkan search feature ke Pejabat Desa jika memungkinkan, atau berikan komentar kenapa tidak ada (karena hanya 1 data).
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Loading State Tidak Selalu Akurat**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/profile.ts`
**Masalah:**
```typescript
// Line ~120 - findUnique.load untuk programInovasi
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
// ❌ Tidak ada loading state update di sini
if (res.ok) {
const data = await res.json();
programInovasi.findUnique.data = data.data ?? null;
}
} catch (error) {
// ❌ Tidak ada finally block untuk stop loading
}
}
```
**Dampak:** UI mungkin stuck di loading state jika ada error.
**Rekomendasi:** Tambahkan finally block:
```typescript
async load(id: string) {
try {
programInovasi.findUnique.loading = true; // ✅ Start loading
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
if (res.ok) {
const data = await res.json();
programInovasi.findUnique.data = data.data ?? null;
}
} catch (error) {
console.error("Error:", error);
} finally {
programInovasi.findUnique.loading = false; // ✅ Stop loading
}
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/profile.ts`
**Masalah:**
```typescript
// Line ~75
data: null as any[] | null, // ❌ Using 'any'
// Line ~120
data: null as Prisma.ProgramInovasiGetPayload<{...}> | null, // ✅ Typed
// Line ~200
data: null as any[] | null, // ❌ Using 'any'
```
**Rekomendasi:** Gunakan typed data:
```typescript
data: null as Prisma.MediaSosialGetPayload<{ include: { image: true } }>[] | null
```
**Priority:** 🟢 Low
**Effort:** Medium (perlu update semua reference)
---
#### **12. Console.log di Production**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// Media Sosial edit page (line ~170)
console.log("Data yang akan dikirim ke backend:", stateMediaSosial.update.form);
// Profile state (multiple places)
console.log("Failed to load program inovasi:", res.statusText);
console.log((error as Error).message);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.log("Data:", stateMediaSosial.update.form);
}
```
Atau gunakan logging library (winston, pino, dll).
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🟡 M | Fetch method inconsistency | All | Medium | Low | Perlu refactor |
| 🟡 M | IconUrl validation terlalu strict | Media Sosial | Low | Low | Perlu fix logic |
| 🟡 M | HTML injection risk | Program Inovasi | **High (Security)** | Low | **Should fix** |
| 🟢 L | Hardcoded ID "edit" | Pejabat Desa | Low | Low | Optional |
| 🟢 L | Button styling inconsistency | All | Low | Low | Optional |
| 🟢 L | Missing search feature | Pejabat Desa | Low | Low | Optional |
| 🟢 L | Loading state inaccurate | All | Low | Low | Perlu fix |
| 🟢 L | Type safety (any usage) | All | Low | Medium | Optional |
| 🟢 L | Console.log in production | All | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8/10)**
**Strengths:**
1. ✅ UI/UX konsisten & responsive
2. ✅ File upload handling sudah solid
3. ✅ Form validation dengan Zod
4. ✅ State management terstruktur
5. ✅ Error handling comprehensive
6. ✅ Edit form reset sudah benar di semua modul
**Areas for Improvement:**
1. ⚠️ **Security:** HTML injection di deskripsi Program Inovasi (prioritas)
2. ⚠️ **Consistency:** Fetch method pattern (ApiFetch vs fetch manual)
3. ⚠️ **Type Safety:** Reduce `any` usage, gunakan Prisma types
4. ⚠️ **Loading States:** Pastikan selalu ada finally block
**Recommended Next Steps:**
1. **Fix HTML injection** dengan DOMPurify atau backend validation
2. **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. **Add loading state cleanup** di semua async operations
4. **Optional:** Improve type safety dengan remove `any`
---
**Catatan:** Secara keseluruhan, modul Profil sudah **production-ready** dengan minor improvements yang bisa dilakukan secara incremental.

View File

@@ -0,0 +1,651 @@
# QC Summary - SDGs Desa Module
**Scope:** List SDGs Desa, Create, Edit, Detail
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| SDGs Desa | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Consistency**
- ✅ Responsive design (desktop table + mobile cards)
- ✅ Loading states dengan Skeleton
- ✅ Search dengan debounce (1000ms)
- ✅ Pagination konsisten
- ✅ Empty state handling yang informatif
- ✅ Modal konfirmasi hapus
### **2. File Upload Handling**
- ✅ Dropzone dengan preview image
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
- ✅ Validasi ukuran file (max 5MB)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
### **3. Form Validation**
- ✅ Zod schema untuk validasi typed
- ✅ isFormValid() check sebelum submit
- ✅ Error toast dengan pesan spesifik
- ✅ Button disabled saat invalid/loading
- ✅ Type number input untuk jumlah
### **4. CRUD Operations**
- ✅ Create dengan upload file
- ✅ FindMany dengan pagination & search
- ✅ FindUnique untuk detail
- ✅ Delete dengan hard delete (via Prisma)
- ✅ Update dengan file replacement
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Preview image dari data lama
- ✅ Reset form mengembalikan ke data original
**Code Example (✅ GOOD):**
```typescript
// Line ~60-80 - Load data
const data = await sdgsState.edit.load(id);
setFormData({
name: data.name || "",
jumlah: data.jumlah || "",
imageId: data.imageId || "",
});
setOriginalData({
...newForm,
imageUrl: data.image?.link || "",
});
setPreviewImage(data.image?.link || null);
// Line ~90 - Handle reset
const handleResetForm = () => {
setFormData({
name: originalData.name,
jumlah: originalData.jumlah,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
```
**Verdict:****SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. State Management - Inconsistency Fetch Pattern**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany)
const res = await ApiFetch.api.landingpage.sdgsdesa["create"].post({...});
const res = await ApiFetch.api.landingpage.sdgsdesa["findMany"].get({query});
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
const response = await fetch(`/api/landingpage/sdgsdesa/del/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
const res = await ApiFetch.api.landingpage.sdgsdesa["create"].post(data);
const res = await ApiFetch.api.landingpage.sdgsdesa[id].get();
const res = await ApiFetch.api.landingpage.sdgsdesa[id].put(data);
const res = await ApiFetch.api.landingpage.sdgsdesa["del"][id].delete();
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di semua state methods)
---
#### **2. findUnique State - Tidak Ada Loading State Management**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
**Masalah:**
```typescript
// Line ~125 - sdgsDesa.findUnique.load()
async load(id: string) {
try {
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
if (res.ok) {
const data = await res.json();
sdgsDesa.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
sdgsDesa.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
sdgsDesa.findUnique.data = null;
}
// ❌ MISSING: finally block untuk stop loading
}
```
**Dampak:** UI mungkin stuck di loading state jika ada error.
**Rekomendasi:** Tambahkan loading state dan finally block:
```typescript
async load(id: string) {
try {
sdgsDesa.findUnique.loading = true; // ✅ Start loading
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
if (res.ok) {
const data = await res.json();
sdgsDesa.findUnique.data = data.data ?? null;
}
} catch (error) {
console.error("Error:", error);
} finally {
sdgsDesa.findUnique.loading = false; // ✅ Stop loading
}
}
```
**Priority:** 🔴 Medium
**Effort:** Low
---
#### **3. findManyAll - Tidak Digunakan di UI**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
**Masalah:**
```typescript
// Line ~95 - findManyAll state
findManyAll: {
data: null as any[] | null,
loading: false,
load: async () => {
// ... fetch all data tanpa pagination
},
}
```
**Analysis:**
- ⚠️ **UNUSED:** Tidak ada component yang menggunakan `findManyAll`
- ⚠️ **DEAD CODE:** Menambah bundle size tanpa manfaat
- ⚠️ **CONFUSING:** Developer baru bisa bingung kapan pakai findMany vs findManyAll
**Rekomendasi:** Remove jika tidak digunakan:
```typescript
// ❌ Remove entire findManyAll block
```
Atau jika diperlukan untuk future feature, tambahkan comment:
```typescript
// Reserved for future use - dropdown select without pagination
findManyAll: { ... }
```
**Priority:** 🔴 Low-Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
**Masalah:**
```typescript
// Line ~58
data: null as any[] | null, // ❌ Using 'any'
// Line ~96
data: null as any[] | null, // ❌ Using 'any'
// Line ~118
data: null as Prisma.SdgsDesaGetPayload<{...}> | null, // ✅ Typed
```
**Rekomendasi:** Gunakan typed data consistently:
```typescript
// findMany
data: null as Prisma.SdgsDesaGetPayload<{
include: { image: true };
}>[] | null,
// findManyAll (jika tidak dihapus)
data: null as Prisma.SdgsDesaGetPayload<{
include: { image: true };
}>[] | null,
```
**Priority:** 🟡 Medium
**Effort:** Medium (perlu update semua reference)
---
#### **5. Console.log di Production**
**Lokasi:** Multiple places di state file
**Masalah:**
```typescript
// Line ~48
console.log(error);
toast.error("Gagal menambahkan data");
// Line ~80
console.error("Failed to load media sosial:", res.data?.message);
// Line ~85
console.error("Error loading media sosial:", error);
// Line ~132
console.error("Failed to fetch data", res.status, res.statusText);
// Line ~136
console.error("Error fetching data:", error);
// ... dan banyak lagi
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
Atau gunakan logging library (winston, pino, dll) dengan levels yang jelas.
**Priority:** 🟡 Low
**Effort:** Low
---
#### **6. Error Message Tidak Konsisten**
**Lokasi:** Multiple places
**Masalah:**
```typescript
// Create - Line ~44
return toast.error("Gagal menambahkan data");
// Create - Line ~46
toast.error("Gagal menambahkan data");
// Delete - Line ~165
toast.error("Terjadi kesalahan saat menghapus sdgs desa");
// Edit - Line ~210
toast.error("Gagal memuat data");
// Edit update - Line ~250
toast.error("Gagal mengupdate sdgs desa");
// Toast success - Line ~240
toast.success("Berhasil update sdgs desa");
```
**Issue:**
- Inconsistent capitalization ("sdgs desa" vs "Sdgs Desa")
- Mixed patterns ("Gagal menambahkan" vs "Terjadi kesalahan")
- Typo: "sdgs" seharusnya "SDGs" (acronym)
**Rekomendasi:** Standardisasi error messages:
```typescript
// Pattern: "[Action] [resource] gagal" dengan proper casing
toast.error("Menambahkan data SDGs Desa gagal");
toast.error("Menghapus data SDGs Desa gagal");
toast.error("Memuat data SDGs Desa gagal");
toast.error("Memperbarui data SDGs Desa gagal");
// Atau lebih spesifik dengan context
toast.error("Gagal menambahkan data SDGs Desa");
toast.error("Gagal menghapus SDGs Desa");
toast.success("Berhasil memperbarui SDGs Desa");
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **7. Zod Schema - Error Message Tidak Akurat**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
**Masalah:**
```typescript
// Line ~8
const templatesdgsDesaForm = z.object({
name: z.string().min(1, "Judul minimal 1 karakter"), // ❌ "Judul" instead of "Nama"
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"), // ❌ "Deskripsi" instead of "Jumlah"
imageId: z.string().min(1, "File minimal 1"),
});
```
**Dampak:** User confusion saat validasi error muncul:
```
Error: "Judul minimal 1 karakter" // User: "Lho, ini field nama bukan judul?"
Error: "Deskripsi minimal 1 karakter" // User: "Ini field jumlah bukan deskripsi?"
```
**Rekomendasi:** Fix error messages:
```typescript
const templatesdgsDesaForm = z.object({
name: z.string().min(1, "Nama SDGs Desa minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
imageId: z.string().min(1, "Gambar wajib dipilih"),
});
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Component Name Mismatch**
**Lokasi:** `src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx`
**Masalah:**
```typescript
// Line ~30
export default function EditKolaborasiInovasi() { // ❌ Wrong name
// ...
}
```
**Dampak:** Confusing untuk developer lain, sulit untuk search/reference.
**Rekomendasi:** Rename ke yang sesuai:
```typescript
export default function EditSDGsDesa() { // ✅ Correct name
// ...
}
```
**Priority:** 🟢 Low
**Effort:** Low (hanya rename)
---
#### **9. Text Label Tidak Konsisten**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// Create page - Line ~100
<Text fw="bold" fz="sm" mb={6}>
Gambar Program Inovasi // ❌ Wrong label
</Text>
// Edit page - Line ~170
<Text fw="bold" fz="sm" mb={6}>
Gambar Program Inovasi // ❌ Wrong label (copy-paste?)
</Text>
```
**Rekomendasi:** Fix label:
```typescript
<Text fw="bold" fz="sm" mb={6}>
Gambar SDGs Desa // ✅ Correct label
</Text>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Placeholder Search Tidak Spesifik**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~17
<HeaderSearch
title='Sdgs Desa'
placeholder='Cari Sdgs Desa...' // ⚠️ Generic
// ...
/>
```
**Rekomendasi:** Lebih spesifik:
```typescript
placeholder='Cari nama SDGs Desa...'
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Capitalization Inconsistency**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// page.tsx - Line ~17
title='Sdgs Desa' // ❌ Mixed case
// create/page.tsx - Line ~90
<Title>Tambah Sdgs Desa</Title> // ❌ Mixed case
// edit/page.tsx - Line ~160
<Title>Edit Sdgs Desa</Title> // ❌ Mixed case
// Should be:
// "SDGs Desa" (all caps for acronym)
```
**Rekomendasi:** Standardisasi:
```typescript
title='SDGs Desa'
<Title>Tambah SDGs Desa</Title>
<Title>Edit SDGs Desa</Title>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Schema - deletedAt Default Value**
**Lokasi:** `prisma/schema.prisma`
**Masalah:**
```prisma
model SdgsDesa {
// ...
deletedAt DateTime @default(now()) // ❌ Always has default value
isActive Boolean @default(true)
}
```
**Issue:** `deletedAt @default(now())` berarti setiap record baru langsung punya `deletedAt` value, yang bisa membingungkan untuk soft delete logic.
**Rekomendasi:**
```prisma
model SdgsDesa {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🟢 Medium (potential logic issue)
**Effort:** Medium (perlu migration)
---
#### **13. Duplicate Error Logging**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~80
} catch (error) {
console.error("Error loading sdgs desa:", error); // ❌ Duplicate
toast.error("Gagal memuat data sdgs desa");
}
// Line ~120
} catch (error) {
console.error("Error updating sdgs desa:", error); // ❌ Duplicate
toast.error("Terjadi kesalahan saat memperbarui sdgs desa");
}
```
**Rekomendasi:** Cukup satu logging yang informatif:
```typescript
} catch (error) {
console.error('Failed to load SDGs Desa:', err);
toast.error('Gagal memuat data SDGs Desa');
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **14. API Response Handling - Inconsistent Error Messages**
**Lokasi:** API endpoints
**Masalah:** (dari grep search results)
```typescript
// del.ts - Line ~18
message: "Berhasil menghapus SDGS Desa", // ✅ Proper
// updt.ts - Line ~38
message: "SDGS Desa berhasil diperbarui", // ✅ Proper
// create.ts - (assumed)
// Might have inconsistent casing
```
**Rekomendasi:** Ensure all API responses use consistent "SDGs Desa" casing.
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P0 | Missing loading state in findUnique | State | Medium | Low | Perlu fix |
| 🔴 P1 | Unused findManyAll code | State | Low | Low | Should remove |
| 🟡 M | Type safety (any usage) | State | Low | Medium | Optional |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Error message inconsistency | State/UI | Low | Low | Optional |
| 🟡 M | Zod schema error messages | State | Low | Low | Should fix |
| 🟢 L | Component name mismatch | Edit page | Low | Low | Optional |
| 🟢 L | Wrong label text ("Program Inovasi") | Create/Edit | Low | Low | Should fix |
| 🟢 L | Placeholder tidak spesifik | List page | Low | Low | Optional |
| 🟢 L | Capitalization inconsistency | All UI | Low | Low | Should fix |
| 🟢 M | deletedAt default value | Schema | Medium | Medium | Should fix |
| 🟢 L | Duplicate error logging | Edit page | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (7.5/10)**
**Strengths:**
1. ✅ UI/UX konsisten & responsive
2. ✅ File upload handling solid
3. ✅ Form validation dengan Zod schema
4. ✅ State management terstruktur (Valtio)
5.**Edit form reset sudah benar** (original data tracking)
6. ✅ Modal konfirmasi hapus untuk user safety
7. ✅ Type number input untuk field jumlah
**Areas for Improvement:**
1. ⚠️ **Consistency:** Fetch method pattern (ApiFetch vs fetch manual)
2. ⚠️ **Loading States:** findUnique tidak ada loading state management
3. ⚠️ **Dead Code:** findManyAll tidak digunakan
4. ⚠️ **Type Safety:** Reduce `any` usage, gunakan Prisma types
5. ⚠️ **Schema:** deletedAt default value bisa menyebabkan logic issue
6. ⚠️ **Naming:** Component name & label text masih ada yang salah
**Recommended Next Steps:**
1. **Refactor fetch methods** untuk gunakan ApiFetch consistently
2. **Add loading state** di findUnique operations
3. **Remove findManyAll** jika tidak digunakan
4. **Fix component name** (EditKolaborasiInovasi → EditSDGsDesa)
5. **Fix label text** ("Gambar Program Inovasi" → "Gambar SDGs Desa")
6. **Fix capitalization** (Sdgs → SDGs)
7. **Optional:** Improve type safety dengan remove `any`
---
## 📈 COMPARISON WITH OTHER MODULES
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | Notes |
|--------|--------|-------------------|-----------|-------|
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | Same issue |
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | Consistent |
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
| File Upload | ✅ Images | ✅ Documents | ✅ Images | Different use case |
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | Consistent |
| Dead Code | ❌ None | ❌ None | ⚠️ findManyAll | SDGs unique issue |
| Naming Issues | ❌ None | ⚠️ Some | ⚠️ Some | Similar level |
---
**Catatan:** Secara keseluruhan, modul SDGs Desa sudah **production-ready** dengan beberapa improvements yang bisa dilakukan secara incremental. Module ini memiliki struktur yang mirip dengan modul lain (Profil, Desa Anti Korupsi) sehingga pattern improvement yang sama bisa diterapkan.
**Unique Issues:**
1. findManyAll unused code (tidak ada di modul lain)
2. Component name mismatch (EditKolaborasiInovasi)
3. Wrong label text ("Gambar Program Inovasi") - kemungkinan copy-paste dari modul Program Inovasi

View File

@@ -0,0 +1,879 @@
# QC Summary - Daftar Informasi Publik PPID Module
**Scope:** List Daftar Informasi Publik, Create, Edit, Detail
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Daftar Informasi Publik | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Design**
- ✅ Preview layout yang clean dengan responsive design
- ✅ Loading states dengan Skeleton
- ✅ Empty state handling yang informatif dengan icon
- ✅ Search functionality dengan debounce (1000ms)
- ✅ Pagination yang konsisten
- ✅ Desktop table + mobile cards responsive
- ✅ Sticky table header untuk better UX
- ✅ Responsive button text ("Tambah" vs "Tambah Baru")
### **2. Table & Card Layout**
- ✅ Fixed column widths (25%, 40%, 20%)
- ✅ Sticky header table untuk long lists
- ✅ Striped rows untuk readability
- ✅ Highlight on hover
- ✅ HTML tag stripping untuk preview deskripsi
- ✅ Text truncation dengan lineClamp dan substring
- ✅ Mobile card view dengan proper information hierarchy
**Code Example (✅ GOOD):**
```typescript
// page.tsx - Line ~95-120
<Table
highlightOnHover
striped
stickyHeader // ✅ GOOD - Header tetap visible saat scroll
style={{ minWidth: '700px' }} // ✅ GOOD - Minimum width untuk readability
>
<TableThead>
<TableTr>
<TableTh w="25%">
<Text fw={600} lh={1.4}>Jenis Informasi</Text>
</TableTh>
<TableTh w="40%">
<Text fw={600} lh={1.4}>Deskripsi</Text>
</TableTh>
<TableTh ta="center" w="20%">
<Text fw={600} lh={1.4}>Aksi</Text>
</TableTh>
</TableTr>
</TableThead>
```
**Verdict:****BAIK** - Table layout dengan sticky header yang helpful!
---
### **3. State Management**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
-**ApiFetch consistency** untuk create & findMany! ✅
- ✅ Zod validation untuk form data
- ✅ Proper date formatting untuk update operation
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~50-85
findMany: {
data: null as Prisma.DaftarInformasiPublikGetPayload<{ omit: { isActive: true } }>[] | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
daftarInformasiPublik.findMany.loading = true; // ✅ Start loading
daftarInformasiPublik.findMany.page = page;
daftarInformasiPublik.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
daftarInformasiPublik.findMany.data = res.data.data ?? [];
daftarInformasiPublik.findMany.totalPages = res.data.totalPages ?? 1;
}
} catch (err) {
console.error("Gagal fetch daftar informasi publik:", err);
daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.totalPages = 1;
} finally {
daftarInformasiPublik.findMany.loading = false; // ✅ Stop loading
}
},
}
```
**Verdict:****BAIK** - State management sudah proper dengan ApiFetch!
---
### **4. Zod Schema Validation**
- ✅ Comprehensive validation untuk semua fields
- ✅ Specific error messages untuk setiap field
- ✅ Minimum character validation (3 characters)
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~8-12
const templateDaftarInformasi = z.object({
jenisInformasi: z.string().min(3, "Jenis Informasi minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
tanggal: z.string().min(3, "Tanggal minimal 3 karakter"),
});
```
**Verdict:****BAIK** - Validation yang proper!
---
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form (via useState)
- ✅ Load data existing dengan benar
- ✅ Reset form mengembalikan ke data original
- ✅ Rich text content handling yang proper
- ✅ Date formatting untuk input type="date"
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~30-60
const [formData, setFormData] = useState<FormDaftarInformasi>({
jenisInformasi: '',
deskripsi: '',
tanggal: '',
});
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0]; // ✅ Format untuk input date
};
// Load data
useEffect(() => {
const loadDaftarInformasi = async () => {
const data = await daftarInformasi.edit.load(id);
if (data) {
setFormData({
jenisInformasi: data.jenisInformasi || '',
deskripsi: data.deskripsi || '',
tanggal: data.tanggal || '',
});
}
};
loadDaftarInformasi();
}, [params?.id]);
```
**Verdict:****BAIK** - Original data tracking sudah implementasi dengan baik!
---
### **6. Rich Text Editor**
- ✅ CreateEditor untuk create page
- ✅ EditEditor untuk edit page
- ✅ Reusable component pattern
- ✅ HTML content handling yang proper
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH**
**Lokasi:** `prisma/schema.prisma` (line 414)
**Masalah:**
```prisma
model DaftarInformasiPublik {
id String @id @default(cuid())
jenisInformasi String
deskripsi String
tanggal DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
**Contoh Issue:**
```prisma
// Record baru dibuat
CREATE DaftarInformasiPublik {
jenisInformasi: "Informasi 1",
deskripsi: "Deskripsi 1",
tanggal: "2024-01-01",
// deletedAt otomatis ter-set ke now() ❌
// isActive: true ✅
}
// Query untuk data aktif (seharusnya return data ini)
prisma.daftarInformasiPublik.findMany({
where: { deletedAt: null, isActive: true }
})
// ❌ Return kosong! Karena deletedAt sudah ter-set
```
**Rekomendasi:** Fix schema:
```prisma
model DaftarInformasiPublik {
id String @id @default(cuid())
jenisInformasi String
deskripsi String
tanggal DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. State Management - Fetch Pattern Inconsistency**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany)
const res = await ApiFetch.api.ppid.daftarinformasipublik["create"].post(form);
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
const res = await fetch(`/api/ppid/daftarinformasipublik/${id}`);
const response = await fetch(`/api/ppid/daftarinformasipublik/del/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
const res = await ApiFetch.api.ppid.daftarinformasipublik[id].get();
if (res.data?.success) {
const data = res.data.data;
this.id = data.id;
this.form = {
jenisInformasi: data.jenisInformasi,
deskripsi: data.deskripsi,
tanggal: data.tanggal,
};
return data;
} else {
throw new Error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error:", error);
toast.error("Gagal memuat data");
return null;
}
}
async byId(id: string) {
try {
const res = await ApiFetch.api.ppid.daftarinformasipublik["del"][id].delete();
if (res.data?.success) {
toast.success(res.data.message || "Berhasil hapus");
await daftarInformasiPublik.findMany.load();
} else {
toast.error(res.data?.message || "Gagal hapus");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di findUnique, edit, delete methods)
---
#### **3. Missing Loading State di Edit Button**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~130-145
<Button
onClick={handleSubmit}
disabled={!isFormValid()} // ⚠️ Missing loading check
radius="md"
size="md"
// ...
>
Simpan Perubahan
</Button>
```
**Issue:** Button tidak disabled saat submitting. User bisa click multiple times.
**Rekomendasi:** Add loading state:
```typescript
const [isSubmitting, setIsSubmitting] = useState(false);
// In handleSubmit
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await daftarInformasi.edit.update();
router.push('/admin/ppid/daftar-informasi-publik');
} catch (error) {
// ...
} finally {
setIsSubmitting(false);
}
};
// In button
<Button
onClick={handleSubmit}
disabled={!isFormValid() || isSubmitting}
// ...
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan Perubahan'}
</Button>
```
**Priority:** 🔴 Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts`
**Masalah:**
```typescript
// Line ~45
console.log((error as Error).message);
// Line ~80
console.error("Gagal fetch daftar informasi publik paginated:", err);
// Line ~100
console.error("Failed to fetch daftar informasi publik:", res.statusText);
// Line ~104
console.error("Error fetching daftar informasi publik:", error);
// Line ~180
console.error("Error loading daftar informasi publik:", error);
// Line ~230
console.error("Error updating daftar informasi publik:", error);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts`
**Masalah:**
```typescript
// Line ~70
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;
```
**Rekomendasi:** Gunakan typed query:
```typescript
// Define type
interface FindManyQuery {
page: number | string;
limit?: number | string;
search?: string;
}
// Use typed query
const query: FindManyQuery = { page, limit };
if (search) query.search = search;
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Alert() Instead of Toast**
**Lokasi:** `create/page.tsx`
**Masalah:**
```typescript
// Line ~30-40
const handleSubmit = async () => {
if (!daftarInformasi.create.form.jenisInformasi) {
return alert('Mohon isi jenis informasi'); // ❌ Using alert()
}
if (!daftarInformasi.create.form.deskripsi) {
return alert('Mohon isi deskripsi'); // ❌ Using alert()
}
if (!daftarInformasi.create.form.tanggal) {
return alert('Mohon pilih tanggal publikasi'); // ❌ Using alert()
}
try {
await daftarInformasi.create.create();
// ...
} catch (error) {
console.error('Error creating informasi publik:', error);
alert('Terjadi kesalahan saat menyimpan data'); // ❌ Using alert()
}
};
```
**Rekomendasi:** Gunakan toast untuk consistency:
```typescript
if (!daftarInformasi.create.form.jenisInformasi) {
return toast.warn('Mohon isi jenis informasi'); // ✅ Using toast
}
// ...
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **7. Missing Reset Form Function**
**Lokasi:** `create/page.tsx`
**Masalah:**
```typescript
// Line ~20-25
const resetForm = () => {
daftarInformasi.create.form = {
jenisInformasi: "",
deskripsi: "",
tanggal: "",
};
};
// resetForm dipanggil di handleSubmit tapi tidak ada di form inputs
// Form inputs langsung update state tanpa reset setelah submit
```
**Issue:** Form tidak reset setelah successful submit.
**Rekomendasi:** Ensure reset is called:
```typescript
const handleSubmit = async () => {
// ... validation
try {
await daftarInformasi.create.create();
resetForm(); // ✅ Make sure this is called
router.push("/admin/ppid/daftar-informasi-publik");
} catch (error) {
// ...
}
};
```
**Verdict:****SUDAH BENAR** - resetForm() sudah dipanggil di handleSubmit!
**Priority:** 🟢 None
**Effort:** None
---
### **🟢 LOW (Minor Polish)**
#### **8. Pagination onChange Tidak Include Search**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~190-200
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ⚠️ Missing search parameter
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang.
**Rekomendasi:** Include search:
```typescript
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Duplicate Error Logging**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// edit/page.tsx - Line ~60
} catch (error) {
console.error('Error loading daftar informasi:', error); // ❌ Duplicate
toast.error('Gagal memuat data daftar informasi');
}
// edit/page.tsx - Line ~80
} catch (error) {
console.error('Error updating berita:', error); // ❌ Duplicate + wrong module name
toast.error('Terjadi kesalahan saat memperbarui berita'); // ❌ Wrong module name
}
```
**Issue:** Copy-paste error dari module "berita"!
**Rekomendasi:** Fix error messages:
```typescript
} catch (error) {
console.error('Failed to load Daftar Informasi Publik:', err);
toast.error('Gagal memuat data Daftar Informasi Publik');
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Missing Loading State di Detail Page**
**Lokasi:** `[id]/page.tsx`
**Masalah:**
```typescript
// Line ~20-25
useShallowEffect(() => {
stateDaftarInformasi.findUnique.load(params?.id as string)
}, [params?.id])
if (!stateDaftarInformasi.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
```
**Issue:** Skeleton ditampilkan untuk semua kondisi (loading, error, not found).
**Rekomendasi:** Add proper loading state:
```typescript
if (stateDaftarInformasi.findUnique.loading) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
if (!stateDaftarInformasi.findUnique.data) {
return (
<Alert icon={<IconAlertCircle />} color="red">
Data tidak ditemukan
</Alert>
);
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Search Placeholder Tidak Spesifik**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~30-35
<HeaderSearch
title='Daftar Informasi Publik'
placeholder='Cari jenis informasi atau deskripsi...' // ✅ Actually pretty specific!
// ...
/>
```
**Verdict:****SUDAH BENAR** - Placeholder sudah spesifik!
**Priority:** 🟢 None
**Effort:** None
---
#### **12. Empty State Icon Consistency**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~85-95
<Stack align="center" py="xl">
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada informasi publik yang tersedia
</Text>
</Stack>
```
**Verdict:****SUDAH BENAR** - Empty state dengan icon yang proper!
**Priority:** 🟢 None
**Effort:** None
---
#### **13. HTML Tag Stripping for Preview**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~125-130
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
</Text>
```
**Verdict:****SUDAH BENAR** - HTML tag stripping yang proper untuk preview!
**Priority:** 🟢 None
**Effort:** None
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P1 | Missing loading state di edit button | UI | Medium | Low | Should fix |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
| 🟡 M | Alert() instead of toast | Create UI | Low | Low | Should fix |
| 🟡 M | Copy-paste error messages (berita) | Edit UI | Low | Low | Should fix |
| 🟢 L | Pagination missing search param | UI | Low | Low | Optional |
| 🟢 L | Missing loading state di detail page | UI | Low | Low | Optional |
| 🟢 L | Duplicate error logging | UI/State | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8/10)**
**Strengths:**
1. ✅ UI/UX clean & responsive
2.**Sticky header table** - Better UX untuk long lists
3.**HTML tag stripping** untuk preview deskripsi
4. ✅ Search functionality dengan debounce
5. ✅ Empty state handling yang informatif
6.**Zod validation** comprehensive
7. ✅ State management dengan ApiFetch untuk create & findMany
8. ✅ Loading state management dengan finally block
9. ✅ Mobile cards responsive
10.**Responsive button text** ("Tambah" vs "Tambah Baru")
11. ✅ Edit form dengan original data tracking
12. ✅ Date formatting untuk input type="date"
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. ⚠️ Missing loading state di edit button
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. ⚠️ **Add loading state** di edit button
4. ⚠️ **Fix alert()** ke toast
5. ⚠️ **Fix copy-paste error messages** dari module "berita"
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH: Refactor findUnique, edit, delete** ke ApiFetch - 1 jam
3. **🔴 HIGH: Add loading state** di edit button - 15 menit
4. **🟡 MEDIUM: Fix alert()** ke toast - 15 menit
5. **🟡 MEDIUM: Fix copy-paste error messages** - 10 menit
6. **🟢 LOW: Add pagination search param** - 10 menit
7. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Fetch Pattern | State | Validation | Schema | Loading State | Overall |
|--------|--------------|-------|------------|--------|---------------|---------|
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ⚠️ Some missing | 🟢 |
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ⚠️ Some missing | 🟢 |
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ⚠️ Missing | 🟢 |
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Good | ✅ Good | 🟢 |
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ⚠️ Some missing | 🟢 |
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ Good | ❌ WRONG | ✅ Good | 🟢⭐ |
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ⚠️ Inconsistent | ✅ Good | 🟢 |
| Visi Misi PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | ✅ Good | 🟢⭐⭐ |
| Dasar Hukum PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | ✅ Good | 🟢⭐⭐ |
| Permohonan Informasi | ⚠️ Mixed | ⚠️ Good | ✅ **Best** | ❌ **4 models WRONG** | ✅ Good | 🟡 |
| **Daftar Informasi** | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ⚠️ Some missing | 🟢 |
**Daftar Informasi PPID Highlights:**
-**Sticky header table** - Unique feature untuk better UX
-**HTML tag stripping** untuk preview - Good practice
-**Responsive button text** - Attention to detail
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
- ⚠️ **Copy-paste errors** dari module "berita"
---
## 🎯 UNIQUE FEATURES OF DAFTAR INFORMASI MODULE
**Best Table Implementation:**
1.**Sticky header table** - Unique feature!
2.**HTML tag stripping** untuk preview deskripsi
3.**Responsive button text** - "Tambah" vs "Tambah Baru"
4.**Fixed column widths** - 25%, 40%, 20%
5.**Minimum table width** - 700px untuk readability
**Best Practices:**
1.**Sticky header** - Best practice untuk long lists
2.**HTML stripping** - Good practice untuk rich text preview
3.**Loading state management** - Proper dengan finally block
4.**Original data tracking** - Edit form reset yang proper
5.**Date formatting** - Proper untuk input type="date"
**Critical Issues:**
1.**Schema deletedAt SALAH** - Same issue seperti modul PPID lain
2.**Fetch pattern inconsistency** - findUnique, edit, delete pakai fetch manual
3.**Copy-paste error messages** - Dari module "berita"
---
**Catatan:** **Daftar Informasi PPID adalah MODULE DENGAN TABLE IMPLEMENTATION TERBAIK** dengan sticky header dan HTML tag stripping untuk preview. Module ini juga punya attention to detail dengan responsive button text.
**Unique Strengths:**
1.**Sticky header table** - Best table UX
2.**HTML tag stripping** - Best practice untuk preview
3.**Responsive button text** - Attention to detail
4.**Fixed column widths** - Consistent layout
5.**Date formatting** - Proper handling
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 414
model DaftarInformasiPublik {
id String @id @default(cuid())
jenisInformasi String
deskripsi String
tanggal DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_daftar_informasi
```
```diff
🔴 FIX COPY-PASTE ERRORS (10 MENIT):
File: edit/page.tsx
// Line ~80
- console.error('Error updating berita:', error);
+ console.error('Error updating daftar informasi:', error);
- toast.error('Terjadi kesalahan saat memperbarui berita');
+ toast.error('Terjadi kesalahan saat memperbarui daftar informasi');
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST TABLE IMPLEMENTATION**! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**Daftar Informasi PPID Module adalah BEST PRACTICE untuk:**
1.**Sticky header table** - Best practice untuk long lists
2.**HTML tag stripping** - Good practice untuk rich text preview
3.**Responsive button text** - Attention to detail
4.**Fixed column widths** - Consistent layout
5.**Date formatting** - Proper handling untuk date inputs
**Modules lain bisa belajar dari Daftar Informasi:**
- **ALL MODULES WITH TABLES:** Use sticky header untuk better UX
- **ALL MODULES WITH RICH TEXT:** Strip HTML tags untuk preview
- **ALL MODULES:** Responsive text untuk buttons
- **ALL MODULES:** Fixed column widths untuk consistency
- **ALL MODULES:** Proper date formatting untuk date inputs
---
**File Location:** `QC/PPID/QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md` 📄

View File

@@ -0,0 +1,821 @@
# QC Summary - Dasar Hukum PPID Module
**Scope:** Preview Dasar Hukum, Edit Dasar Hukum dengan Rich Text Editor
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Dasar Hukum PPID | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ✅ Baik | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Design**
- ✅ Preview layout yang clean dengan logo desa
- ✅ Responsive design (mobile & desktop)
- ✅ Loading states dengan Skeleton
- ✅ Empty state handling yang informatif
- ✅ Edit button yang prominent
- ✅ Divider visual yang jelas antara Judul dan Content
### **2. Rich Text Editor (Tiptap)**
- ✅ Full-featured editor dengan toolbar lengkap (reuse dari PPIDTextEditor)
- ✅ Extensions: Bold, Italic, Underline, Highlight, Link, dll
- ✅ Text alignment (left, center, justify, right)
- ✅ Heading levels (H1-H4)
- ✅ Lists (bullet & ordered)
- ✅ Blockquote, code, superscript, subscript
- ✅ Undo/Redo
- ✅ Sticky toolbar untuk UX yang lebih baik
-**Dynamic import dengan `ssr: false`** untuk menghindari hydration issues! ✅
### **3. Form Component Structure**
- ✅ Reusable PPIDTextEditor component (shared dengan Visi Misi)
- ✅ Proper TypeScript typing
- ✅ Controlled components dengan onChange handler
- ✅ SSR handling yang proper dengan dynamic import
**Code Example (✅ EXCELLENT):**
```typescript
// edit/page.tsx - Line ~13-17
const PPIDTextEditor = dynamic(
() => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
{ ssr: false } // ✅ Disable SSR untuk avoid hydration mismatch
);
```
**Verdict:****EXCELLENT** - Proper SSR handling!
---
### **4. State Management**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
-**ApiFetch consistency** - Semua operasi pakai ApiFetch! ✅
- ✅ Zod validation untuk form data
**Code Example (✅ EXCELLENT):**
```typescript
// state file - Line ~20-45
findById: {
data: null as DasarHukumForm | null,
loading: false,
initialize() {
stateDasarHukumPPID.findById.data = {
id: '',
judul: '',
content: '',
} as DasarHukumForm;
},
async load(id: string) {
try {
stateDasarHukumPPID.findById.loading = true; // ✅ Start loading
const res = await ApiFetch.api.ppid.dasarhukumppid["find-by-id"].get({
query: { id },
});
if (res.status === 200) {
stateDasarHukumPPID.findById.data = res.data?.data ?? null;
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data dasar hukum");
} finally {
stateDasarHukumPPID.findById.loading = false; // ✅ Stop loading
}
},
}
```
**Verdict:****SANGAT BAIK** - State management sudah konsisten dengan ApiFetch!
---
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Reset form mengembalikan ke data original
- ✅ Rich text content handling yang proper
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~20-45
const [formData, setFormData] = useState({ judul: '', content: '' });
const [originalData, setOriginalData] = useState({
judul: '',
content: '',
});
// Initialize from global state
useEffect(() => {
if (dasarHukumState.findById.data) {
setFormData({
judul: dasarHukumState.findById.data.judul ?? '',
content: dasarHukumState.findById.data.content ?? '',
});
setOriginalData({
judul: dasarHukumState.findById.data.judul ?? '',
content: dasarHukumState.findById.data.content ?? '',
});
}
}, [dasarHukumState.findById.data]);
// Line ~65 - Handle reset
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
content: originalData.content,
});
toast.info("Form dikembalikan ke data awal");
};
```
**Verdict:****BAIK** - Original data tracking sudah implementasi dengan baik!
---
### **6. Rich Text Validation**
- ✅ Custom validation function untuk rich text content
- ✅ Check empty content setelah remove HTML tags
- ✅ Validation untuk kedua fields (judul & content)
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~25-35
const isRichTextEmpty = (content: string) => {
// Remove HTML tags and check if the resulting text is empty
const plainText = content.replace(/<[^>]*>/g, '').trim();
return plainText === '' || content.trim() === '<p></p>' || content.trim() === '<p><br></p>';
};
const isFormValid = () => {
return (
!isRichTextEmpty(formData.judul) &&
!isRichTextEmpty(formData.content)
);
};
```
**Verdict:****EXCELLENT** - Rich text validation yang comprehensive!
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH**
**Lokasi:** `prisma/schema.prisma` (line 385)
**Masalah:**
```prisma
model DasarHukumPPID {
id String @id @default(cuid())
judul String @db.Text
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
**Contoh Issue:**
```prisma
// Record baru dibuat
CREATE DasarHukumPPID {
judul: "Judul 1",
content: "Content 1",
// deletedAt otomatis ter-set ke now() ❌
// isActive: true ✅
}
// Query untuk data aktif (seharusnya return data ini)
prisma.dasarHukumPPID.findMany({
where: { deletedAt: null, isActive: true }
})
// ❌ Return kosong! Karena deletedAt sudah ter-set
```
**Rekomendasi:** Fix schema:
```prisma
model DasarHukumPPID {
id String @id @default(cuid())
judul String @db.Text
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. HTML Injection Risk - dangerouslySetInnerHTML**
**Lokasi:** `page.tsx` (preview page)
**Masalah:**
```typescript
// Line ~65-75
<Title
order={3}
ta="center"
lh={{ base: 1.15, md: 1.1 }}
fw="bold"
c={colors['blue-button']}
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} // ❌ No sanitization
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>
// Line ~80-90 (Content)
<Text
dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }} // ❌ No sanitization
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
fontSize: '1rem',
lineHeight: 1.55,
textAlign: 'justify',
}}
/>
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
- Security vulnerability
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedJudul = DOMPurify.sanitize(listDasarHukum.findById.data.judul);
const sanitizedContent = DOMPurify.sanitize(listDasarHukum.findById.data.content);
<Title
dangerouslySetInnerHTML={{ __html: sanitizedJudul }}
// ...
/>
<Text
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
// ...
/>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
**Priority:** 🔴 **HIGH** (**Security concern**)
**Effort:** Low
---
#### **3. Missing Delete/Hard Delete Protection**
**Lokasi:** `page.tsx`, `edit/page.tsx`
**Masalah:**
- ❌ Tidak ada tombol delete untuk Dasar Hukum (correct - single record)
-**GOOD:** Single record pattern yang benar
- ⚠️ **ISSUE:** Tidak ada konfirmasi sebelum update (direct save)
**Issue:** User bisa accidentally save changes tanpa konfirmasi.
**Rekomendasi:** Add confirmation dialog sebelum save:
```typescript
const handleSubmit = () => {
// Check if data has changed
if (formData.judul === originalData.judul && formData.content === originalData.content) {
toast.info('Tidak ada perubahan');
return;
}
// Show confirmation
const confirmed = window.confirm('Apakah Anda yakin ingin mengubah Dasar Hukum PPID?');
if (!confirmed) return;
// Then save...
};
```
**Priority:** 🔴 Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts`
**Masalah:**
```typescript
// Line ~40
console.error((error as Error).message);
// Line ~65
console.error((error as Error).message);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Missing Loading State di Submit Button**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~130-140
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
// ...
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Issue:** Button tidak check `dasarHukumState.update.loading` dari global state.
**Rekomendasi:** Check both states:
```typescript
disabled={!isFormValid() || isSubmitting || dasarHukumState.update.loading}
{isSubmitting || dasarHukumState.update.loading ? (
<Loader size="sm" color="white" />
) : (
'Simpan'
)}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **6. Zod Schema - Could Be More Specific**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts`
**Masalah:**
```typescript
// Line ~7
const templateForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"), // ⚠️ Generic
content: z.string().min(3, "Content minimal 3 karakter"), // ⚠️ Generic
});
```
**Rekomendasi:** More specific error messages:
```typescript
const templateForm = z.object({
judul: z.string().min(3, "Judul dasar hukum minimal 3 karakter"),
content: z.string().min(3, "Konten dasar hukum minimal 3 karakter"),
});
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **7. Missing Change Detection**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~75-85
const handleSubmit = () => {
try {
setIsSubmitting(true);
if (dasarHukumState.findById.data) {
// Update global state hanya saat submit
const updated = { ...dasarHukumState.findById.data, ...formData };
dasarHukumState.update.save(updated);
}
router.push('/admin/ppid/dasar-hukum');
} catch (error) {
console.error("Error updating dasar hukum:", error);
toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
} finally {
setIsSubmitting(false);
}
};
```
**Issue:** Tidak ada check apakah data sudah berubah. User bisa save tanpa perubahan.
**Rekomendasi:** Add change detection:
```typescript
const handleSubmit = () => {
// Check if data has changed
if (formData.judul === originalData.judul && formData.content === originalData.content) {
toast.info('Tidak ada perubahan');
return;
}
try {
setIsSubmitting(true);
// ... rest of save logic
}
};
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **8. Editor - Duplicate useEffect**
**Lokasi:** `PPIDTextEditor.tsx` (shared component)
**Masalah:**
```typescript
// Line ~30-35 (di PPIDTextEditor.tsx)
const editor = useEditor({
extensions: [...],
immediatelyRender: false,
content: initialContent, // ✅ Set content directly
onUpdate: ({editor}) => {
onChange(editor.getHTML()) // ✅ Handle changes
}
});
// Line ~37-42
useEffect(() => {
if (editor && initialContent !== editor.getHTML()) {
editor.commands.setContent(initialContent || '<p></p>');
}
}, [initialContent, editor]);
```
**Issue:** Ada useEffect tambahan untuk set content, padahal sudah ada di `useEditor`. Bisa menyebabkan double content update.
**Rekomendasi:** Simplify - remove useEffect:
```typescript
const editor = useEditor({
extensions: [...],
immediatelyRender: false,
content: initialContent || '<p></p>', // ✅ Set content directly
onUpdate: ({editor}) => {
onChange(editor.getHTML())
},
});
// Remove useEffect completely
```
**Priority:** 🟢 Low
**Effort:** Low (perlu update shared component)
---
#### **9. Missing Error Boundary**
**Lokasi:** `edit/page.tsx`
**Masalah:**
- Tidak ada error boundary untuk handle unexpected errors
- Jika editor gagal load, tidak ada fallback UI
**Rekomendasi:** Add error boundary:
```typescript
if (dasarHukumState.findById.error) {
return (
<Alert icon={<IconAlertCircle />} color="red">
<Text fw="bold">Error</Text>
<Text>{dasarHukumState.findById.error}</Text>
</Alert>
);
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Preview Page - Title Order Inconsistency**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~40
<Title order={3} ...>Preview Dasar Hukum PPID</Title>
// Line ~65
<Title order={3} ... dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} />
```
**Issue:** Title hierarchy agak confusing. Page title dan content title sama-sama order 3.
**Rekomendasi:** Samakan hierarchy:
```typescript
// Page title: order={2}
// Content title (judul): order={3}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Missing Toast Success After Save**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~75-90
const handleSubmit = () => {
try {
setIsSubmitting(true);
if (dasarHukumState.findById.data) {
const updated = { ...dasarHukumState.findById.data, ...formData };
dasarHukumState.update.save(updated);
}
router.push('/admin/ppid/dasar-hukum'); // ✅ Redirect tanpa toast
} catch (error) {
console.error("Error updating dasar hukum:", error);
toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
} finally {
setIsSubmitting(false);
}
};
```
**Issue:** Toast success ada di state `update.save()`, tapi user mungkin tidak lihat karena langsung redirect.
**Rekomendasi:** Add toast before redirect atau wait untuk toast selesai:
```typescript
const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (dasarHukumState.findById.data) {
const updated = { ...dasarHukumState.findById.data, ...formData };
await dasarHukumState.update.save(updated);
toast.success("Dasar Hukum berhasil diperbarui!");
setTimeout(() => {
router.push('/admin/ppid/dasar-hukum');
}, 1000); // Wait 1 second for toast to show
}
} catch (error) {
console.error("Error updating dasar hukum:", error);
toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
} finally {
setIsSubmitting(false);
}
};
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. SSR Dynamic Import - Good but Could Add Loading**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~13-17
const PPIDTextEditor = dynamic(
() => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
{ ssr: false } // ✅ Good
);
```
**Issue:** Tidak ada loading state untuk dynamic import. Jika editor lambat load, user lihat kosong.
**Rekomendasi:** Add loading option:
```typescript
const PPIDTextEditor = dynamic(
() => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
{
ssr: false,
loading: () => (
<Center py={40}>
<Loader size="sm" />
<Text ml="md">Loading editor...</Text>
</Center>
)
}
);
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | **HTML injection risk** | UI | **HIGH (Security)** | Low | **Should fix** |
| 🔴 P1 | Missing delete confirmation | UI | Medium | Low | Should fix |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Missing loading state di submit button | UI | Low | Low | Should fix |
| 🟡 M | Zod schema error messages | State | Low | Low | Optional |
| 🟢 L | Missing change detection | Edit UI | Low | Low | Optional |
| 🟢 L | Duplicate useEffect di editor | Editor | Low | Low | Optional |
| 🟢 L | Missing error boundary | UI | Low | Low | Optional |
| 🟢 L | Title order inconsistency | UI | Low | Low | Optional |
| 🟢 L | Missing toast success timing | UI | Low | Low | Optional |
| 🟢 L | SSR loading state | UI | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8.5/10) - CLEAN & SIMPLE!**
**Strengths:**
1. ✅ UI/UX clean & responsive
2.**Rich Text Editor** full-featured (Tiptap, shared component)
3.**Dynamic import dengan `ssr: false`** - Proper SSR handling! ✅
4.**State management BEST PRACTICES** - **100% ApiFetch!**
5.**Edit form reset sudah benar** (original data tracking)
6.**Rich text validation** comprehensive (check empty content)
7. ✅ Error handling comprehensive
8. ✅ Loading state management dengan finally block
9.**Reusable component** (PPIDTextEditor shared dengan Visi Misi)
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
2. ⚠️ **HTML injection risk** - dangerouslySetInnerHTML tanpa sanitization (HIGH Security)
3. ⚠️ Missing confirmation sebelum save (Medium UX)
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
3. ⚠️ **Add confirmation dialog** sebelum save
4. ⚠️ **Add change detection** untuk avoid unnecessary saves
5. ⚠️ **Fix loading state** di submit button
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH: Fix HTML injection** dengan DOMPurify - 30 menit
3. **🟡 MEDIUM: Add confirmation dialog** - 15 menit
4. **🟢 LOW: Add change detection** - 15 menit
5. **🟢 LOW: Add SSR loading state** - 10 menit
6. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Fetch Pattern | State | Edit Reset | Rich Text | SSR Handling | HTML Injection | deletedAt | Overall |
|--------|--------------|-------|------------|-----------|--------------|----------------|-----------|---------|
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | ⚠️ Present | ⚠️ Issue | 🟢 |
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Present | ⚠️ None | ⚠️ Present | ⚠️ Issue | 🟢 |
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | N/A | ⚠️ Issue | 🟢 |
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | N/A | ✅ Good | 🟢 |
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Present | ⚠️ None | ⚠️ Present | ❌ WRONG | 🟢 |
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ **Excellent** | ✅ **Best** | ⚠️ None | ⚠️ Present | ❌ WRONG | 🟢⭐ |
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ✅ Present | ⚠️ None | ⚠️ Present | ⚠️ Inconsistent | 🟢 |
| Visi Misi PPID | ✅ **100% ApiFetch!** | ✅ **Best** | ✅ Good | ✅ Present | ⚠️ None | ⚠️ Present | ❌ WRONG | 🟢⭐⭐ |
| **Dasar Hukum PPID** | ✅ **100% ApiFetch!** | ✅ **Best** | ✅ Good | ✅ Present | ✅ **EXCELLENT** | ⚠️ Present | ❌ WRONG | 🟢⭐⭐ |
**Dasar Hukum PPID Highlights:**
-**100% ApiFetch** - NO fetch manual sama sekali!
-**SSR Handling** - Dynamic import dengan `ssr: false` (UNIQUE!)
-**Reusable component** - Share PPIDTextEditor dengan Visi Misi
-**Simple & clean** - No unnecessary complexity
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
---
## 🎯 UNIQUE FEATURES OF DASAR HUKUM PPID MODULE
**Simplest & Cleanest Module:**
1.**100% ApiFetch consistency** - NO fetch manual sama sekali!
2.**SSR Handling** - Dynamic import dengan `ssr: false` (UNIQUE!)
3.**Reusable component** - Share PPIDTextEditor dengan Visi Misi
4.**Simple single record pattern** - Only 2 fields (judul, content)
5.**Rich text validation** - Check empty content
**Best Practices:**
1.**API consistency** - 100% ApiFetch
2.**SSR handling** - Best practice untuk Next.js
3.**Loading state management** proper (dengan finally block)
4.**Rich text validation** comprehensive
5.**Original data tracking** untuk reset form
6.**Component reusability** - Share editor component
**Critical Issues:**
1.**Schema deletedAt SALAH** - Same issue seperti modul PPID lain
2.**HTML injection risk** - Same issue seperti modul dengan rich text lain
---
**Catatan:** **Dasar Hukum PPID adalah MODULE PALING CLEAN** bersama Visi Misi PPID dengan codebase paling simple dan **100% PAKAI ApiFetch** (no fetch manual sama sekali!). Module ini juga **SATU-SATUNYA MODULE** yang punya proper SSR handling dengan dynamic import!
**Unique Strengths:**
1.**100% ApiFetch** - Best API consistency
2.**SSR Handling** - Best practice untuk Next.js (UNIQUE!)
3.**Component reusability** - Share editor component
4.**Simple & clean** - No unnecessary complexity
5.**Rich text validation** - Most comprehensive
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 385
model DasarHukumPPID {
id String @id @default(cuid())
judul String @db.Text
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_dasarhukum_ppid
```
```diff
🔴 FIX HTML INJECTION (30 MENIT):
File: page.tsx
+ import DOMPurify from 'dompurify';
// Line ~65
- dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.judul) }}
// Line ~80
- dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.content) }}
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dan bisa jadi **REFERENCE untuk SSR HANDLING & API CONSISTENCY**! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**Dasar Hukum PPID Module adalah BEST PRACTICE untuk:**
1.**API consistency** - 100% ApiFetch, NO fetch manual!
2.**SSR handling** - Dynamic import dengan `ssr: false`
3.**Simple state management** - Clean, straightforward
4.**Rich text validation** - Check empty content pattern
5.**Component reusability** - Share editor component
**Modules lain bisa belajar dari Dasar Hukum PPID:**
- **ALL MODULES:** Use ApiFetch consistently (NO fetch manual!)
- **ALL MODULES WITH RICH TEXT:** Use dynamic import dengan `ssr: false`
- **ALL MODULES:** Keep it simple (avoid unnecessary complexity)
- **Rich Text Modules:** Implement empty content validation
- **ALL MODULES:** Share reusable components
---
**File Location:** `QC/PPID/QC-DASAR-HUKUM-PPID-MODULE.md` 📄

913
QC/PPID/QC-IKM-MODULE.md Normal file
View File

@@ -0,0 +1,913 @@
# QC Summary - Indeks Kepuasan Masyarakat (IKM) PPID Module
**Scope:** Responden (CRUD), Grafik Kepuasan Masyarakat, Master Data (Jenis Kelamin, Rating, Kelompok Umur)
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Sub-Module | Schema | API | UI Admin | State Management | Overall |
|------------|--------|-----|----------|-----------------|---------|
| Responden | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 |
| Grafik IKM | ✅ Baik | ✅ Baik | ✅ **Excellent** | ✅ Baik | 🟢 |
| Master Data (JK, Rating, Umur) | ⚠️ Ada issue | ✅ Baik | N/A | ⚠️ Ada issue | 🟡 |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX - Grafik & Charts (UNIQUE FEATURE!)**
-**Mantine Charts** - PieChart & BarChart yang modern
-**3 Distribusi Charts**: Jenis Kelamin, Penilaian, Kelompok Umur
-**Bar Chart Tren** - Monthly respondent trends
-**Responsive design** - SimpleGrid dengan proper breakpoints
-**Empty state handling** - "Tidak ada data" message
-**Loading states** dengan Skeleton
-**Color coding** yang konsisten
-**Legend & Labels** yang informatif
-**Tooltip** untuk interactive charts
**Code Example (✅ EXCELLENT):**
```typescript
// grafik-kepuasan-masyarakat/page.tsx - Line ~100-150
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
<Title order={3} mb="md" ta="center">Tren Jumlah Responden</Title>
<Box h={320}>
<BarChart
h={300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Box>
</Paper>
```
**Verdict:****EXCELLENT** - Best chart implementation di semua modul PPID!
---
### **2. Data Processing untuk Charts**
- ✅ Automatic calculation dari data responden
- ✅ Grouping by gender, rating, age group
- ✅ Monthly aggregation untuk bar chart
- ✅ Date parsing dari multiple fields (createdAt, tanggal)
- ✅ Sorting by month/year
- ✅ Empty data handling (all values = 0)
**Code Example (✅ EXCELLENT):**
```typescript
// grafik-kepuasan-masyarakat/page.tsx - Line ~45-85
// Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) =>
item.jenisKelamin?.name?.toLowerCase() === 'laki-laki'
).length;
const totalPerempuan = data.filter((item: any) =>
item.jenisKelamin?.name?.toLowerCase() === 'perempuan'
).length;
// Update gender chart data
setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]);
// Process data for bar chart (group by month)
const monthYearMap = new Map<string, number>();
data.forEach((item: any) => {
const dateValue = item.tanggal || item.createdAt;
const parsedDate = new Date(dateValue);
const month = parsedDate.getMonth() + 1;
const year = parsedDate.getFullYear();
const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
});
```
**Verdict:****EXCELLENT** - Data processing yang comprehensive!
---
### **3. Form Validation**
- ✅ Zod schema untuk semua forms
- ✅ Required field validation
- ✅ Multiple dropdown dependencies (Jenis Kelamin, Rating, Umur)
- ✅ Loading state handling untuk dropdown data
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~10-16
const templateResponden = z.object({
name: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(1, "Tanggal harus diisi"),
jenisKelaminId: z.string().min(1, "Jenis kelamin harus diisi"),
ratingId: z.string().min(1, "Rating harus diisi"),
kelompokUmurId: z.string().min(1, "Kelompok umur harus diisi"),
});
```
**Verdict:****BAIK** - Validation yang proper!
---
### **4. State Management**
- ✅ Proper typing dengan Prisma types (untuk findUnique)
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
-**ApiFetch consistency** untuk create & findMany! ✅
- ✅ Multiple related states (responden, jenisKelamin, rating, umur)
- ✅ Reusable Select component di edit page
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~60-95
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
responden.findMany.loading = true; // ✅ Start loading
responden.findMany.page = page;
responden.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.responden["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
responden.findMany.data = res.data.data || [];
responden.findMany.total = res.data.total || 0;
responden.findMany.totalPages = res.data.totalPages || 1;
}
} catch (error) {
console.error("Error loading responden:", error);
responden.findMany.data = [];
responden.findMany.total = 0;
responden.findMany.totalPages = 1;
} finally {
responden.findMany.loading = false; // ✅ Stop loading
}
},
}
```
**Verdict:****BAIK** - State management sudah proper dengan ApiFetch!
---
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Reset form mengembalikan ke data original
- ✅ Reusable ControlledSelect component
- ✅ Error display untuk setiap field
**Code Example (✅ EXCELLENT):**
```typescript
// edit/page.tsx - Line ~40-60
const [formData, setFormData] = useState<FormResponden>({
name: '',
tanggal: '',
jenisKelaminId: '',
ratingId: '',
kelompokUmurId: '',
});
const [originalData, setOriginalData] = useState<FormResponden>({
name: '',
tanggal: '',
jenisKelaminId: '',
ratingId: '',
kelompokUmurId: '',
});
// Load data
const data = await state.update.load(id);
setFormData(newForm);
setOriginalData(newForm); // ✅ Save original
// Line ~130 - Handle reset
const handleResetForm = () => {
setFormData({ ...originalData });
toast.info('Form dikembalikan ke data awal');
};
// Line ~150 - Reusable Select component
const ControlledSelect = ({
label, value, onChange, options, error, loading,
}) => (
<Select
label={<Text fw="bold" fz="sm" mb={4}>{label}</Text>}
value={value}
onChange={(val) => onChange(val || '')}
data={options}
disabled={loading}
clearable
searchable
required
radius="md"
error={error}
/>
);
```
**Verdict:****EXCELLENT** - Best edit form implementation dengan reusable component!
---
### **6. Master Data Management**
- ✅ 3 master data tables: Jenis Kelamin, Rating, Kelompok Umur
- ✅ Separate proxy states untuk masing-masing
- ✅ Auto-load saat create/edit form
- ✅ Proper filtering dan mapping untuk dropdown options
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH (5 MODELS AFFECTED!)**
**Lokasi:** `prisma/schema.prisma` (line 266-297)
**Masalah:**
```prisma
model Responden {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
model JenisKelaminResponden {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
model PilihanRatingResponden {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
model UmurResponden {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- **5 models affected!** (Responden + 3 master data + StrukturPPID)
**Rekomendasi:** Fix semua schema:
```prisma
model Responden {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable
isActive Boolean @default(true)
}
model JenisKelaminResponden {
// ...
deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model PilihanRatingResponden {
// ...
deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model UmurResponden {
// ...
deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration untuk 5 models)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. State Management - Fetch Pattern Inconsistency**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany)
const res = await ApiFetch.api.landingpage.responden["create"].post(form);
const res = await ApiFetch.api.landingpage.responden["findMany"].get({ query });
// ❌ Pattern 2: fetch manual (findUnique, update)
const res = await fetch(`/api/landingpage/responden/${id}`);
const response = await fetch(`/api/landingpage/responden/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
const res = await ApiFetch.api.landingpage.responden[id].get();
if (res.data?.success) {
responden.findUnique.data = res.data.data;
} else {
toast.error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error:", error);
toast.error("Gagal memuat data");
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di findUnique, update methods)
---
#### **3. Type Safety - Any Usage di findMany**
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts`
**Masalah:**
```typescript
// Line ~58
data: null as any[] | null, // ❌ Using 'any'
// Line ~270
data: null as any[] | null, // ❌ Using 'any'
// Line ~370
data: null as any[] | null, // ❌ Using 'any'
// Line ~470
data: null as any[] | null, // ❌ Using 'any'
```
**Issue:** findMany data tidak typed dengan Prisma types, hanya findUnique yang typed.
**Rekomendasi:** Gunakan typed data:
```typescript
// Define type
type RespondenWithRelations = Prisma.RespondenGetPayload<{
include: {
jenisKelamin: true;
rating: true;
kelompokUmur: true;
};
}>;
// Use typed data
data: null as RespondenWithRelations[] | null,
```
**Priority:** 🟡 Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** Multiple places di state file
**Masalah:**
```typescript
// Line ~80
console.error("Failed to load responden:", res.data?.message);
// Line ~85
console.error("Error loading responden:", error);
// Line ~110
console.error("Failed to fetch data", res.status, res.statusText);
// Line ~114
console.error("Error loading responden:", error);
// ... dan banyak lagi di semua master data states
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Missing Loading State di Submit Button**
**Lokasi:** `create/page.tsx`
**Masalah:**
```typescript
// Line ~100-110
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
// ...
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Verdict:****SUDAH BENAR** - Loading state sudah ada di create page!
**Priority:** 🟢 None
**Effort:** None
---
#### **6. Missing Loading State di Edit Submit Button**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~220-230
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
// ⚠️ Missing state.update.loading check
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Issue:** Button tidak check `state.update.loading` dari global state.
**Rekomendasi:** Check both states:
```typescript
disabled={!isFormValid() || isSubmitting || state.update.loading}
{isSubmitting || state.update.loading ? (
<Loader size="sm" color="white" />
) : (
'Simpan'
)}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **7. Pagination onChange Tidak Include Search**
**Lokasi:** `responden/page.tsx`
**Masalah:**
```typescript
// Line ~200-210
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ⚠️ Missing search parameter
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang.
**Rekomendasi:** Include search:
```typescript
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Missing Delete Function di Master Data**
**Lokasi:** State file untuk master data
**Masalah:**
```typescript
// Line ~270-290 (jenisKelaminResponden)
delete: {
loading: false,
async byId(id: string) {
// ✅ Method sudah ada
},
}
```
**Verdict:****SUDAH BENAR** - Delete function sudah ada di semua master data!
**Priority:** 🟢 None
**Effort:** None
---
#### **9. Duplicate Loading State Assignment**
**Lokasi:** State file untuk master data
**Masalah:**
```typescript
// Line ~290-295 (jenisKelaminResponden.create)
async create() {
// ...
jenisKelaminResponden.create.loading = true; // ✅ First assignment
try {
jenisKelaminResponden.create.loading = true; // ❌ Duplicate!
const res = await ApiFetch.api.landingpage.jeniskelaminresponden["create"].post(form);
// ...
}
}
```
**Rekomendasi:** Remove duplicate:
```typescript
async create() {
// ...
jenisKelaminResponden.create.loading = true; // ✅ Keep only this
try {
// Remove duplicate line
const res = await ApiFetch.api.landingpage.jeniskelaminresponden["create"].post(form);
// ...
}
}
```
**Priority:** 🟢 Low
**Effort:** Low (ada di 3 master data states)
---
#### **10. Inconsistent Toast Messages**
**Lokasi:** State file
**Masalah:**
```typescript
// Line ~45 (responden.create)
toast.success("Responden berhasil ditambahkan");
// Line ~295 (jenisKelaminResponden.create)
toast.success("Jenis kelamin responden berhasil ditambahkan");
// Line ~400 (pilihanRatingResponden.create)
toast.success("Jenis kelamin responden berhasil ditambahkan"); // ❌ Wrong message!
// Line ~505 (kelompokUmurResponden.create)
toast.success("Kelompok umur responden berhasil ditambahkan");
```
**Issue:** Copy-paste error di pilihanRatingResponden (masih "Jenis kelamin responden").
**Rekomendasi:** Fix message:
```typescript
toast.success("Pilihan rating responden berhasil ditambahkan");
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Missing Edit Page untuk Master Data**
**Lokasi:** Module structure
**Masalah:**
- ✅ Responden: Create, Edit, Detail, Delete
- ❌ Jenis Kelamin: Create, Delete (NO EDIT)
- ❌ Rating: Create, Delete (NO EDIT)
- ❌ Kelompok Umur: Create, Delete (NO EDIT)
**Issue:** Master data tidak bisa diedit, hanya bisa delete & create ulang.
**Rekomendasi:** Consider adding edit pages untuk master data jika diperlukan:
```typescript
// Add edit method di state (sudah ada)
// Add edit page di UI
/admin/ppid/indeks-kepuasan-masyarakat/jenis-kelamin/[id]/edit
```
**Priority:** 🟢 Low (business decision)
**Effort:** Medium
---
#### **12. Search Placeholder Tidak Spesifik**
**Lokasi:** `responden/page.tsx`
**Masalah:**
```typescript
// Line ~30-35
<HeaderSearch
title="Data Responden"
placeholder="Cari nama responden..." // ✅ Actually pretty specific!
// ...
/>
```
**Verdict:****SUDAH BENAR** - Placeholder sudah spesifik!
**Priority:** 🟢 None
**Effort:** None
---
#### **13. Chart Color Hardcoding**
**Lokasi:** `grafik-kepuasan-masyarakat/page.tsx`
**Masalah:**
```typescript
// Line ~55-60
setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' }, // ❌ Hardcoded
]);
setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' }, // ❌ Hardcoded
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' }, // ❌ Hardcoded
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' }, // ❌ Hardcoded
]);
```
**Rekomendasi:** Define color constants:
```typescript
// con/colors.ts atau file terpisah
export const chartColors = {
primary: colors['blue-button'],
success: '#10A85AFF',
warning: '#FFA500',
danger: '#FF4500',
};
// Use in chart data
{ name: 'Perempuan', value: totalPerempuan, color: chartColors.success },
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **14. Date Parsing di Detail Page**
**Lokasi:** `responden/[id]/page.tsx`
**Masalah:**
```typescript
// Line ~65-70
<Text fz="md" c="dimmed">{
stateDetail.findUnique.data?.tanggal
? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
: '-'
}</Text>
```
**Verdict:****SUDAH BENAR** - Date formatting yang proper!
**Priority:** 🟢 None
**Effort:** None
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH (5 models)** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Missing loading state di edit submit | UI | Low | Low | Should fix |
| 🟡 M | Pagination missing search param | UI | Low | Low | Should fix |
| 🟢 L | Duplicate loading state assignment | State | Low | Low | Optional |
| 🟢 L | Inconsistent toast messages | State | Low | Low | Should fix |
| 🟢 L | Missing edit page untuk master data | UI | Low | Medium | Optional |
| 🟢 L | Chart color hardcoding | UI | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8/10)**
**Strengths:**
1.**Grafik & Charts EXCELLENT** - Best chart implementation di semua modul PPID!
2.**Data processing comprehensive** - Automatic calculation dari data responden
3.**3 Distribusi Charts** - Jenis Kelamin, Penilaian, Kelompok Umur
4.**Bar Chart Tren** - Monthly respondent trends
5. ✅ UI/UX clean & responsive
6. ✅ Form validation comprehensive
7. ✅ State management dengan ApiFetch untuk create & findMany
8.**Edit form EXCELLENT** - Reusable ControlledSelect component
9. ✅ Original data tracking untuk reset form
10. ✅ Master data management proper (3 tables)
11. ✅ Loading state management dengan finally block
12. ✅ Mobile cards responsive
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - 5 models affected (CRITICAL)
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. ⚠️ Type safety (any usage di findMany)
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** untuk 5 models dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. ⚠️ **Improve type safety** dengan remove `any` usage
4. ⚠️ **Add loading state** di edit submit button
5. ⚠️ **Fix duplicate loading state** di master data create methods
6. ⚠️ **Fix copy-paste toast message** di pilihanRatingResponden
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** untuk 5 models - 1 jam (perlu migration)
2. **🔴 HIGH: Refactor findUnique, update** ke ApiFetch - 1 jam
3. **🟡 MEDIUM: Improve type safety** - 30 menit
4. **🟡 MEDIUM: Add loading state** di edit submit - 10 menit
5. **🟡 MEDIUM: Fix pagination search param** - 10 menit
6. **🟢 LOW: Fix duplicate loading state** - 15 menit
7. **🟢 LOW: Fix toast message** - 5 menit
8. **🟢 LOW: Define chart color constants** - 15 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Charts | Data Processing | Edit Form | State | Schema | Overall |
|--------|--------|----------------|-----------|-------|--------|---------|
| Profil | ❌ None | N/A | ✅ Good | ⚠️ Good | ⚠️ deletedAt | 🟢 |
| Desa Anti Korupsi | ❌ None | N/A | ✅ Good | ⚠️ Good | ⚠️ deletedAt | 🟢 |
| SDGs Desa | ❌ None | N/A | ✅ Good | ⚠️ Good | ⚠️ deletedAt | 🟢 |
| APBDes | ❌ None | ✅ Items hierarchy | ✅ Good | ⚠️ Good | ✅ Good | 🟢 |
| Prestasi Desa | ❌ None | N/A | ✅ Good | ⚠️ Good | ❌ WRONG | 🟢 |
| PPID Profil | ❌ None | N/A | ✅ **Excellent** | ✅ **Best** | ❌ WRONG | 🟢⭐ |
| Struktur PPID | ❌ None | N/A | ✅ Good | ✅ Good | ⚠️ Inconsistent | 🟢 |
| Visi Misi PPID | ❌ None | N/A | ✅ Good | ✅ **Best** | ❌ WRONG | 🟢⭐⭐ |
| Dasar Hukum PPID | ❌ None | N/A | ✅ Good | ✅ **Best** | ❌ WRONG | 🟢⭐⭐ |
| Permohonan Informasi | ❌ None | N/A | ❌ Missing | ⚠️ Good | ❌ **4 models WRONG** | 🟡 |
| Permohonan Keberatan | ❌ None | N/A | ❌ Missing | ⚠️ Good | ❌ WRONG | 🟡 |
| Daftar Informasi | ❌ None | N/A | ✅ Good | ⚠️ Good | ❌ WRONG | 🟢 |
| **IKM (Indeks Kepuasan)** | ✅ **EXCELLENT** | ✅ **EXCELLENT** | ✅ **Excellent** | ⚠️ Good | ❌ **5 models WRONG** | 🟢 |
**IKM Highlights:**
-**BEST CHARTS** - Mantine Charts (PieChart, BarChart)
-**BEST DATA PROCESSING** - Automatic calculation & grouping
-**BEST EDIT FORM** - Reusable ControlledSelect component
- ⚠️ **5 models affected** - deletedAt issue (most affected module!)
---
## 🎯 UNIQUE FEATURES OF IKM MODULE
**Most Advanced Data Visualization:**
1.**Mantine Charts** - PieChart & BarChart (UNIQUE!)
2.**3 Distribusi Charts** - Jenis Kelamin, Penilaian, Kelompok Umur
3.**Monthly Trend Chart** - Bar chart dengan grouping
4.**Automatic Calculation** - Filter & count dari data
5.**Reusable Select Component** - ControlledSelect di edit form
6.**3 Master Data Tables** - Jenis Kelamin, Rating, Kelompok Umur
**Best Practices:**
1.**Chart implementation** - Best practice untuk data visualization
2.**Data processing** - Comprehensive calculation & grouping
3.**Reusable components** - ControlledSelect untuk dropdowns
4.**Loading state management** - Proper dengan finally block
5.**Original data tracking** - Edit form reset yang proper
6.**Master data management** - Separate states untuk masing-masing
**Critical Issues:**
1.**5 models dengan deletedAt SALAH** - Most affected module!
2.**Fetch pattern inconsistency** - findUnique, update pakai fetch manual
3.**Type safety** - any usage di findMany
---
**Catatan:** **IKM adalah MODULE DENGAN CHARTS & DATA VISUALIZATION TERBAIK** dengan Mantine Charts implementation yang excellent. Module ini juga punya **BEST EDIT FORM** dengan reusable ControlledSelect component. Tapi juga **MODULE DENGAN PALING BANYAK MODEL AFFECTED** oleh deletedAt issue (5 models!).
**Unique Strengths:**
1.**Charts EXCELLENT** - Best data visualization
2.**Data processing** - Automatic calculation & grouping
3.**Edit form EXCELLENT** - Reusable ControlledSelect
4.**Master data management** - 3 separate tables
5.**Monthly trends** - Bar chart dengan grouping
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (1 JAM + MIGRATION):
File: prisma/schema.prisma
Line: 266-297
# Fix 5 models:
model Responden {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model JenisKelaminResponden {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model PilihanRatingResponden {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model UmurResponden {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_ikm
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST CHARTS & DATA VISUALIZATION**! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**IKM Module adalah BEST PRACTICE untuk:**
1.**Charts & Data Visualization** - Mantine Charts implementation
2.**Data Processing** - Automatic calculation & grouping
3.**Reusable Components** - ControlledSelect untuk dropdowns
4.**Edit Form** - Original data tracking dengan reusable components
5.**Master Data Management** - Separate states untuk multiple tables
**Modules lain bisa belajar dari IKM:**
- **ALL MODULES WITH CHARTS:** Use Mantine Charts (PieChart, BarChart)
- **ALL MODULES WITH DROPDOWNS:** Use reusable ControlledSelect component
- **ALL MODULES:** Automatic data calculation untuk charts
- **ALL MODULES:** Master data management dengan separate states
- **ALL MODULES:** Edit form dengan original data tracking
---
**File Location:** `QC/PPID/QC-IKM-MODULE.md` 📄

View File

@@ -0,0 +1,844 @@
# QC Summary - Permohonan Informasi Publik PPID Module
**Scope:** List Permohonan Informasi Publik, Detail Permohonan
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Permohonan Informasi Publik | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Design**
- ✅ Preview layout yang clean dengan responsive design
- ✅ Loading states dengan Skeleton
- ✅ Empty state handling yang informatif dengan icon
- ✅ Search functionality dengan debounce (1000ms)
- ✅ Pagination yang konsisten
- ✅ Desktop table + mobile cards responsive
- ✅ Icon integration (User, ID, Phone, Info) untuk visual clarity
### **2. Table & Card Layout**
- ✅ Fixed layout table untuk consistency
- ✅ Column headers dengan icon yang descriptive
- ✅ Row numbering otomatis (index + 1)
- ✅ Text truncation dengan lineClamp untuk long text
- ✅ Mobile card view dengan proper information hierarchy
**Code Example (✅ GOOD):**
```typescript
// page.tsx - Line ~130-180
<Table highlightOnHover
layout="fixed" // ✅ PENTING - consistent column widths
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} ta="center" w={60}>No</TableTh>
<TableTh fz="sm" fw={600}>
<Group gap={5}>
<IconUser size={16} />
Nama
</Group>
</TableTh>
<TableTh fz="sm" fw={600}>
<Group gap={5}>
<IconId size={16} />
NIK
</Group>
</TableTh>
// ...
</TableTr>
</TableThead>
```
**Verdict:****BAIK** - Table layout dengan icon yang helpful!
---
### **3. State Management**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
-**ApiFetch consistency** untuk create & findMany! ✅
- ✅ Zod validation untuk form data dengan specific rules
- ✅ Separate proxy states untuk related data (jenisInformasi, caraMemperoleh, dll)
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~110-150
findMany: {
data: null as Prisma.PermohonanInformasiPublikGetPayload<{...}>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
statepermohonanInformasiPublik.findMany.loading = true; // ✅ Start loading
statepermohonanInformasiPublik.findMany.page = page;
statepermohonanInformasiPublik.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
statepermohonanInformasiPublik.findMany.data = res.data.data || [];
statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
}
} catch (error) {
console.error("Error loading permohonan:", error);
statepermohonanInformasiPublik.findMany.data = [];
// ...
} finally {
statepermohonanInformasiPublik.findMany.loading = false; // ✅ Stop loading
}
},
}
```
**Verdict:****BAIK** - State management sudah proper dengan ApiFetch!
---
### **4. Zod Schema Validation**
- ✅ Comprehensive validation untuk semua fields
- ✅ Specific error messages untuk setiap field
- ✅ Phone number length validation (3-15 chars)
- ✅ NIK length validation (3-16 chars)
- ✅ Email format validation
- ✅ Required field validation untuk dropdowns
**Code Example (✅ EXCELLENT):**
```typescript
// state file - Line ~8-22
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
nik: z
.string()
.min(3, "NIK minimal 3 karakter")
.max(16, "NIK maksimal 16 angka"), // ✅ Specific validation
notelp: z
.string()
.min(3, "Nomor Telepon minimal 3 karakter")
.max(15, "Nomor Telepon maksimal 15 angka"), // ✅ Specific validation
alamat: z.string().min(3, "Alamat minimal 3 karakter"),
email: z.string().min(3, "Email minimal 3 karakter"),
jenisInformasiDimintaId: z.string().nonempty(), // ✅ Required dropdown
caraMemperolehInformasiId: z.string().nonempty(), // ✅ Required dropdown
caraMemperolehSalinanInformasiId: z.string().nonempty(), // ✅ Required dropdown
});
```
**Verdict:****EXCELLENT** - Validation yang comprehensive!
---
### **5. Related Data Management**
- ✅ Separate proxy states untuk dropdown data
- ✅ JenisInformasiDiminta, CaraMemperolehInformasi, CaraMemperolehSalinanInformasi
- ✅ Proper typing dengan Prisma types
- ✅ ApiFetch consistency untuk load dropdown data
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~24-40
const jenisInformasiDiminta = proxy({
findMany: {
data: null as Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[] | null,
async load() {
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
if (res.status === 200) {
jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
}
},
},
});
```
**Verdict:****BAIK** - Related data management yang proper!
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH (MULTIPLE MODELS)**
**Lokasi:** `prisma/schema.prisma` (line 435-467)
**Masalah:**
```prisma
model PermohonanInformasiPublik {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
model JenisInformasiDiminta {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
model CaraMemperolehInformasi {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
model CaraMemperolehSalinanInformasi {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
- **4 models affected!** (PermohonanInformasiPublik + 3 related models)
**Rekomendasi:** Fix semua schema:
```prisma
model PermohonanInformasiPublik {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
model JenisInformasiDiminta {
// ...
deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model CaraMemperolehInformasi {
// ...
deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model CaraMemperolehSalinanInformasi {
// ...
deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration untuk 4 models)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. State Management - Fetch Pattern Inconsistency**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany, dropdowns)
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(form);
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get({ query });
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
// ❌ Pattern 2: fetch manual (findUnique)
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
const res = await ApiFetch.api.ppid.permohonaninformasipublik[id].get();
if (res.data?.success) {
statepermohonanInformasiPublik.findUnique.data = res.data.data;
} else {
toast.error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error:", error);
toast.error("Gagal memuat data");
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di findUnique method)
---
#### **3. Console.log di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts`
**Masalah:**
```typescript
// Line ~70
console.log(caraMemperolehSalinanInformasi); // ❌ Debug log
// Line ~160
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
// Line ~165
console.error("Error loading permohonan keberatan informasi:", error);
// Line ~185
console.error("Failed to fetch program inovasi:", res.statusText);
// Line ~188
console.error("Error fetching program inovasi:", error);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **4. Missing Delete/Hard Delete Protection**
**Lokasi:** `page.tsx`, `[id]/page.tsx`
**Masalah:**
- ❌ Tidak ada tombol delete untuk Permohonan Informasi (correct - read-only data)
-**GOOD:** Read-only pattern yang benar untuk data permohonan
- ⚠️ **ISSUE:** Tidak ada fitur untuk mark sebagai "processed" atau "completed"
**Issue:** User tidak bisa update status permohonan (pending → processed → completed).
**Rekomendasi:** Add status management:
```prisma
// Add to schema
model PermohonanInformasiPublik {
// ...
status String @default("pending") // pending, processed, completed
processedAt DateTime?
processedBy String?
}
```
```typescript
// Add action buttons di detail page
<Group>
<Button color="yellow" onClick={() => updateStatus("processed")}>
Mark as Processed
</Button>
<Button color="green" onClick={() => updateStatus("completed")}>
Mark as Completed
</Button>
</Group>
```
**Priority:** 🔴 Medium
**Effort:** Medium (perlu schema change + UI update)
---
### **🟡 MEDIUM**
#### **5. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts`
**Masalah:**
```typescript
// Line ~145
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;
```
**Rekomendasi:** Gunakan typed query:
```typescript
// Define type
interface FindManyQuery {
page: number | string;
limit?: number | string;
search?: string;
}
// Use typed query
const query: FindManyQuery = { page, limit };
if (search) query.search = search;
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Error Message Tidak Konsisten**
**Lokasi:** Multiple places
**Masalah:**
```typescript
// Line ~160
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
// ⚠️ Wrong module name - ini "permohonan informasi publik" bukan "keberatan"
// Line ~165
console.error("Error loading permohonan keberatan informasi:", error);
// ⚠️ Same issue
// Line ~185
console.error("Failed to fetch program inovasi:", res.statusText);
// ⚠️ Wrong module name - ini "permohonan informasi" bukan "program inovasi"
// Line ~188
console.error("Error fetching program inovasi:", error);
// ⚠️ Same issue
```
**Issue:** Copy-paste error dari module lain!
**Rekomendasi:** Fix error messages:
```typescript
console.error("Failed to load permohonan informasi publik:", res.data?.message);
console.error("Error loading permohonan informasi publik:", error);
console.error("Failed to fetch permohonan informasi:", res.statusText);
console.error("Error fetching permohonan informasi:", error);
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **7. Pagination onChange Tidak Include Search**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~250-260
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ⚠️ Missing search parameter
window.scrollTo(0, 0);
}}
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang.
**Rekomendasi:** Include search:
```typescript
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search
window.scrollTo(0, 0);
}}
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Missing Loading State di Detail Page**
**Lokasi:** `[id]/page.tsx`
**Masalah:**
```typescript
// Line ~20-25
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [params?.id])
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
```
**Issue:** Skeleton ditampilkan untuk semua kondisi (loading, error, not found).
**Rekomendasi:** Add proper loading state:
```typescript
if (state.findUnique.loading) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
if (!state.findUnique.data) {
return (
<Alert icon={<IconAlertCircle />} color="red">
Data tidak ditemukan
</Alert>
);
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Duplicate Error Logging**
**Lokasi:** `page.tsx`, state file
**Masalah:**
```typescript
// page.tsx - Line ~160-165
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
console.error("Error loading permohonan keberatan informasi:", error);
// state file - Line ~185-188
console.error("Failed to fetch program inovasi:", res.statusText);
console.error("Error fetching program inovasi:", error);
```
**Rekomendasi:** Cukup satu logging yang informatif:
```typescript
console.error('Failed to load Permohonan Informasi Publik:', err);
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Search Placeholder Tidak Spesifik**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~70, 110
<TextInput
placeholder={"Cari nama..."} // ⚠️ Generic
// ...
/>
```
**Rekomendasi:** Lebih spesifik:
```typescript
placeholder={"Cari nama pemohon..."}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Missing Data Relationships di Detail Page**
**Lokasi:** `[id]/page.tsx`
**Masalah:**
```typescript
// Line ~60-90
<Box>
<Text fz="lg" fw="bold" mb={4}>Jenis Informasi</Text>
<Text fz="md" c="dimmed">{data.jenisInformasiDiminta?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold" mb={4}>Cara Akses Informasi</Text>
<Text fz="md" c="dimmed">{data.caraMemperolehInformasi?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold" mb={4}>Cara Akses Salinan Informasi</Text>
<Text fz="md" c="dimmed">{data.caraMemperolehSalinanInformasi?.name || '-'}</Text>
</Box>
```
**Issue:** Tidak menampilkan data `alamat` yang ada di schema.
**Rekomendasi:** Add missing field:
```typescript
<Box>
<Text fz="lg" fw="bold" mb={4}>Alamat</Text>
<Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Unused Console.log**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts`
**Masalah:**
```typescript
// Line ~70
console.log(caraMemperolehSalinanInformasi); // ❌ Debug log yang tidak terpakai
```
**Rekomendasi:** Remove:
```typescript
// Remove this line completely
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **13. Missing Empty State Icon di Mobile**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~60-75 (Desktop empty state)
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
{search
? 'Tidak ditemukan data yang sesuai dengan pencarian'
: 'Belum ada permohonan yang tercatat'
}
</Text>
</Stack>
// Line ~120-130 (Mobile - missing icon)
<Stack align="center" py={{ base: 'xl', md: 'xl' }}>
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
// ✅ Icon ada di sini juga
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
Belum ada permohonan informasi yang tercatat
</Text>
</Stack>
```
**Verdict:****SUDAH BENAR** - Icon ada di kedua empty states!
**Priority:** 🟢 None
**Effort:** None
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH (4 models)** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P1 | Missing status management | UI/Schema | Medium | Medium | Should add |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
| 🟡 M | Error message inconsistency (copy-paste) | State | Low | Low | Should fix |
| 🟡 M | Pagination missing search param | UI | Low | Low | Should fix |
| 🟢 L | Missing loading state di detail page | UI | Low | Low | Optional |
| 🟢 L | Duplicate error logging | UI/State | Low | Low | Optional |
| 🟢 L | Search placeholder tidak spesifik | UI | Low | Low | Optional |
| 🟢 L | Missing alamat field di detail page | UI | Low | Low | Optional |
| 🟢 L | Unused console.log | State | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (7.5/10)**
**Strengths:**
1. ✅ UI/UX clean & responsive
2. ✅ Table layout dengan icon yang helpful
3. ✅ Search functionality dengan debounce
4. ✅ Empty state handling yang informatif
5.**Zod validation comprehensive** dengan specific rules
6.**Related data management** proper (dropdowns)
7. ✅ State management dengan ApiFetch untuk create & findMany
8. ✅ Loading state management dengan finally block
9. ✅ Mobile cards responsive
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - 4 models affected (CRITICAL)
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. ⚠️ Missing status management untuk permohonan (pending → processed → completed)
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** untuk 4 models dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. ⚠️ **Add status management** untuk tracking status permohonan
4. ⚠️ **Fix error messages** (copy-paste error dari module lain)
5. ⚠️ **Improve type safety** dengan remove `any` usage
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** untuk 4 models - 1 jam (perlu migration)
2. **🔴 HIGH: Refactor findUnique** ke ApiFetch - 30 menit
3. **🔴 HIGH: Add status management** - 1 jam (schema + UI)
4. **🟡 MEDIUM: Fix error messages** (copy-paste) - 10 menit
5. **🟢 LOW: Add pagination search param** - 10 menit
6. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Fetch Pattern | State | Validation | Schema | Status Mgmt | Overall |
|--------|--------------|-------|------------|--------|-------------|---------|
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | N/A | 🟢 |
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | N/A | 🟢 |
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | N/A | 🟢 |
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Good | N/A | 🟢 |
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | N/A | 🟢 |
| PPID Profil | ⚠️ Mixed | ✅ Best | ✅ Good | ❌ WRONG | N/A | 🟢⭐ |
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ⚠️ Inconsistent | ✅ Active/Non-active | 🟢 |
| Visi Misi PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | N/A | 🟢⭐⭐ |
| Dasar Hukum PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | N/A | 🟢⭐⭐ |
| **Permohonan Informasi** | ⚠️ Mixed | ⚠️ Good | ✅ **Best** | ❌ **4 models WRONG** | ❌ Missing | 🟡 |
**Permohonan Informasi PPID Highlights:**
-**Best validation** - Comprehensive Zod schema dengan specific rules
-**Related data management** - Separate proxy states untuk dropdowns
-**Icon integration** - Table headers dengan icon yang helpful
- ⚠️ **4 models affected** - deletedAt issue (most affected module!)
- ⚠️ **Missing status management** - No workflow tracking
- ⚠️ **Copy-paste errors** - Error messages dari module lain
---
## 🎯 UNIQUE FEATURES OF PERMOHONAN INFORMASI MODULE
**Most Complex Data Structure:**
1.**3 related dropdown models** - JenisInformasi, CaraMemperoleh, CaraMemperolehSalinan
2.**Comprehensive validation** - Phone length, NIK length, email format
3.**Icon integration** - User, ID, Phone, Info icons di table headers
4.**Auto-increment nomor** - Automatic numbering system
5.**Missing status workflow** - Should have pending → processed → completed
**Best Practices:**
1.**Validation comprehensive** - Best Zod schema dengan specific rules
2.**Related data management** - Separate proxy states
3.**Icon integration** - Visual clarity di table headers
4.**Loading state management** - Proper dengan finally block
**Critical Issues:**
1.**4 models dengan deletedAt SALAH** - Most affected module!
2.**Fetch pattern inconsistency** - findUnique pakai fetch manual
3.**Missing status workflow** - No tracking untuk permohonan status
4.**Copy-paste error messages** - Dari module lain
---
**Catatan:** **Permohonan Informasi PPID adalah MODULE DENGAN VALIDATION TERBAIK** tapi juga **MODULE DENGAN PALING BANYAK MODEL AFFECTED** oleh deletedAt issue (4 models!). Module ini butuh status management workflow untuk tracking status permohonan.
**Unique Strengths:**
1.**Best validation** - Comprehensive Zod schema
2.**Related data management** - 3 dropdown models handled properly
3.**Icon integration** - Visual clarity
4.**Auto-increment nomor** - Automatic numbering
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (1 JAM + MIGRATION):
File: prisma/schema.prisma
Line: 435-467
model PermohonanInformasiPublik {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model JenisInformasiDiminta {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model CaraMemperolehInformasi {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
model CaraMemperolehSalinanInformasi {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_permohonan_informasi
```
```diff
🔴 ADD STATUS MANAGEMENT (1 JAM):
File: prisma/schema.prisma
model PermohonanInformasiPublik {
// ...
+ status String @default("pending") // pending, processed, completed
+ processedAt DateTime?
+ processedBy String?
}
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST VALIDATION**! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**Permohonan Informasi PPID Module adalah BEST PRACTICE untuk:**
1.**Comprehensive validation** - Zod schema dengan specific rules (phone, NIK length)
2.**Related data management** - Separate proxy states untuk dropdowns
3.**Icon integration** - Visual clarity di table headers
4.**Auto-increment numbering** - Automatic nomor urut
**Modules lain bisa belajar dari Permohonan Informasi:**
- **ALL MODULES:** Use specific validation rules (min/max length)
- **MODULES WITH DROPDOWNS:** Separate proxy states untuk related data
- **ALL MODULES:** Icon integration untuk visual clarity
- **ALL MODULES:** Auto-increment untuk numbering systems
---
**File Location:** `QC/PPID/QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md` 📄

View File

@@ -0,0 +1,771 @@
# QC Summary - Permohonan Keberatan Informasi Publik PPID Module
**Scope:** List Permohonan Keberatan, Detail Permohonan Keberatan
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Permohonan Keberatan | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Design**
- ✅ Preview layout yang clean dengan responsive design
- ✅ Loading states dengan Skeleton
- ✅ Empty state handling yang informatif dengan icon
- ✅ Search functionality dengan debounce (1000ms)
- ✅ Pagination yang konsisten
- ✅ Desktop table + mobile cards responsive
- ✅ Icon integration (User, Mail, Phone, Info) untuk visual clarity
- ✅ Consistent empty state messages
### **2. Table & Card Layout**
- ✅ Fixed layout table untuk consistency
- ✅ Column headers dengan icon yang descriptive
- ✅ Row numbering otomatis (index + 1)
- ✅ Text truncation dengan lineClamp untuk long text
- ✅ Mobile card view dengan proper information hierarchy
- ✅ Proper spacing dan gap untuk readability
**Code Example (✅ GOOD):**
```typescript
// page.tsx - Line ~130-180
<Table highlightOnHover
layout="fixed" // ✅ PENTING - consistent column widths
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} lh={1.4} ta="center">No</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
<Group gap={5}>
<IconUser size={16} />
Nama
</Group>
</TableTh>
<TableTh fz="sm" fw={600} lh={1.4}>
<Group gap={5}>
<IconMail size={16} />
Email
</Group>
</TableTh>
// ...
</TableTr>
</TableThead>
```
**Verdict:****BAIK** - Table layout dengan icon yang helpful!
---
### **3. State Management**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
-**ApiFetch consistency** untuk create & findMany! ✅
- ✅ Zod validation untuk form data dengan specific rules
- ✅ Return boolean untuk create operation (success/failure handling)
**Code Example (✅ EXCELLENT):**
```typescript
// state file - Line ~30-55
create: {
form: {} as PermohonanKeberatanInformasiForm,
loading: false,
async create() {
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
if (!cek.success) {
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
return false; // ✅ GOOD - Return false untuk failure
}
try {
permohonanKeberatanInformasi.create.loading = true;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(form);
if (res.data?.success === false) {
toast.error(res.data?.message);
return false; // ✅ GOOD - Return false untuk API failure
}
toast.success("Sukses menambahkan");
return true; // ✅ GOOD - Return true untuk success
} catch {
toast.error("Terjadi kesalahan server");
return false;
} finally {
permohonanKeberatanInformasi.create.loading = false;
}
},
}
```
**Verdict:****EXCELLENT** - Proper return value handling untuk create operation!
---
### **4. Zod Schema Validation**
- ✅ Comprehensive validation untuk semua fields
- ✅ Specific error messages untuk setiap field
- ✅ Phone number length validation (3-15 chars)
- ✅ Minimum character validation (3 characters)
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~8-15
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"),
email: z.string().min(3, "Email minimal 3 karakter"),
notelp: z
.string()
.min(3, "Nomor Telepon minimal 3 karakter")
.max(15, "Nomor Telepon maksimal 15 angka"), // ✅ Specific validation
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
});
```
**Verdict:****BAIK** - Validation yang proper dengan specific rules!
---
### **5. Empty State Handling**
- ✅ Different messages untuk search vs empty data
- ✅ Icon integration untuk visual clarity
- ✅ Proper text formatting dan centering
**Code Example (✅ GOOD):**
```typescript
// page.tsx - Line ~70-85
<Stack align="center" py="xl" ta="center">
<IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
{search
? 'Tidak ditemukan data yang sesuai dengan pencarian'
: 'Belum ada permohonan keberatan yang tercatat'
}
</Text>
</Stack>
```
**Verdict:****BAIK** - Empty state dengan conditional messages yang helpful!
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH**
**Lokasi:** `prisma/schema.prisma` (line 478)
**Masalah:**
```prisma
model FormulirPermohonanKeberatan {
id String @id @default(cuid())
name String
email String
notelp String
alasan String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
**Rekomendasi:** Fix schema:
```prisma
model FormulirPermohonanKeberatan {
id String @id @default(cuid())
name String
email String
notelp String
alasan String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. State Management - Fetch Pattern Inconsistency**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany)
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(form);
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get({ query });
// ❌ Pattern 2: fetch manual (findUnique)
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[id].get();
if (res.data?.success) {
permohonanKeberatanInformasi.findUnique.data = res.data.data;
} else {
toast.error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error:", error);
toast.error("Gagal memuat data");
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di findUnique method)
---
#### **3. Missing Delete Function**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
**Masalah:**
```typescript
// state file - Line ~100-120
// ❌ MISSING: delete method
const permohonanKeberatanInformasi = proxy({
create: { ... },
findMany: { ... },
findUnique: { ... },
// ❌ NO delete method!
});
```
**Issue:** Tidak ada cara untuk menghapus data permohonan keberatan.
**Rekomendasi:** Add delete method:
```typescript
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
permohonanKeberatanInformasi.delete.loading = true;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["del"][id].delete();
if (res.data?.success) {
toast.success(res.data.message || "Berhasil hapus permohonan keberatan");
await permohonanKeberatanInformasi.findMany.load();
} else {
toast.error(res.data?.message || "Gagal hapus permohonan keberatan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
permohonanKeberatanInformasi.delete.loading = false;
}
},
}
```
**Priority:** 🔴 Medium
**Effort:** Medium (perlu add method + API endpoint)
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
**Masalah:**
```typescript
// Line ~85
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
// Line ~90
console.error("Error loading permohonan keberatan informasi:", error);
// Line ~110
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
// Line ~114
console.error("Error fetching permohonan keberatan informasi:", error);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
**Masalah:**
```typescript
// Line ~75
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;
```
**Rekomendasi:** Gunakan typed query:
```typescript
// Define type
interface FindManyQuery {
page: number | string;
limit?: number | string;
search?: string;
}
// Use typed query
const query: FindManyQuery = { page, limit };
if (search) query.search = search;
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Missing Edit Function**
**Lokasi:** Module structure
**Masalah:**
- ❌ Tidak ada halaman edit untuk permohonan keberatan
- ❌ Tidak ada edit method di state
- ⚠️ **QUESTION:** Apakah permohonan keberatan harus bisa diedit?
**Issue:** Jika ada kesalahan input, user tidak bisa mengoreksi data.
**Rekomendasi:** Consider adding edit functionality jika diperlukan:
```typescript
// Add edit method di state
edit: {
id: "",
form: { ... },
loading: false,
async load(id: string) { ... },
async update() { ... },
}
```
**Priority:** 🟡 Low (depends on business requirement)
**Effort:** Medium
---
#### **7. Pagination onChange Tidak Include Search**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~250-260
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ⚠️ Missing search parameter
window.scrollTo(0, 0);
}}
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang.
**Rekomendasi:** Include search:
```typescript
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search
window.scrollTo(0, 0);
}}
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Missing Loading State di Detail Page**
**Lokasi:** `[id]/page.tsx`
**Masalah:**
```typescript
// Line ~20-25
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [params?.id])
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
)
}
```
**Issue:** Skeleton ditampilkan untuk semua kondisi (loading, error, not found).
**Rekomendasi:** Add proper loading state:
```typescript
if (state.findUnique.loading) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
if (!state.findUnique.data) {
return (
<Alert icon={<IconAlertCircle />} color="red">
Data tidak ditemukan
</Alert>
);
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Duplicate Error Logging**
**Lokasi:** `page.tsx`, state file
**Masalah:**
```typescript
// state file - Line ~85-90
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
console.error("Error loading permohonan keberatan informasi:", error);
// state file - Line ~110-114
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
console.error("Error fetching permohonan keberatan informasi:", error);
```
**Rekomendasi:** Cukup satu logging yang informatif:
```typescript
console.error('Failed to load Permohonan Keberatan:', err);
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Search Placeholder Tidak Spesifik**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~70, 110
<TextInput
placeholder={"Cari nama..."} // ⚠️ Generic
// ...
/>
```
**Rekomendasi:** Lebih spesifik:
```typescript
placeholder={"Cari nama pemohon..."}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Missing Data di Detail Page**
**Lokasi:** `[id]/page.tsx`
**Masalah:**
```typescript
// Line ~50-80
// Menampilkan: name, notelp, email, alasan
// ❌ MISSING: createdAt, updatedAt, atau status
```
**Issue:** Tidak menampilkan timestamp atau status permohonan.
**Rekomendasi:** Add missing fields jika ada di schema:
```typescript
<Box>
<Text fz="lg" fw="bold" mb={4}>Tanggal Pengajuan</Text>
<Text fz="md" c="dimmed">
{data.createdAt ? new Date(data.createdAt).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</Text>
</Box>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Title Inconsistency di Detail Page**
**Lokasi:** `[id]/page.tsx`
**Masalah:**
```typescript
// Line ~40
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Informasi Publik // ⚠️ Generic title
</Text>
```
**Issue:** Title seharusnya lebih spesifik "Detail Permohonan Keberatan".
**Rekomendasi:** Fix title:
```typescript
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Permohonan Keberatan Informasi Publik
</Text>
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P1 | Missing delete function | State | Medium | Medium | Should add |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
| 🟡 M | Missing edit function | State/UI | Low | Medium | Optional (business decision) |
| 🟡 M | Pagination missing search param | UI | Low | Low | Should fix |
| 🟢 L | Missing loading state di detail page | UI | Low | Low | Optional |
| 🟢 L | Duplicate error logging | UI/State | Low | Low | Optional |
| 🟢 L | Search placeholder tidak spesifik | UI | Low | Low | Optional |
| 🟢 L | Missing data di detail page | UI | Low | Low | Optional |
| 🟢 L | Title inconsistency di detail page | UI | Low | Low | Should fix |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (7.5/10)**
**Strengths:**
1. ✅ UI/UX clean & responsive
2. ✅ Table layout dengan icon yang helpful
3. ✅ Search functionality dengan debounce
4. ✅ Empty state handling yang informatif (conditional messages)
5.**Zod validation** comprehensive dengan specific rules
6.**Proper return value handling** untuk create operation (return true/false)
7. ✅ State management dengan ApiFetch untuk create & findMany
8. ✅ Loading state management dengan finally block
9. ✅ Mobile cards responsive
10. ✅ Icon integration (User, Mail, Phone, Info)
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. ⚠️ Missing delete function untuk hapus data
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. ⚠️ **Add delete method** untuk hapus data
4. ⚠️ **Consider adding edit functionality** (business decision)
5. ⚠️ **Improve type safety** dengan remove `any` usage
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH: Refactor findUnique** ke ApiFetch - 30 menit
3. **🔴 HIGH: Add delete method** - 45 menit
4. **🟡 MEDIUM: Add pagination search param** - 10 menit
5. **🟢 LOW: Fix title di detail page** - 5 menit
6. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Fetch Pattern | State | Validation | Schema | Delete | Edit | Overall |
|--------|--------------|-------|------------|--------|--------|------|---------|
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ✅ Yes | ✅ Yes | 🟢 |
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ✅ Yes | ✅ Yes | 🟢 |
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ✅ Yes | ✅ Yes | 🟢 |
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Good | ✅ Yes | ✅ Yes | 🟢 |
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ✅ Yes | ✅ Yes | 🟢 |
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ Good | ❌ WRONG | N/A | ✅ Yes | 🟢⭐ |
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ⚠️ Inconsistent | ✅ Yes | ✅ Yes | 🟢 |
| Visi Misi PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | N/A | ✅ Yes | 🟢⭐⭐ |
| Dasar Hukum PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | N/A | ✅ Yes | 🟢⭐⭐ |
| Permohonan Informasi | ⚠️ Mixed | ⚠️ Good | ✅ **Best** | ❌ **4 models WRONG** | ❌ Missing | ❌ Missing | 🟡 |
| **Permohonan Keberatan** | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ❌ **MISSING** | ❌ **MISSING** | 🟡 |
**Permohonan Keberatan PPID Highlights:**
-**Proper return value handling** - Return true/false untuk create operation
-**Icon integration** - User, Mail, Phone, Info icons di table headers
-**Conditional empty state messages** - Different messages untuk search vs empty
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
- ⚠️ **Missing delete function** - Cannot delete data
- ⚠️ **Missing edit function** - Cannot edit data (same as Permohonan Informasi)
---
## 🎯 UNIQUE FEATURES OF PERMOHONAN KEBERATAN MODULE
**Simplest Read-Only Module:**
1.**Proper return value handling** - Return true/false untuk create operation (UNIQUE!)
2.**Conditional empty state messages** - Different messages untuk search vs empty
3.**Icon integration** - User, Mail, Phone, Info icons
4.**Missing delete function** - Cannot delete data
5.**Missing edit function** - Cannot edit data
**Best Practices:**
1.**Return value handling** - Best practice untuk create operation
2.**Conditional empty state** - Good UX untuk search feedback
3.**Loading state management** - Proper dengan finally block
4.**Icon integration** - Visual clarity di table headers
**Critical Issues:**
1.**Schema deletedAt SALAH** - Same issue seperti modul PPID lain
2.**Fetch pattern inconsistency** - findUnique pakai fetch manual
3.**Missing delete function** - Cannot delete data
4.**Missing edit function** - Cannot edit data (same as Permohonan Informasi)
---
**Catatan:** **Permohonan Keberatan PPID adalah MODULE DENGAN RETURN VALUE HANDLING TERBAIK** tapi juga **MISSING DELETE & EDIT FUNCTIONS**. Module ini mirip dengan Permohonan Informasi (read-only, no delete/edit).
**Unique Strengths:**
1.**Return value handling** - Best practice (return true/false)
2.**Conditional empty state** - Good UX
3.**Icon integration** - Visual clarity
4.**Validation comprehensive** - Phone length validation
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 478
model FormulirPermohonanKeberatan {
id String @id @default(cuid())
name String
email String
notelp String
alasan String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_keberatan
```
```diff
🔴 ADD DELETE FUNCTION (45 MENIT):
File: state file
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
permohonanKeberatanInformasi.delete.loading = true;
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["del"][id].delete();
if (res.data?.success) {
toast.success(res.data.message || "Berhasil hapus permohonan keberatan");
await permohonanKeberatanInformasi.findMany.load();
} else {
toast.error(res.data?.message || "Gagal hapus permohonan keberatan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
permohonanKeberatanInformasi.delete.loading = false;
}
},
}
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST RETURN VALUE HANDLING**! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**Permohonan Keberatan PPID Module adalah BEST PRACTICE untuk:**
1.**Return value handling** - Return true/false untuk create operation
2.**Conditional empty state** - Different messages untuk search vs empty
3.**Icon integration** - Visual clarity di table headers
4.**Phone validation** - Min/max length validation
**Modules lain bisa belajar dari Permohonan Keberatan:**
- **ALL MODULES:** Use return values untuk handle create success/failure
- **ALL MODULES:** Conditional empty state messages untuk better UX
- **ALL MODULES:** Icon integration untuk visual clarity
- **ALL MODULES:** Specific validation rules (min/max length)
---
**File Location:** `QC/PPID/QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md` 📄

View File

@@ -0,0 +1,802 @@
# QC Summary - PPID Profil Module
**Scope:** Profil PPID (Preview & Edit), Rich Text Editor Forms
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Profil PPID | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ✅ Baik | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Design**
- ✅ Preview layout yang clean dengan logo desa
- ✅ Responsive design (mobile & desktop)
- ✅ Loading states dengan Skeleton
- ✅ Error handling dengan Alert component
- ✅ Empty state handling yang informatif
- ✅ Edit button yang prominent
### **2. File Upload Handling**
- ✅ Dropzone dengan preview image
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
- ✅ Validasi ukuran file (max 5MB)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
- ✅ Error handling untuk image load (onError fallback)
### **3. Rich Text Editor (Tiptap)**
- ✅ Full-featured editor dengan toolbar lengkap
- ✅ Extensions: Bold, Italic, Underline, Highlight, Link, dll
- ✅ Text alignment (left, center, justify, right)
- ✅ Heading levels (H1-H4)
- ✅ Lists (bullet & ordered)
- ✅ Blockquote, code, superscript, subscript
- ✅ Undo/Redo
- ✅ Sticky toolbar untuk UX yang lebih baik
### **4. Form Component Structure**
- ✅ Modular form components (Biodata, Riwayat, Pengalaman, Unggulan)
- ✅ Reusable EditPPIDEditor component
- ✅ Proper TypeScript typing
- ✅ Error display untuk setiap field
- ✅ Controlled components dengan onChange handler
### **5. State Management - BEST PRACTICES**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
- ✅ Reset function untuk cleanup
-**originalForm tracking** untuk reset ke data awal
**Code Example (✅ EXCELLENT):**
```typescript
// state file - Line ~85-105
editForm: {
id: "",
form: { ...defaultForm },
originalForm: { ...defaultForm }, // ✅ Track original data
loading: false,
error: null as string | null,
initialize(profileData: ProfilePPIDForm) {
this.id = profileData.id;
const data = {
name: profileData.name || "",
biodata: profileData.biodata || "",
riwayat: profileData.riwayat || "",
pengalaman: profileData.pengalaman || "",
unggulan: profileData.unggulan || "",
imageId: profileData.imageId || "",
};
this.form = { ...data };
this.originalForm = { ...data }; // ✅ Save original
},
updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value;
},
// ✅ Reset to original
resetToOriginal() {
this.form = { ...this.originalForm };
toast.info("Data dikembalikan ke kondisi awal");
},
};
```
**Verdict:****SANGAT BAIK** - State management paling baik dibanding modul lain!
---
### **6. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Preview image dari data lama
- ✅ Reset form mengembalikan ke data original
- ✅ File replacement logic (upload baru jika ada perubahan)
**Code Example (✅ EXCELLENT):**
```typescript
// edit/page.tsx - Line ~100-115
const handleResetForm = () => {
if (!allState.profile.data) return;
// Reset form ke data awal yang di-load
const original = allState.profile.data;
stateProfilePPID.editForm.form = {
name: original.name || '',
imageId: original.imageId || '',
biodata: original.biodata || '',
riwayat: original.riwayat || '',
pengalaman: original.pengalaman || '',
unggulan: original.unggulan || '',
};
// Reset preview gambar juga
setPreviewImage(original.image?.link || null);
setFile(null);
toast.info('Perubahan dibatalkan');
};
```
**Verdict:****SANGAT BAIK** - Original data tracking sudah implementasi dengan sempurna!
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH**
**Lokasi:** `prisma/schema.prisma` (line 401)
**Masalah:**
```prisma
model ProfilePPID {
// ...
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
**Contoh Issue:**
```prisma
// Record baru dibuat
CREATE ProfilePPID {
name: "PPID 1",
// deletedAt otomatis ter-set ke now() ❌
// isActive: true ✅
}
// Query untuk data aktif (seharusnya return data ini)
prisma.profilePPID.findMany({
where: { deletedAt: null, isActive: true }
})
// ❌ Return kosong! Karena deletedAt sudah ter-set
```
**Rekomendasi:** Fix schema:
```prisma
model ProfilePPID {
// ...
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. HTML Injection Risk - dangerouslySetInnerHTML**
**Lokasi:** `page.tsx` (preview page)
**Masalah:**
```typescript
// Line ~105-110
<Text
fz={{ base: 'sm', md: 'md' }}
ta="justify"
c={colors['blue-button']}
lh={1.5}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: item.biodata }} // ❌ No sanitization
/>
// Line ~115-120 (Riwayat)
dangerouslySetInnerHTML={{ __html: item.riwayat }} // ❌ No sanitization
// Line ~125-130 (Pengalaman)
dangerouslySetInnerHTML={{ __html: item.pengalaman }} // ❌ No sanitization
// Line ~135-140 (Unggulan)
dangerouslySetInnerHTML={{ __html: item.unggulan }} // ❌ No sanitization
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
- Security vulnerability
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedBiodata = DOMPurify.sanitize(item.biodata);
const sanitizedRiwayat = DOMPurify.sanitize(item.riwayat);
const sanitizedPengalaman = DOMPurify.sanitize(item.pengalaman);
const sanitizedUnggulan = DOMPurify.sanitize(item.unggulan);
<Text
dangerouslySetInnerHTML={{ __html: sanitizedBiodata }}
// ...
/>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
**Priority:** 🔴 **HIGH** (**Security concern**)
**Effort:** Low
---
#### **3. State Management - Fetch Pattern Inconsistency**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: fetch manual (profile.load)
const res = await fetch(`/api/ppid/profileppid/${id}`);
// ❌ Pattern 2: fetch manual (editForm.submit)
const res = await fetch(`/api/ppid/profileppid/${this.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.form),
});
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
- Tidak konsisten dengan modul lain yang sudah migrate ke ApiFetch
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
import ApiFetch from "@/lib/api-fetch";
// profile.load
async load(id: string) {
try {
this.loading = true;
this.error = null;
const res = await ApiFetch.api.ppid.profileppid[id].get();
if (res.data?.success) {
this.data = res.data.data;
return res.data.data;
} else {
if (res.data?.message === "Data tidak ditemukan" ||
res.data?.message === "Belum ada data profil PPID yang aktif") {
this.error = res.data.message;
return null;
} else {
throw new Error(res.data?.message || "Gagal memuat data profile");
}
}
} catch (err) {
const msg = (err as Error).message;
this.error = msg;
console.error("Load profile error:", msg);
if (msg !== "Data tidak ditemukan" && msg !== "Belum ada data profil PPID yang aktif") {
toast.error("Gagal memuat data profile");
}
return null;
} finally {
this.loading = false;
}
}
// editForm.submit
async submit() {
const check = templateForm.safeParse(this.form);
if (!check.success) {
toast.error(
check.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
);
return false;
}
this.loading = true;
this.error = null;
try {
const res = await ApiFetch.api.ppid.profileppid[this.id].put(this.form);
if (res.data?.success) {
toast.success("Berhasil update profile");
this.originalForm = { ...this.form };
return true;
} else {
throw new Error(res.data?.message || "Gagal update profile");
}
} catch (err) {
const msg = (err as Error).message;
this.error = msg;
toast.error(msg);
return false;
} finally {
this.loading = false;
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di semua methods)
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts`
**Masalah:**
```typescript
// Line ~65
console.error("Load profile error:", msg);
// edit/page.tsx - Line ~65
console.error("Error updating profile:", error);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Load profile error:", msg);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Zod Schema - Error Message Tidak Konsisten**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts`
**Masalah:**
```typescript
// Line ~6
const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"), // ✅ OK
biodata: z.string().min(3, "Biodata minimal 3 karakter"), // ✅ OK
riwayat: z.string().min(3, "Riwayat minimal 3 karakter"), // ✅ OK
pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"), // ✅ OK
unggulan: z.string().min(3, "Unggulan minimal 3 karakter"), // ✅ OK
imageId: z.string().min(1, "Gambar wajib dipilih"), // ✅ OK
});
```
**Verdict:****SUDAH BENAR** - Error messages sudah spesifik dan konsisten!
**Priority:** 🟢 None
**Effort:** None
---
#### **6. Missing Validation di Submit Button**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~270-280
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{ ... }}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Issue:** Button tidak disabled saat submitting atau form invalid. User bisa click multiple times.
**Rekomendasi:** Add disabled state:
```typescript
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={isSubmitting || allState.editForm.loading}
style={{
background: isSubmitting || allState.editForm.loading
? 'linear-gradient(135deg, #cccccc, #999999)'
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **7. Duplicate useEffect di Editor Component**
**Lokasi:** `editPPIDEditor.tsx`
**Masalah:**
```typescript
// Line ~25-30
useEffect(() => {
if (editor && value && value !== editor.getHTML()) {
editor.commands.setContent(value);
}
}, [editor, value]);
// Line ~32-40
useEffect(() => {
if (!editor) return;
const updateHandler = () => onChange(editor.getHTML());
editor.on('update', updateHandler);
return () => {
editor.off('update', updateHandler);
};
}, [editor, onChange]);
```
**Issue:** Ada 2 useEffect yang handle editor update. Yang pertama set content, yang kedua handle onChange. Bisa digabung untuk clarity.
**Rekomendasi:** Simplify:
```typescript
const editor = useEditor({
extensions: [...],
content: value, // Set content directly
onUpdate({ editor }) {
onChange(editor.getHTML());
},
});
// Remove first useEffect, keep second for cleanup
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **8. Form Label Inconsistency**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~170
<Text fw="bold">Nama Perbekel</Text>
// Should be:
<Text fw="bold">Nama PPID</Text>
```
**Issue:** Label "Nama Perbekel" tidak sesuai dengan context PPID. Ini profil PPID, bukan perbekel.
**Rekomendasi:** Fix label:
```typescript
<Text fw="bold">Nama PPID</Text>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Image Label Text Size**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~180
<Text fz={"md"} fw={"bold"}>Gambar</Text>
// Should be more specific:
<Text fz={"md"} fw={"bold"}>Foto Profil PPID</Text>
```
**Rekomendasi:** More descriptive label:
```typescript
<Text fz={"md"} fw={"bold"}>Foto Profil PPID</Text>
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Dropzone Accept Format**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~190
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
// Missing mime type specifications
```
**Rekomendasi:** Add full mime types:
```typescript
accept={{
'image/jpeg': ['.jpeg', '.jpg'],
'image/png': ['.png'],
'image/webp': ['.webp'],
}}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Preview Page - Title Order Inconsistency**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~55
<Title order={4} ...>
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
</Title>
// Line ~90
<Title order={3} ...>
{item.name}
</Title>
// Line ~100
<Title order={3} ...>
Biodata
</Title>
```
**Issue:** Title hierarchy tidak konsisten. Subtitle (order 4) lebih kecil dari content titles (order 3).
**Rekomendasi:** Samakan hierarchy:
```typescript
// Main title: order={2} atau order={3}
// Section titles: order={4}
// Name: order={3}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Missing Search Feature**
**Lokasi:** N/A (Single record module)
**Verdict:****NOT APPLICABLE** - Module ini hanya handle single record, search tidak diperlukan.
**Priority:** 🟢 None
**Effort:** None
---
#### **13. Button Loading State Tidak Konsisten**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~270-280
<Button
onClick={handleSubmit}
// ...
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Issue:** Button hanya check `isSubmitting` local state, tidak check `allState.editForm.loading` dari global state.
**Rekomendasi:** Check both states:
```typescript
disabled={isSubmitting || allState.editForm.loading}
{isSubmitting || allState.editForm.loading ? (
<Loader size="sm" color="white" />
) : (
'Simpan'
)}
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | **HTML injection risk** | UI | **HIGH (Security)** | Low | **Should fix** |
| 🔴 P1 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Missing validation di submit button | UI | Low | Low | Should fix |
| 🟢 L | Duplicate useEffect di editor | Editor | Low | Low | Optional |
| 🟢 L | Form label inconsistency | UI | Low | Low | Should fix |
| 🟢 L | Image label text size | UI | Low | Low | Optional |
| 🟢 L | Dropzone accept format | UI | Low | Low | Optional |
| 🟢 L | Title order inconsistency | UI | Low | Low | Optional |
| 🟢 L | Button loading state inconsistency | UI | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8/10)**
**Strengths:**
1. ✅ UI/UX clean & responsive
2. ✅ File upload handling solid
3.**Rich Text Editor** full-featured (Tiptap)
4.**Modular form components** (Biodata, Riwayat, Pengalaman, Unggulan)
5.**State management BEST PRACTICES** (originalForm tracking)
6.**Edit form reset SANGAT BAIK** (original data tracking sempurna)
7. ✅ Error handling comprehensive
8. ✅ Loading state management dengan finally block
9. ✅ Modal konfirmasi hapus untuk user safety
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
2. ⚠️ **HTML injection risk** - dangerouslySetInnerHTML tanpa sanitization (HIGH Security)
3. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
3. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
4. ⚠️ **Add disabled state** di submit button
5. ⚠️ **Fix form labels** (Nama Perbekel → Nama PPID)
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH: Fix HTML injection** dengan DOMPurify - 30 menit
3. **🔴 HIGH: Refactor fetch methods** ke ApiFetch - 1 jam
4. **🟡 MEDIUM: Add disabled state** di submit button - 15 menit
5. **🟢 LOW: Fix form labels** - 10 menit
6. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | APBDes | Prestasi Desa | **PPID Profil** | Notes |
|--------|--------|-------------------|-----------|--------|---------------|-----------------|-------|
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | ✅ Good | ⚠️ findUnique missing | ✅ **Good** | PPID salah satu yang terbaik |
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | ✅ Good | ✅ Good | ✅ **EXCELLENT** | **PPID paling baik** (originalForm tracking) |
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ✅ **Good** | PPID typing lebih baik |
| File Upload | ✅ Images | ✅ Documents | ✅ Images | ✅ Dual | ✅ Images | ✅ Images | Similar |
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | ✅ Good | ✅ Good | ✅ **Good** | Consistent |
| **Schema deletedAt** | ⚠️ Issue | ⚠️ Issue | ⚠️ Issue | ✅ Good | ❌ **WRONG** | ❌ **WRONG** | **PPID CRITICAL** |
| HTML Injection | ⚠️ Present | ⚠️ Present | N/A | N/A | ⚠️ Present | ⚠️ **Present** | Security concern |
| Rich Text Editor | ✅ Present | ✅ Present | N/A | N/A | ✅ Present | ✅ **Best** | **PPID editor paling lengkap** |
| Modular Forms | ❌ None | ❌ None | N/A | ❌ None | ❌ None | ✅ **YES** | **PPID unique feature** |
| State Management | ⚠️ Good | ⚠️ Good | ⚠️ Good | ⚠️ Good | ⚠️ Good | ✅ **BEST** | **PPID state management terbaik** |
---
## 🎯 UNIQUE FEATURES OF PPID PROFIL MODULE
**Most Advanced Module:**
1.**Rich Text Editor (Tiptap)** - Full-featured dengan toolbar lengkap
2.**Modular Form Components** - Biodata, Riwayat, Pengalaman, Unggulan forms
3.**originalForm Tracking** - State management best practice (unique to PPID)
4.**Single Record Pattern** - Handle "edit" special ID untuk single profile
5.**Comprehensive Error Handling** - Special handling untuk "data not found" cases
**Best Practices:**
1.**State management PALING BAIK** dibanding semua modul lain
2.**Edit form reset PALING BAIK** (originalForm tracking sempurna)
3.**Type safety LEBIH BAIK** (minimal any usage)
4.**Loading state management PROPER** (dengan finally block)
5.**Modular component design** (reusable forms)
**Critical Issues:**
1.**Schema deletedAt SALAH** - sama seperti SDGs, Desa Anti Korupsi, Prestasi Desa
2.**HTML injection risk** - sama seperti modul lain yang pakai rich text
---
**Catatan:** Secara keseluruhan, modul **PPID Profil adalah YANG PALING BAIK** dibanding semua modul yang sudah di-QC. State management-nya adalah best practice dengan originalForm tracking yang sempurna. Rich Text Editor implementation juga paling advanced.
**Unique Strengths:**
1.**State management terbaik** - originalForm tracking untuk reset yang sempurna
2.**Rich Text Editor terlengkap** - Tiptap dengan semua extensions
3.**Modular form design** - Reusable components untuk setiap section
4.**Type safety lebih baik** - Minimal any usage
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 401
model ProfilePPID {
// ...
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_default_ppid
```
```diff
🔴 FIX HTML INJECTION (30 MENIT):
File: src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx
+ import DOMPurify from 'dompurify';
// Line ~105
- dangerouslySetInnerHTML={{ __html: item.biodata }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.biodata) }}
// Repeat for riwayat, pengalaman, unggulan
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dan bisa jadi **REFERENCE** untuk modul lain! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**PPID Profil Module adalah BEST PRACTICE untuk:**
1.**State management** - originalForm tracking pattern
2.**Edit form reset** - Comprehensive reset logic
3.**Modular form components** - Reusable design pattern
4.**Rich Text Editor** - Tiptap implementation
5.**Type safety** - Proper TypeScript typing
**Modules lain bisa belajar dari PPID Profil:**
- APBDes: Implement originalForm tracking
- Prestasi Desa: Implement originalForm tracking
- SDGs Desa: Implement originalForm tracking
- Desa Anti Korupsi: Implement originalForm tracking
- Profil (Media Sosial, Program Inovasi): Implement originalForm tracking
---
**File Location:** `QC/PPID/QC-PPID-PROFIL-MODULE.md` 📄

View File

@@ -0,0 +1,936 @@
# QC Summary - Struktur PPID Module
**Scope:** Struktur Organisasi (Organization Chart), Pegawai PPID, Posisi Organisasi
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Sub-Module | Schema | API | UI Admin | State Management | Overall |
|------------|--------|-----|----------|-----------------|---------|
| Struktur Organisasi | ✅ Baik | ✅ Baik | ✅ **Excellent** | ✅ Baik | 🟢 |
| Posisi Organisasi | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 |
| Pegawai PPID | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX - Organization Chart (UNIQUE FEATURE!)**
-**PrimeReact OrganizationChart** - Visual hierarchy yang excellent
- ✅ Interactive tree structure dengan expand/collapse
- ✅ Custom node template dengan foto, nama, dan posisi
- ✅ Responsive design dengan overflow handling
- ✅ Empty state yang informatif
- ✅ Loading state dengan spinner
**Code Example (✅ EXCELLENT):**
```typescript
// struktur-organisasi/page.tsx - Line ~45-75
const posisiMap = new Map<string, any>();
const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
for (const pegawai of aktifPegawai) {
const posisiId = pegawai.posisi.id;
if (!posisiMap.has(posisiId)) {
posisiMap.set(posisiId, {
...pegawai.posisi,
pegawaiList: [],
children: [],
});
}
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
}
// Build tree structure
let root: any[] = [];
posisiMap.forEach((posisi) => {
if (posisi.parentId) {
const parent = posisiMap.get(posisi.parentId);
if (parent) {
parent.children.push(posisi);
}
} else {
root.push(posisi);
}
});
// Convert to OrganizationChart format
function toOrgChartFormat(node: any): any {
return {
expanded: true,
type: 'person',
styleClass: 'p-person',
data: {
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
status: node.nama,
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
},
children: node.children.map(toOrgChartFormat),
};
}
```
**Verdict:****UNIQUE & EXCELLENT** - Satu-satunya modul dengan organization chart visual!
---
### **2. File Upload Handling**
- ✅ Dropzone dengan preview image
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
- ✅ Validasi ukuran file (max 5MB)
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
- ✅ URL.createObjectURL untuk preview lokal
### **3. Form Validation**
- ✅ Zod schema untuk validasi typed
- ✅ Email validation dengan regex
- ✅ Required field validation
- ✅ isFormValid() check sebelum submit
- ✅ Error toast dengan pesan spesifik
- ✅ Button disabled saat invalid/loading
### **4. CRUD Operations**
- ✅ Create dengan upload file
- ✅ FindMany dengan pagination & search
- ✅ FindUnique untuk detail
- ✅ Delete dengan hard delete
- ✅ Update dengan file replacement
-**Non-active feature** untuk soft disable pegawai
### **5. State Management**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
- ✅ Reset function untuk cleanup
- ✅ findManyAll untuk organization chart data
**Code Example (✅ GOOD):**
```typescript
// state file - Line ~270-290
findManyAll: {
data: null as Prisma.PegawaiPPIDGetPayload<{...}>[] | null,
loading: false,
search: "",
load: async (search = "") => {
posisiOrganisasi.findManyAll.loading = true; // ✅ Start loading
posisiOrganisasi.findManyAll.search = search;
try {
const query: any = { search };
if (search) query.search = search;
const res = await ApiFetch.api.ppid.strukturppid.pegawai["find-many-all"].get({ query });
if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findManyAll.data = res.data.data || [];
}
} catch (error) {
console.error("Error loading pegawai:", error);
posisiOrganisasi.findManyAll.data = [];
} finally {
posisiOrganisasi.findManyAll.loading = false; // ✅ Stop loading
}
},
}
```
**Verdict:****BAIK** - Loading state management sudah proper!
---
### **6. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Preview image dari data lama
- ✅ Reset form mengembalikan ke data original
- ✅ File replacement logic (upload baru jika ada perubahan)
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~80-115
const [originalData, setOriginalData] = useState({
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
imageUrl: "",
isActive: true,
});
// Load data
const data = await stateOrganisasi.edit.load(id);
setOriginalData({
...data,
imageUrl: data.image?.link || '',
});
setPreviewImage(data.image?.link || null);
// Line ~135 - Handle reset
const handleResetForm = () => {
setFormData({
namaLengkap: originalData.namaLengkap,
gelarAkademik: originalData.gelarAkademik,
imageId: originalData.imageId,
tanggalMasuk: originalData.tanggalMasuk,
email: originalData.email,
telepon: originalData.telepon,
alamat: originalData.alamat,
posisiId: originalData.posisiId,
isActive: originalData.isActive,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
```
**Verdict:****BAIK** - Original data tracking sudah implementasi dengan baik!
---
### **7. Unique Features**
-**Organization Chart** - Visual hierarchy tree (UNIQUE!)
-**Hierarchical Positions** - Parent-child relationships
-**Active/Non-active Toggle** - Soft disable untuk pegawai
-**Email Validation** - Regex validation untuk email format
-**Date Input Handling** - Proper date formatting untuk tanggal masuk
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - Missing deletedAt for Soft Delete**
**Lokasi:** `prisma/schema.prisma` (line 327-332, 343-351)
**Masalah:**
```prisma
model PosisiOrganisasiPPID {
// ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// ❌ MISSING: deletedAt field untuk soft delete
}
model PegawaiPPID {
// ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// ❌ MISSING: deletedAt field untuk soft delete
}
```
**Dampak:**
- **INCONSISTENT!** Model `StrukturOrganisasiPPID` punya `deletedAt`, tapi Posisi dan Pegawai tidak
- Hard delete vs soft delete inconsistency
- Data integrity issue saat delete (data hilang permanen)
- Tidak bisa restore data yang ter-delete
**Rekomendasi:** Add deletedAt field:
```prisma
model PosisiOrganisasiPPID {
// ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(null) // ✅ Add for soft delete
}
model PegawaiPPID {
// ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(null) // ✅ Add for soft delete
}
```
**Priority:** 🔴 **HIGH**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & consistency)
---
#### **2. State Management - Fetch Pattern Inconsistency**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
```typescript
// ❌ Pattern 1: ApiFetch (create, findMany, findManyAll)
const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(pegawai.create.form);
const res = await ApiFetch.api.ppid.strukturppid.pegawai["find-many"].get({ query });
const res = await ApiFetch.api.ppid.strukturppid.pegawai["find-many-all"].get({ query });
// ❌ Pattern 2: fetch manual (findUnique, edit, delete, nonActive)
const res = await fetch(`/api/ppid/strukturppid/pegawai/${id}`);
const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, { method: "DELETE" });
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, { method: "DELETE" });
```
**Dampak:**
- Code consistency buruk
- Sulit maintenance
- Type safety tidak konsisten
- Duplikasi logic error handling
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
```typescript
// ✅ Unified pattern
async load(id: string) {
try {
const res = await ApiFetch.api.ppid.strukturppid.pegawai[id].get();
if (res.data?.success) {
const data = res.data.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data;
} else {
throw new Error(res.data?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pegawai:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
}
async byId(id: string) {
try {
const res = await ApiFetch.api.ppid.strukturppid.pegawai["del"][id].delete();
if (res.data?.success) {
toast.success(res.data.message || "Berhasil hapus pegawai");
await pegawai.findMany.load();
} else {
toast.error(res.data?.message || "Gagal hapus pegawai");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
}
}
```
**Priority:** 🔴 High
**Effort:** Medium (refactor di semua methods)
---
#### **3. HTML Injection Risk - dangerouslySetInnerHTML**
**Lokasi:**
- `posisi-organisasi/page.tsx` (line ~95, 155)
- `posisi-organisasi/create/page.tsx` (CreateEditor component)
- `posisi-organisasi/[id]/edit/page.tsx` (EditEditor component)
**Masalah:**
```typescript
// ❌ Direct HTML render tanpa sanitization
<Text
fz="sm"
lh={1.5}
c="dimmed"
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
/>
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
- Security vulnerability
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedDeskripsi = DOMPurify.sanitize(item.deskripsi);
<Text
dangerouslySetInnerHTML={{ __html: sanitizedDeskripsi }}
// ...
/>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan.
**Priority:** 🔴 **HIGH** (**Security concern**)
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** Multiple places di state file
**Masalah:**
```typescript
// Line ~65
console.error("Load struktur error:", errorMessage);
// Line ~130
console.error("Update struktur error:", errorMessage);
// Line ~220
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
// Line ~224
console.error("Error fetching posisiOrganisasi:", error);
// Line ~370
console.error("Gagal fetch posisi organisasi paginated:", err);
// Line ~400
console.error("Failed to load posisiOrganisasi:", res.data?.message);
// Line ~404
console.error("Error loading posisiOrganisasi:", error);
// ... dan banyak lagi
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Type Safety - Any Usage**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
**Masalah:**
```typescript
// Line ~190
const query: any = { page, limit: appliedLimit }; // ❌ Using 'any'
if (search) query.search = search;
// Line ~215
const query: any = { search }; // ❌ Using 'any'
if (search) query.search = search;
// Line ~365
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;
// Line ~395
const query: any = { search }; // ❌ Using 'any'
if (search) query.search = search;
```
**Rekomendasi:** Gunakan typed query:
```typescript
// Define type
interface FindManyQuery {
page: number | string;
limit?: number | string;
search?: string;
}
// Use typed query
const query: FindManyQuery = { page, limit: appliedLimit };
if (search) query.search = search;
```
**Priority:** 🟡 Medium
**Effort:** Low
---
#### **6. Error Message Tidak Konsisten**
**Lokasi:** Multiple places
**Masalah:**
```typescript
// Create posisi - Line ~180
toast.error("Terjadi kesalahan saat menambahkan posisi");
// Create pegawai - Line ~280
toast.error("Terjadi kesalahan saat menambahkan pegawai");
// Delete - Line ~430
toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
// Edit - Line ~520
toast.error("Gagal memuat data");
// Update - Line ~560
toast.error("Gagal mengupdate posisi organisasi");
```
**Issue:**
- Generic error messages
- Inconsistent patterns ("Terjadi kesalahan" vs "Gagal")
- Tidak spesifik ke resource type
**Rekomendasi:** Standardisasi error messages:
```typescript
// Pattern: "[Action] [resource] gagal"
toast.error("Menambahkan Posisi Organisasi gagal");
toast.error("Menghapus Posisi Organisasi gagal");
toast.error("Memuat data Posisi Organisasi gagal");
toast.error("Memperbarui data Posisi Organisasi gagal");
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **7. Zod Schema - Error Message Tidak Konsisten**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
**Masalah:**
```typescript
// Line ~170
const templatePosisiOrganisasi = z.object({
nama: z.string().min(1, "Nama harus diisi"), // ✅ OK
deskripsi: z.string().optional(), // ⚠️ No min message
hierarki: z.number().int().positive("Hierarki harus angka positif"), // ✅ OK
});
// Line ~450
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"), // ✅ OK
gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"), // ✅ OK
imageId: z.string().min(1, "Gambar wajib dipilih"), // ✅ OK
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ✅ OK
email: z.string().email("Email tidak valid").optional(), // ⚠️ Optional tapi ada validation
telepon: z.string().min(1, "Telepom wajib diisi"), // ❌ Typo: "Telepom"
alamat: z.string().min(1, "Alamat wajib diisi"), // ✅ OK
posisiId: z.string().min(1, "Posisi wajib diisi"), // ✅ OK
isActive: z.boolean().default(true), // ✅ OK
});
```
**Rekomendasi:** Fix typo dan standardisasi:
```typescript
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama lengkap wajib diisi"),
gelarAkademik: z.string().min(1, "Gelar akademik wajib diisi"),
imageId: z.string().min(1, "Foto profil wajib diunggah"),
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"),
email: z.string().email("Format email tidak valid").optional().or(z.literal('')),
telepon: z.string().min(1, "Nomor telepon wajib diisi"), // ✅ Fix typo
alamat: z.string().min(1, "Alamat wajib diisi"),
posisiId: z.string().min(1, "Posisi wajib dipilih"),
isActive: z.boolean().default(true),
});
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **8. Pagination onChange Tidak Include Search**
**Lokasi:** `pegawai/page.tsx`
**Masalah:**
```typescript
// Line ~170
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ⚠️ Missing search parameter
window.scrollTo(0, 0);
}}
total={totalPages}
// ...
/>
```
**Issue:** Saat ganti page, search query hilang.
**Rekomendasi:** Include search:
```typescript
onChange={(newPage) => {
load(newPage, 10, debouncedSearch); // ✅ Include search
window.scrollTo(0, 0);
}}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Missing Loading State di Submit Button**
**Lokasi:** `pegawai/create/page.tsx`, `pegawai/[id]/edit/page.tsx`
**Masalah:**
```typescript
// create/page.tsx - Line ~240
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
// ...
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Issue:** Button tidak check `stateOrganisasi.create.loading` dari global state.
**Rekomendasi:** Check both states:
```typescript
disabled={!isFormValid() || isSubmitting || stateOrganisasi.create.loading}
{isSubmitting || stateOrganisasi.create.loading ? (
<Loader size="sm" color="white" />
) : (
'Simpan'
)}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Duplicate Error Logging**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// edit/page.tsx - Line ~120
} catch (error) {
console.error('Error loading pegawai:', error); // ❌ Duplicate
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
}
// edit/page.tsx - Line ~160
} catch (error) {
console.error('Error updating pegawai:', error); // ❌ Duplicate
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
}
```
**Rekomendasi:** Cukup satu logging yang informatif:
```typescript
} catch (error) {
console.error('Failed to load Pegawai:', err);
toast.error('Gagal memuat data Pegawai');
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **11. Button Label Inconsistency**
**Lokasi:** Multiple files
**Masalah:**
```typescript
// create/page.tsx - Line ~230
<Button ...>Reset</Button>
// edit/page.tsx - Line ~140
<Button ...>Batal</Button>
// Should be consistent: "Reset" atau "Batal"
```
**Rekomendasi:** Standardisasi:
```typescript
// Create: "Reset"
// Edit: "Batal" (lebih descriptive untuk cancel changes)
// OR both: "Reset" / "Batal"
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Search Placeholder Tidak Spesifik**
**Lokasi:**
- `pegawai/page.tsx`: `placeholder='Cari nama pegawai atau posisi...'` ✅ Spesifik
- `posisi-organisasi/page.tsx`: `placeholder='Cari posisi organisasi...'` ✅ OK
**Verdict:****SUDAH BENAR** - Placeholder sudah spesifik.
**Priority:** 🟢 None
**Effort:** None
---
#### **13. Non-Active Endpoint Method**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
**Masalah:**
```typescript
// Line ~490
nonActive: {
loading: false,
async byId(id: string) {
// ...
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
method: "DELETE", // ⚠️ Biasanya nonActive pakai PATCH atau PUT
});
// ...
},
}
```
**Issue:** Method "DELETE" untuk non-active agak confusing. Biasanya pakai "PATCH" atau "PUT".
**Rekomendasi:** Consider using PATCH:
```typescript
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
method: "PATCH", // ✅ More semantic for toggle active/inactive
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isActive: false }),
});
```
**Priority:** 🟢 Low
**Effort:** Low (perlu update API juga)
---
#### **14. OrganizationChart - Missing Expand/Collapse Controls**
**Lokasi:** `struktur-organisasi/page.tsx`
**Masalah:**
```typescript
// Line ~80
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
```
**Issue:** Tidak ada controls untuk expand/collapse all nodes.
**Rekomendasi:** Add toggle button:
```typescript
const [expanded, setExpanded] = useState(true);
const toggleAll = () => {
const newExpanded = !expanded;
setExpanded(newExpanded);
// Update chartData dengan expanded: newExpanded untuk semua nodes
};
return (
<Box>
<Group justify="flex-end" mb="md">
<Button size="xs" onClick={toggleAll}>
{expanded ? 'Collapse All' : 'Expand All'}
</Button>
</Group>
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
</Box>
);
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema missing deletedAt** | Schema | **HIGH** | Medium | **MUST FIX** |
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
| 🔴 P1 | **HTML injection risk** | UI | **HIGH (Security)** | Low | **Should fix** |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
| 🟡 M | Error message inconsistency | State/UI | Low | Low | Optional |
| 🟡 M | Zod schema typo ("Telepom") | State | Low | Low | Should fix |
| 🟢 L | Pagination missing search param | Pegawai UI | Low | Low | Should fix |
| 🟢 L | Missing loading state di submit button | UI | Low | Low | Optional |
| 🟢 L | Duplicate error logging | UI | Low | Low | Optional |
| 🟢 L | Button label inconsistency | UI | Low | Low | Optional |
| 🟢 L | Non-active endpoint method | API | Low | Low | Optional |
| 🟢 L | OrganizationChart expand/collapse controls | UI | Low | Low | Nice to have |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8/10)**
**Strengths:**
1.**Organization Chart** - Unique visual hierarchy feature (EXCELLENT!)
2. ✅ UI/UX clean & responsive
3. ✅ File upload handling solid
4. ✅ Form validation comprehensive (email validation, required fields)
5. ✅ State management terstruktur (Valtio)
6.**Edit form reset sudah benar** (original data tracking)
7.**Active/Non-active toggle** untuk pegawai
8. ✅ Loading state management dengan finally block
9. ✅ findManyAll untuk organization chart data
**Critical Issues:**
1. ⚠️ **Schema missing deletedAt** - Inconsistency dengan StrukturOrganisasiPPID (HIGH)
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
3. ⚠️ **HTML injection risk** di deskripsi posisi (HIGH Security)
**Areas for Improvement:**
1. ⚠️ **Add deletedAt field** ke PosisiOrganisasiPPID dan PegawaiPPID
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
3. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
4. ⚠️ **Fix typo** "Telepom" → "Telepon" di Zod schema
5. ⚠️ **Improve type safety** dengan remove `any` usage
**Recommended Next Steps:**
1. **🔴 CRITICAL: Add schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH: Fix HTML injection** dengan DOMPurify - 30 menit
3. **🔴 HIGH: Refactor fetch methods** ke ApiFetch - 1 jam
4. **🟡 MEDIUM: Fix typo** di Zod schema - 5 menit
5. **🟢 LOW: Add pagination search param** - 10 menit
6. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Unique Features | Schema | State | Edit Reset | Overall |
|--------|----------------|--------|-------|------------|---------|
| Profil | ❌ None | ✅ Good | ⚠️ Good | ✅ Good | 🟢 |
| Desa Anti Korupsi | ❌ None | ⚠️ deletedAt | ⚠️ Good | ✅ Good | 🟢 |
| SDGs Desa | ❌ None | ⚠️ deletedAt | ⚠️ Good | ✅ Good | 🟢 |
| APBDes | ✅ Dual upload, Items hierarchy | ✅ **Best** | ⚠️ Good | ✅ Good | 🟢 |
| Prestasi Desa | ❌ None | ⚠️ deletedAt | ⚠️ Good | ✅ Good | 🟢 |
| PPID Profil | ✅ Rich Text, Modular forms | ⚠️ deletedAt | ✅ **Best** | ✅ **Excellent** | 🟢⭐ |
| **Struktur PPID** | ✅ **Org Chart**, Hierarchy, Non-active | ⚠️ Inconsistent | ✅ Good | ✅ Good | 🟢 |
**Struktur PPID Highlights:**
-**UNIQUE:** Organization Chart visualization (no other module has this!)
-**UNIQUE:** Hierarchical position structure (parent-child)
-**UNIQUE:** Active/Non-active toggle feature
-**GOOD:** Email validation dengan regex
- ⚠️ **ISSUE:** Schema inconsistency (deletedAt missing di 2 models)
---
## 🎯 UNIQUE FEATURES OF STRUKTUR PPID MODULE
**Most Unique Module:**
1.**PrimeReact OrganizationChart** - Visual tree hierarchy (UNIQUE!)
2.**Parent-child position relationships** - Hierarchical structure
3.**Active/Non-active toggle** - Soft disable tanpa delete
4.**Email validation** - Regex validation untuk email format
5.**findManyAll pattern** - Load all data untuk organization chart
**Best Practices:**
1. ✅ Organization chart implementation excellent
2. ✅ Loading state management proper (dengan finally block)
3. ✅ Edit form reset comprehensive (original data tracking)
4. ✅ Email validation di form (create & edit)
5. ✅ Date input handling untuk tanggal masuk
**Critical Issues:**
1.**Schema deletedAt missing** - Inconsistency issue
2.**HTML injection risk** - Same issue as modul lain dengan rich text
---
**Catatan:** Secara keseluruhan, modul **Struktur PPID adalah YANG PALING UNIQUE** dengan Organization Chart visualization yang excellent. Module ini punya fitur-fitur yang tidak ada di modul lain (hierarchical positions, org chart, active/non-active toggle).
**Unique Strengths:**
1.**Organization Chart** - Best visual representation
2.**Hierarchical data structure** - Parent-child relationships
3.**Active/Non-active feature** - Soft disable tanpa delete
4.**Email validation** - Comprehensive form validation
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 327-332, 343-351
model PosisiOrganisasiPPID {
// ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ deletedAt DateTime? @default(null) // ✅ Add for soft delete
}
model PegawaiPPID {
// ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+ deletedAt DateTime? @default(null) // ✅ Add for soft delete
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name add_deletedat_struktur_ppid
```
```diff
🔴 FIX HTML INJECTION (30 MENIT):
File: posisi-organisasi/page.tsx
+ import DOMPurify from 'dompurify';
// Line ~95
- dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.deskripsi) }}
// Repeat for mobile view line ~155
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dan **ORGANIZATION CHART** adalah fitur yang bisa jadi **SHOWCASE**! 🎉
---
**File Location:** `QC/PPID/QC-STRUKTUR-PPID-MODULE.md` 📄

View File

@@ -0,0 +1,797 @@
# QC Summary - Visi Misi PPID Module
**Scope:** Preview Visi Misi, Edit Visi Misi dengan Rich Text Editor
**Date:** 2026-02-23
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
---
## 📊 OVERVIEW
| Aspect | Schema | API | UI Admin | State Management | Overall |
|--------|--------|-----|----------|-----------------|---------|
| Visi Misi PPID | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ✅ Baik | 🟡 Perlu fix |
---
## ✅ YANG SUDAH BAIK
### **1. UI/UX Design**
- ✅ Preview layout yang clean dengan logo desa
- ✅ Responsive design (mobile & desktop)
- ✅ Loading states dengan Skeleton
- ✅ Empty state handling yang informatif
- ✅ Edit button yang prominent
- ✅ Divider visual yang jelas antara Visi dan Misi
### **2. Rich Text Editor (Tiptap)**
- ✅ Full-featured editor dengan toolbar lengkap
- ✅ Extensions: Bold, Italic, Underline, Highlight, Link, dll
- ✅ Text alignment (left, center, justify, right)
- ✅ Heading levels (H1-H4)
- ✅ Lists (bullet & ordered)
- ✅ Blockquote, code, superscript, subscript
- ✅ Undo/Redo
- ✅ Sticky toolbar untuk UX yang lebih baik
-`immediatelyRender: false` untuk menghindari hydration mismatch
### **3. Form Component Structure**
- ✅ Modular form components (VisiPPID, MisiPPID)
- ✅ Reusable PPIDTextEditor component
- ✅ Proper TypeScript typing
- ✅ Controlled components dengan onChange handler
### **4. State Management**
- ✅ Proper typing dengan Prisma types
- ✅ Loading state management dengan finally block
- ✅ Error handling yang comprehensive
-**ApiFetch consistency** - Semua operasi pakai ApiFetch! ✅
- ✅ Zod validation untuk form data
**Code Example (✅ EXCELLENT):**
```typescript
// state file - Line ~30-50
findById: {
data: null as VisiMisiPPIDForm | null,
loading: false,
initialize() {
stateVisiMisiPPID.findById.data = {
id: "",
misi: "",
visi: "",
} as VisiMisiPPIDForm;
},
async load(id: string) {
try {
stateVisiMisiPPID.findById.loading = true; // ✅ Start loading
const res = await ApiFetch.api.ppid.visimisippid["find-by-id"].get({
query: { id },
});
if (res.status === 200) {
stateVisiMisiPPID.findById.data = res.data?.data ?? null;
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data visi misi");
} finally {
stateVisiMisiPPID.findById.loading = false; // ✅ Stop loading
}
},
}
```
**Verdict:****SANGAT BAIK** - State management sudah konsisten dengan ApiFetch!
---
### **5. Edit Form - Original Data Tracking**
- ✅ Original data state untuk reset form
- ✅ Load data existing dengan benar
- ✅ Reset form mengembalikan ke data original
- ✅ Rich text content handling yang proper
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~20-45
const [formData, setFormData] = useState({ visi: '', misi: '' });
const [originalData, setOriginalData] = useState({ visi: '', misi: '' });
// Initialize from global state
useEffect(() => {
if (visiMisi.findById.data) {
setFormData({
visi: visiMisi.findById.data.visi ?? '',
misi: visiMisi.findById.data.misi ?? '',
});
setOriginalData({
visi: visiMisi.findById.data.visi ?? '',
misi: visiMisi.findById.data.misi ?? '',
});
}
}, [visiMisi.findById.data]);
// Line ~60 - Handle reset
const handleResetForm = () => {
setFormData({
visi: originalData.visi,
misi: originalData.misi,
});
toast.info('Form dikembalikan ke data awal');
};
```
**Verdict:****BAIK** - Original data tracking sudah implementasi dengan baik!
---
### **6. Rich Text Validation**
- ✅ Custom validation function untuk rich text content
- ✅ Check empty content setelah remove HTML tags
**Code Example (✅ GOOD):**
```typescript
// edit/page.tsx - Line ~25-35
const isRichTextEmpty = (content: string) => {
// Remove HTML tags and check if the resulting text is empty
const plainText = content.replace(/<[^>]*>/g, '').trim();
return plainText === '' || content.trim() === '<p></p>' || content.trim() === '<p><br></p>';
};
const isFormValid = () => {
return (
!isRichTextEmpty(formData.visi) &&
!isRichTextEmpty(formData.misi)
);
};
```
**Verdict:****EXCELLENT** - Rich text validation yang comprehensive!
---
## ⚠️ ISSUES & SARAN PERBAIKAN
### **🔴 CRITICAL**
#### **1. Schema - deletedAt Default Value SALAH**
**Lokasi:** `prisma/schema.prisma` (line 374)
**Masalah:**
```prisma
model VisiMisiPPID {
id String @id @default(cuid())
visi String @db.Text
misi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
isActive Boolean @default(true)
}
```
**Dampak:**
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value (timestamp creation)
- Soft delete tidak berfungsi dengan benar
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
- Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
**Contoh Issue:**
```prisma
// Record baru dibuat
CREATE VisiMisiPPID {
visi: "Visi 1",
misi: "Misi 1",
// deletedAt otomatis ter-set ke now() ❌
// isActive: true ✅
}
// Query untuk data aktif (seharusnya return data ini)
prisma.visiMisiPPID.findMany({
where: { deletedAt: null, isActive: true }
})
// ❌ Return kosong! Karena deletedAt sudah ter-set
```
**Rekomendasi:** Fix schema:
```prisma
model VisiMisiPPID {
id String @id @default(cuid())
visi String @db.Text
misi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
isActive Boolean @default(true)
}
```
**Priority:** 🔴 **CRITICAL**
**Effort:** Medium (perlu migration)
**Impact:** **HIGH** (data integrity & soft delete logic)
---
#### **2. HTML Injection Risk - dangerouslySetInnerHTML**
**Lokasi:** `page.tsx` (preview page)
**Masalah:**
```typescript
// Line ~85-95
<Text
ta={{ base: "center", md: "justify" }}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }} // ❌ No sanitization
style={{ ... }}
/>
// Line ~105-115 (Misi)
<Text
ta={"justify"}
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }} // ❌ No sanitization
style={{ ... }}
/>
```
**Risk:**
- XSS attack jika admin input script malicious
- Bisa inject iframe, script tag, dll
- Security vulnerability
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
```typescript
import DOMPurify from 'dompurify';
// Sanitize sebelum render
const sanitizedVisi = DOMPurify.sanitize(listVisiMisi.findById.data.visi);
const sanitizedMisi = DOMPurify.sanitize(listVisiMisi.findById.data.misi);
<Text
dangerouslySetInnerHTML={{ __html: sanitizedVisi }}
// ...
/>
```
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
**Priority:** 🔴 **HIGH** (**Security concern**)
**Effort:** Low
---
#### **3. Missing Delete/Hard Delete Protection**
**Lokasi:** `page.tsx`, `edit/page.tsx`
**Masalah:**
- ❌ Tidak ada tombol delete untuk Visi Misi (correct - single record)
-**GOOD:** Single record pattern yang benar
- ⚠️ **ISSUE:** Tidak ada konfirmasi sebelum update (direct save)
**Issue:** User bisa accidentally save changes tanpa konfirmasi.
**Rekomendasi:** Add confirmation dialog sebelum save:
```typescript
const submit = () => {
// Check if data has changed
if (formData.visi === originalData.visi && formData.misi === originalData.misi) {
toast.info('Tidak ada perubahan');
return;
}
// Show confirmation
const confirmed = window.confirm('Apakah Anda yakin ingin mengubah Visi Misi PPID?');
if (!confirmed) return;
// Then save...
};
```
**Priority:** 🔴 Medium
**Effort:** Low
---
### **🟡 MEDIUM**
#### **4. Console.log di Production**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID.ts`
**Masalah:**
```typescript
// Line ~40
console.error((error as Error).message);
// Line ~65
console.error((error as Error).message);
```
**Rekomendasi:** Gunakan conditional logging:
```typescript
if (process.env.NODE_ENV === 'development') {
console.error("Error:", error);
}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **5. Missing Loading State di Submit Button**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~120-130
<Button
onClick={submit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
// ...
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
```
**Issue:** Button tidak check `visiMisi.update.loading` dari global state.
**Rekomendasi:** Check both states:
```typescript
disabled={!isFormValid() || isSubmitting || visiMisi.update.loading}
{isSubmitting || visiMisi.update.loading ? (
<Loader size="sm" color="white" />
) : (
'Simpan'
)}
```
**Priority:** 🟡 Low
**Effort:** Low
---
#### **6. Zod Schema - Could Be More Specific**
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID.ts`
**Masalah:**
```typescript
// Line ~7
const templateForm = z.object({
misi: z.string().min(3, "Misi minimal 3 karakter"), // ⚠️ Generic
visi: z.string().min(3, "Visi minimal 3 karakter"), // ⚠️ Generic
});
```
**Rekomendasi:** More specific error messages:
```typescript
const templateForm = z.object({
misi: z.string().min(3, "Misi PPID minimal 3 karakter"),
visi: z.string().min(3, "Visi PPID minimal 3 karakter"),
});
```
**Priority:** 🟡 Low
**Effort:** Low
---
### **🟢 LOW (Minor Polish)**
#### **7. Missing Change Detection**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~70-80
const submit = () => {
try {
setIsSubmitting(true);
if (visiMisi.findById.data) {
// update nilai global hanya saat submit
visiMisi.findById.data.visi = formData.visi;
visiMisi.findById.data.misi = formData.misi;
visiMisi.update.save(visiMisi.findById.data);
}
router.push('/admin/ppid/visi-misi-ppid');
} catch (error) {
console.error("Error updating visi misi:", error);
toast.error("Terjadi kesalahan saat memperbarui visi misi");
} finally {
setIsSubmitting(false);
}
};
```
**Issue:** Tidak ada check apakah data sudah berubah. User bisa save tanpa perubahan.
**Rekomendasi:** Add change detection:
```typescript
const submit = () => {
// Check if data has changed
if (formData.visi === originalData.visi && formData.misi === originalData.misi) {
toast.info('Tidak ada perubahan');
return;
}
try {
setIsSubmitting(true);
// ... rest of save logic
}
};
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **8. Editor - Duplicate useEffect**
**Lokasi:** `PPIDTextEditor.tsx`
**Masalah:**
```typescript
// Line ~30-35
const editor = useEditor({
extensions: [...],
immediatelyRender: false,
content: initialContent, // ✅ Set content directly
onUpdate: ({editor}) => {
onChange(editor.getHTML()) // ✅ Handle changes
}
});
// Line ~37-42
useEffect(() => {
if (editor && initialContent !== editor.getHTML()) {
editor.commands.setContent(initialContent || '<p></p>');
}
}, [initialContent, editor]);
```
**Issue:** Ada useEffect tambahan untuk set content, padahal sudah ada di `useEditor`. Bisa menyebabkan double content update.
**Rekomendasi:** Simplify - remove useEffect:
```typescript
const editor = useEditor({
extensions: [...],
immediatelyRender: false,
content: initialContent || '<p></p>', // ✅ Set content directly
onUpdate: ({editor}) => {
onChange(editor.getHTML())
},
editorProps: {
// Optional: handle content updates better
}
});
// Remove useEffect completely
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **9. Missing Error Boundary**
**Lokasi:** `edit/page.tsx`
**Masalah:**
- Tidak ada error boundary untuk handle unexpected errors
- Jika editor gagal load, tidak ada fallback UI
**Rekomendasi:** Add error boundary:
```typescript
if (visiMisi.findById.error) {
return (
<Alert icon={<IconAlertCircle />} color="red">
<Text fw="bold">Error</Text>
<Text>{visiMisi.findById.error}</Text>
</Alert>
);
}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **10. Preview Page - Hardcoded Moto PPID**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~60-70
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={{ base: 1.5, md: 1.5 }}
mt="sm"
c="black"
>
MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
</Text>
```
**Issue:** Moto PPID hardcoded di UI. Seharusnya dari database/config.
**Rekomendasi:** Move to database or config file:
```typescript
// Add to schema
model VisiMisiPPID {
// ...
moto String? @db.Text
}
// Or use config
const PPID_MOTO = "MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN";
```
**Priority:** 🟢 Low
**Effort:** Medium (perlu schema change)
---
#### **11. Title Order Inconsistency**
**Lokasi:** `page.tsx`
**Masalah:**
```typescript
// Line ~45
<Title order={3} ...>Preview Visi Misi PPID</Title>
// Line ~65
<Title order={2} ...>MOTO PPID DESA DARMASABA</Title>
// Line ~80
<Title order={2} ...>VISI PPID</Title>
// Line ~100
<Title order={2} ...>MISI PPID</Title>
```
**Issue:** Title hierarchy agak confusing. Page title (order 3) lebih kecil dari section titles (order 2).
**Rekomendasi:** Samakan hierarchy:
```typescript
// Page title: order={2}
// Section titles: order={3}
```
**Priority:** 🟢 Low
**Effort:** Low
---
#### **12. Missing Toast Success After Save**
**Lokasi:** `edit/page.tsx`
**Masalah:**
```typescript
// Line ~70-85
const submit = () => {
try {
setIsSubmitting(true);
if (visiMisi.findById.data) {
visiMisi.findById.data.visi = formData.visi;
visiMisi.findById.data.misi = formData.misi;
visiMisi.update.save(visiMisi.findById.data);
}
router.push('/admin/ppid/visi-misi-ppid'); // ✅ Redirect tanpa toast
} catch (error) {
console.error("Error updating visi misi:", error);
toast.error("Terjadi kesalahan saat memperbarui visi misi");
} finally {
setIsSubmitting(false);
}
};
```
**Issue:** Toast success ada di state `update.save()`, tapi user mungkin tidak lihat karena langsung redirect.
**Rekomendasi:** Add toast before redirect atau wait untuk toast selesai:
```typescript
const submit = async () => {
try {
setIsSubmitting(true);
if (visiMisi.findById.data) {
visiMisi.findById.data.visi = formData.visi;
visiMisi.findById.data.misi = formData.misi;
await visiMisi.update.save(visiMisi.findById.data);
toast.success("Visi Misi berhasil diperbarui!");
setTimeout(() => {
router.push('/admin/ppid/visi-misi-ppid');
}, 1000); // Wait 1 second for toast to show
}
} catch (error) {
console.error("Error updating visi misi:", error);
toast.error("Terjadi kesalahan saat memperbarui visi misi");
} finally {
setIsSubmitting(false);
}
};
```
**Priority:** 🟢 Low
**Effort:** Low
---
## 📋 RINGKASAN ACTION ITEMS
| Priority | Issue | Module | Impact | Effort | Status |
|----------|-------|--------|--------|--------|--------|
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
| 🔴 P0 | **HTML injection risk** | UI | **HIGH (Security)** | Low | **Should fix** |
| 🔴 P1 | Missing delete confirmation | UI | Medium | Low | Should fix |
| 🟡 M | Console.log in production | State | Low | Low | Optional |
| 🟡 M | Missing loading state di submit button | UI | Low | Low | Should fix |
| 🟡 M | Zod schema error messages | State | Low | Low | Optional |
| 🟢 L | Missing change detection | Edit UI | Low | Low | Optional |
| 🟢 L | Duplicate useEffect di editor | Editor | Low | Low | Optional |
| 🟢 L | Missing error boundary | UI | Low | Low | Optional |
| 🟢 L | Hardcoded Moto PPID | UI | Low | Medium | Optional |
| 🟢 L | Title order inconsistency | UI | Low | Low | Optional |
| 🟢 L | Missing toast success timing | UI | Low | Low | Optional |
---
## ✅ KESIMPULAN
### **Overall Quality: 🟢 BAIK (8.5/10) - CLEANEST MODULE!**
**Strengths:**
1. ✅ UI/UX clean & responsive
2.**Rich Text Editor** full-featured (Tiptap)
3.**Modular form components** (Visi, Misi)
4.**State management BEST PRACTICES** - **ONLY MODULE YANG 100% ApiFetch!**
5.**Edit form reset sudah benar** (original data tracking)
6.**Rich text validation** comprehensive (check empty content)
7. ✅ Error handling comprehensive
8. ✅ Loading state management dengan finally block
9.`immediatelyRender: false` untuk menghindari hydration mismatch
**Critical Issues:**
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
2. ⚠️ **HTML injection risk** - dangerouslySetInnerHTML tanpa sanitization (HIGH Security)
3. ⚠️ Missing confirmation sebelum save (Medium UX)
**Areas for Improvement:**
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
2. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
3. ⚠️ **Add confirmation dialog** sebelum save
4. ⚠️ **Add change detection** untuk avoid unnecessary saves
5. ⚠️ **Fix loading state** di submit button
**Recommended Next Steps:**
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
2. **🔴 HIGH: Fix HTML injection** dengan DOMPurify - 30 menit
3. **🟡 MEDIUM: Add confirmation dialog** - 15 menit
4. **🟢 LOW: Add change detection** - 15 menit
5. **🟢 LOW: Polish minor issues** - 30 menit
---
## 📈 COMPARISON WITH OTHER MODULES
| Module | Fetch Pattern | State | Edit Reset | Rich Text | HTML Injection | deletedAt | Overall |
|--------|--------------|-------|------------|-----------|----------------|-----------|---------|
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | ⚠️ Present | ⚠️ Issue | 🟢 |
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Present | ⚠️ Present | ⚠️ Issue | 🟢 |
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | ⚠️ Issue | 🟢 |
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | ✅ Good | 🟢 |
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Present | ⚠️ Present | ❌ WRONG | 🟢 |
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ **Excellent** | ✅ **Best** | ⚠️ Present | ❌ WRONG | 🟢⭐ |
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ✅ Present | ⚠️ Present | ⚠️ Inconsistent | 🟢 |
| **Visi Misi PPID** | ✅ **100% ApiFetch!** | ✅ **Best** | ✅ Good | ✅ Present | ⚠️ Present | ❌ WRONG | 🟢⭐⭐ |
**Visi Misi PPID Highlights:**
-**ONLY MODULE** yang 100% konsisten pakai ApiFetch! (NO fetch manual!)
-**CLEANEST CODE** - Simple, straightforward, no complexity
-**Rich text validation** paling comprehensive (check empty content)
-**Best state management** pattern (ApiFetch consistency)
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
---
## 🎯 UNIQUE FEATURES OF VISI MISI PPID MODULE
**Simplest & Cleanest Module:**
1.**100% ApiFetch consistency** - NO fetch manual sama sekali! (UNIQUE!)
2.**Simple single record pattern** - Only 2 fields (visi, misi)
3.**Rich text validation** - Check empty content after remove HTML tags
4.**Modular editor components** - VisiPPID, MisiPPID separate
5.**No file upload** - Simplest form (text only)
**Best Practices:**
1.**ApiFetch 100%** - Best practice untuk API consistency
2.**Loading state management** proper (dengan finally block)
3.**Rich text validation** comprehensive
4.**Original data tracking** untuk reset form
5.**`immediatelyRender: false`** - Avoid hydration mismatch
**Critical Issues:**
1.**Schema deletedAt SALAH** - Same issue seperti modul PPID lain
2.**HTML injection risk** - Same issue seperti modul dengan rich text lain
---
**Catatan:** **Visi Misi PPID adalah MODULE PALING CLEAN** dengan codebase paling simple dan **SATU-SATUNYA MODULE YANG 100% PAKAI ApiFetch** (no fetch manual sama sekali!). Module ini bisa jadi **REFERENCE** untuk API consistency!
**Unique Strengths:**
1.**100% ApiFetch** - Best API consistency (NO fetch manual!)
2.**Simple & clean** - No unnecessary complexity
3.**Rich text validation** - Most comprehensive
4.**Best state management** pattern
**Priority Action:**
```diff
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 374
model VisiMisiPPID {
id String @id @default(cuid())
visi String @db.Text
misi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
- deletedAt DateTime @default(now())
+ deletedAt DateTime? @default(null)
isActive Boolean @default(true)
}
# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_visimisi_ppid
```
```diff
🔴 FIX HTML INJECTION (30 MENIT):
File: page.tsx
+ import DOMPurify from 'dompurify';
// Line ~85
- dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.visi) }}
// Line ~105
- dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.misi) }}
```
Setelah fix critical issues, module ini **PRODUCTION-READY** dan bisa jadi **REFERENCE untuk API CONSISTENCY**! 🎉
---
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
**Visi Misi PPID Module adalah BEST PRACTICE untuk:**
1.**API consistency** - 100% ApiFetch, NO fetch manual!
2.**Simple state management** - Clean, straightforward
3.**Rich text validation** - Check empty content pattern
4.**Modular editor components** - Separate Visi & Misi
**Modules lain bisa belajar dari Visi Misi PPID:**
- **ALL MODULES:** Use ApiFetch consistently (NO fetch manual!)
- **ALL MODULES:** Keep it simple (avoid unnecessary complexity)
- **Rich Text Modules:** Implement empty content validation
- **ALL MODULES:** Proper loading state management
---
**File Location:** `QC/PPID/QC-VISI-MISI-PPID-MODULE.md` 📄

271
QWEN.md Normal file
View File

@@ -0,0 +1,271 @@
# 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
## Qwen Added Memories
- **GitHub Workflow Execution**: Project ini memiliki 3 workflow GitHub Action:
1. `publish.yml` - Build & push Docker image ke GHCR (manual trigger, butuh input: stack_env + tag)
2. `re-pull.yml` - Re-pull Docker image di Portainer (manual trigger, butuh input: stack_name + stack_env)
3. `docker-publish.yml` - Auto build & push saat ada tag versi v*
Workflow bisa dijalankan via GitHub CLI: `gh workflow run <nama.yml> -f param=value --ref branch`
Setelah commit ke branch deployment (dev/stg/prod), otomatis trigger workflow publish + re-pull untuk deploy ke server.
- **Deployment Workflow Sistematis**:
1. **Version Bump** - Update `version` di `package.json` sebelum deploy (ikuti semver: major.minor.patch)
2. **Commit** - Commit perubahan + version bump dengan pesan yang jelas
3. **Buat Branch dan Push ke Branch yang baru dibuat** - Untuk branchnya buat sesuai dengan apa yang dikerjakan dengan format [apa-yang-dikerjakan]-[date-time]
4. **Push ke 2 Remote** - Push ke 2 remote origin dan deploy
5. **Merge ke Branch** - Merge ke branch target (biasanya `stg` untuk staging atau `prod` untuk production) ke 2 remote origin dan deploy
6. **Trigger publish.yml** - Gunakan GitHub API atau CLI dengan: `ref: main`, `stack_env: stg`, `tag: <versi-dari-package.json>`
7. **Tunggu publish selesai** - Workflow harus completed baru lanjut ke re-pull
8. **Trigger re-pull.yml** - Gunakan GitHub API atau CLI dengan: `ref: main`, `stack_name: desa-darmasaba`, `stack_env: stg`
Branch deployment: `stg` (staging) atau `prod` (production)
Version format di package.json: `"version": "major.minor.patch"`
- **Deployment Workflow HARUS Sequential (Berurutan)**:
Saat deploy ke stg atau prod, workflow TIDAK BOLEH dijalankan bersamaan. Harus menunggu yang pertama SELESAI total baru trigger yang kedua.
**Urutan yang BENAR:**
1. ✅ **publish.yml** - Tunggu sampai SELESAI (status: ✓ success)
2. ✅ **Setelah publish selesai**, baru trigger **re-pull.yml**
**JANGAN trigger keduanya bersamaan!** Ini akan menyebabkan race condition karena re-pull akan menarik image yang belum selesai di-build.
**Cara cek workflow selesai via GitHub CLI:**
```bash
gh run watch <publish_run_id>
# Tunggu sampai ada checkmark ✓
```

678
STRUKTUR-PROJEK.md Normal file
View File

@@ -0,0 +1,678 @@
# Dokumentasi Struktur Proyek - Desa Darmasaba
## 1. Ringkasan Proyek
**Desa Darmasaba** adalah aplikasi web komprehensif untuk layanan pemerintahan desa di Desa Darmasaba, Kabupaten Badung, Bali. Aplikasi ini berfungsi sebagai platform digital untuk layanan pemerintah, informasi publik, dan keterlibatan masyarakat.
### Tech Stack
| Kategori | Teknologi |
|----------|-----------|
| **Framework Frontend** | Next.js 15 dengan App Router |
| **Bahasa** | TypeScript (strict mode) |
| **Styling** | Mantine UI v7/v8 + Custom CSS |
| **Backend API** | Elysia.js (high-performance TypeScript framework) |
| **Database** | PostgreSQL |
| **ORM** | Prisma 6.3.1 |
| **Runtime** | Bun |
| **State Management** | Jotai + Valtio + SWR |
| **Autentikasi** | iron-session + JWT |
| **File Storage** | Seafile |
| **Rich Text Editor** | TipTap |
| **Charts** | Recharts + Chart.js |
| **Maps** | Leaflet + react-leaflet |
| **UI Components** | Mantine, PrimeReact, Framer Motion |
| **Validasi** | Zod |
| **Testing** | Vitest (unit), Playwright (E2E) |
| **Deployment** | Docker + GitHub Actions + Portainer |
| **Registry** | GitHub Container Registry (GHCR) |
---
## 2. Struktur Direktori
```
desa-darmasaba/
├── .github/workflows/ # GitHub Actions CI/CD
│ ├── docker-publish.yml # Auto build & push saat tag v*
│ ├── publish.yml # Manual build & push ke GHCR
│ ├── re-pull.yml # Manual re-pull image di Portainer
│ └── script/ # Script deployment
├── prisma/
│ ├── schema.prisma # Database schema (2413 baris, 100+ model)
│ ├── seed.ts # Database seeder utama
│ └── _seeder_list/ # Data seed per modul
│ ├── desa/ # Seed berita, gallery, layanan, dll
│ ├── ekonomi/ # Seed APBDes, demografi, dll
│ ├── inovasi/ # Seed ide inovatif, desa digital
│ ├── keamanan/ # Seed keamanan, kontak darurat
│ ├── kesehatan/ # Seed fasilitas kesehatan, posyandu
│ ├── kependudukan/ # Seed data penduduk
│ ├── lingkungan/ # Seed lingkungan desa
│ ├── pendidikan/ # Seed sekolah, beasiswa
│ ├── ppid/ # Seed PPID
│ └── landing-page/ # Seed landing page
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── _com/ # Komponen global (SplashScreen, WebVitals)
│ │ ├── admin/ # Panel administrasi (protected)
│ │ │ ├── _com/ # Komponen admin shared
│ │ │ ├── (dashboard)/ # Dashboard admin dengan route groups
│ │ │ │ ├── _com/ # Komponen dashboard shared
│ │ │ │ ├── _state/ # State khusus dashboard
│ │ │ │ ├── _utils/ # Utilitas dashboard
│ │ │ │ ├── auth/ # Autentikasi admin
│ │ │ │ ├── desa/ # Admin: berita, gallery, profil, layanan
│ │ │ │ ├── ekonomi/ # Admin: APBDes, demografi, BUMDes
│ │ │ │ ├── inovasi/ # Admin: ide inovatif, desa digital
│ │ │ │ ├── keamanan/ # Admin: keamanan, kontak darurat
│ │ │ │ ├── kependudukan/# Admin: banjar, agama, umur, migrasi
│ │ │ │ ├── kesehatan/ # Admin: puskesmas, posyandu, wabah
│ │ │ │ ├── landing-page/# Admin: konten landing page
│ │ │ │ ├── lingkungan/ # Admin: konservasi, sampah, penghijauan
│ │ │ │ ├── musik/ # Admin: musik desa
│ │ │ │ ├── pendidikan/ # Admin: sekolah, beasiswa, perpustakaan
│ │ │ │ ├── ppid/ # Admin: PPID, IKM, permohonan
│ │ │ │ └── user&role/ # Admin: manajemen user & role
│ │ │ ├── auth/ # Halaman login admin
│ │ │ ├── csv/ # Upload/demo CSV
│ │ │ ├── images/ # Manajemen gambar
│ │ │ └── upload-demo/ # Demo upload
│ │ │
│ │ ├── api/ # API routes (Elysia.js)
│ │ │ ├── [[...slugs]]/ # Catch-all route untuk Elysia
│ │ │ │ ├── _lib/ # Modul API per domain
│ │ │ │ │ ├── auth/ # Autentikasi API
│ │ │ │ │ ├── desa/ # API modul desa
│ │ │ │ │ ├── ekonomi/ # API modul ekonomi
│ │ │ │ │ ├── fileStorage/ # API file storage
│ │ │ │ │ ├── inovasi/ # API modul inovasi
│ │ │ │ │ ├── keamanan/# API modul keamanan
│ │ │ │ │ ├── kependudukan/ # API modul kependudukan
│ │ │ │ │ ├── kesehatan/ # API modul kesehatan
│ │ │ │ │ ├── landing_page/ # API landing page
│ │ │ │ │ ├── lingkungan/ # API modul lingkungan
│ │ │ │ │ ├── pendidikan/ # API modul pendidikan
│ │ │ │ │ ├── ppid/ # API modul PPID
│ │ │ │ │ ├── search/ # API pencarian global
│ │ │ │ │ └── user/ # API user management
│ │ │ │ └── route.ts # Entry point Elysia server
│ │ │ ├── admin/ # API khusus admin
│ │ │ ├── auth/ # API autentikasi
│ │ │ ├── health/ # Health check endpoint
│ │ │ ├── layout/ # API layout
│ │ │ ├── news/ # API berita
│ │ │ ├── subscribe/ # API subscription (email)
│ │ │ └── tts/ # Text-to-Speech (ElevenLabs)
│ │ │
│ │ ├── context/ # React contexts
│ │ │ └── MusicContext.tsx # Context untuk pemutar musik
│ │ │
│ │ ├── darmasaba/ # Halaman publik (front-facing)
│ │ │ ├── _com/ # Komponen shared publik
│ │ │ │ ├── main-page/ # Komponen halaman utama
│ │ │ │ ├── Navbar.tsx # Navigasi utama
│ │ │ │ ├── Footer.tsx # Footer
│ │ │ │ ├── FixedPlayerBar.tsx # Music player bar
│ │ │ │ ├── LoadDataFirstClient.tsx # Data prefetching
│ │ │ │ ├── NewsReader.tsx # Component pembaca berita
│ │ │ │ ├── globalSearch.tsx # Pencarian global
│ │ │ │ └── scrollToTopButton.tsx
│ │ │ ├── (pages)/ # Halaman publik utama
│ │ │ │ ├── desa/ # Halaman: profil, berita, gallery, layanan
│ │ │ │ ├── ekonomi/ # Halaman: APBDes, BUMDes, demografi
│ │ │ │ ├── inovasi/ # Halaman: inovasi desa
│ │ │ │ ├── keamanan/ # Halaman: keamanan lingkungan
│ │ │ │ ├── kependudukan/# Halaman: data penduduk
│ │ │ │ ├── kesehatan/ # Halaman: fasilitas kesehatan
│ │ │ │ ├── lingkungan/ # Halaman: lingkungan desa
│ │ │ │ ├── module/ # Halaman modul tambahan
│ │ │ │ ├── musik/ # Halaman: musik desa
│ │ │ │ ├── pendidikan/ # Halaman: pendidikan
│ │ │ │ └── ppid/ # Halaman: PPID publik
│ │ │ ├── (tambahan)/ # Halaman tambahan
│ │ │ ├── layout.tsx # Layout utama publik
│ │ │ └── page.tsx # Landing page utama
│ │ │
│ │ ├── login/ # Halaman login
│ │ ├── registrasi/ # Halaman registrasi
│ │ ├── waiting-room/ # Halaman waiting room
│ │ ├── terms-of-service/ # Halaman syarat layanan
│ │ ├── test-upload/ # Halaman tes upload
│ │ ├── validasi/ # Halaman validasi
│ │ ├── coba/ # Halaman percobaan
│ │ ├── percobaan/ # Halaman percobaan lainnya
│ │ ├── layout.tsx # Root layout (MantineProvider)
│ │ ├── page.tsx # Root page
│ │ ├── error.tsx # Error boundary
│ │ ├── not-found.tsx # 404 page
│ │ ├── globals.css # Global styles
│ │ └── favicon.ico
│ │
│ ├── components/
│ │ └── admin/ # Komponen admin reusable
│ │ ├── AdminThemeProvider.tsx
│ │ ├── DarkModeToggle.tsx
│ │ ├── UnifiedSurface.tsx
│ │ └── UnifiedTypography.tsx
│ │
│ ├── con/ # Constants & konfigurasi
│ │ └── colors.ts # Palet warna
│ │
│ ├── lib/ # Utility functions
│ │ ├── router/ # Router utilities
│ │ ├── api-auth.ts # Autentikasi API
│ │ ├── api-fetch.ts # Helper fetch API
│ │ ├── EnvStringParse.ts # Parser environment variables
│ │ ├── prisma.ts # Prisma client instance
│ │ ├── seafile-auth-service.ts # Integrasi Seafile
│ │ └── session.ts # iron-session helper
│ │
│ ├── middlewares/ # Next.js middleware
│ ├── state/ # Global state (Jotai/Valtio)
│ │ ├── darkModeStore.ts # State dark mode
│ │ ├── state-layanan.ts # State layanan
│ │ ├── state-list-image.ts # State daftar gambar
│ │ └── state-nav.ts # State navigasi
│ │
│ ├── store/ # State management tambahan
│ └── types/ # TypeScript type definitions
├── public/ # Static assets
│ └── assets/ # Gambar, icon, dll
├── uploads/ # Directory upload (runtime)
│ └── image/ # Upload gambar
├── .env.example # Contoh environment variables
├── .gitignore
├── AGENTS.md # Panduan untuk AI coding agents
├── Dockerfile # Docker image definition
├── docker-entrypoint.sh # Entry point container
├── next.config.ts # Next.js configuration
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript configuration
├── biome.json # Biome linter config
├── eslint.config.mjs # ESLint config
├── NOTE.md # Catatan deployment
└── QWEN.md # Konteks & memori proyek
```
---
## 3. Arsitektur Aplikasi
### 3.1 Arsitektur Keseluruhan
```
┌─────────────────────────────────────────────────────────┐
│ Client (Browser) │
└────────────┬────────────────────────────┬────────────────┘
│ │
│ Next.js Pages │ API Calls
│ (SSR/CSR) │
▼ ▼
┌────────────────────────┐ ┌────────────────────────────┐
│ Next.js 15 App Router│ │ Elysia.js API Server │
│ - Pages publik │ │ - RESTful endpoints │
│ - Admin dashboard │ │ - File upload │
│ - Server components │ │ - Swagger docs (/api/docs│
│ - Client components │ │ - Static file serving │
└────────────┬───────────┘ └────────────┬───────────────┘
│ │
│ │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
│ (via Prisma ORM) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Seafile File Storage │
│ (Images & Documents) │
└─────────────────────────────────────────────────────────┘
```
### 3.2 Next.js App Router
- Menggunakan **App Router** (bukan Pages Router)
- Route groups `(dashboard)`, `(pages)`, `(tambahan)` untuk organisasi tanpa mempengaruhi URL
- Layout bersarang: root layout -> admin/darmasaba layout -> page layouts
- `force-dynamic` digunakan untuk menghindari error prerendering
- View Transitions API diaktifkan via `next-view-transitions`
### 3.3 Elysia.js API Server
- Terintegrasi sebagai **catch-all route** di `/api/[[...slugs]]/route.ts`
- Semua HTTP methods (GET, POST, PATCH, DELETE, PUT) di-handle oleh Elysia
- Plugin yang digunakan:
- `@elysiajs/cors` - CORS configuration
- `@elysiajs/static` - Static file serving dari `/uploads`
- `@elysiajs/swagger` - API documentation di `/api/docs`
- `@elysiajs/jwt` - JWT authentication
- `@elysiajs/cookie` - Cookie handling
- Endpoint file upload: `/api/upl-img`, `/api/upl-img-single`, `/api/upl-csv`
- Image serving: `/api/img/:name` dengan resize support
### 3.4 Rendering Strategy
- **Server Components**: Halaman publik untuk SEO optimal
- **Client Components**: Komponen interaktif (form, state, animasi)
- **Force Dynamic**: Beberapa halaman menggunakan `force-dynamic`
- **ISR**: Caching header untuk assets (1 jam cache)
---
## 4. Modul Domain
### 4.1 Profil Desa (Desa)
**Admin**: `/admin/desa/*` | **Publik**: `/darmasaba/desa/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `berita` | CRUD berita/pengumuman desa |
| `gallery` | Galeri foto dan video |
| `layanan` | Manajemen layanan desa |
| `penghargaan` | Penghargaan yang diraih |
| `pengumuman` | Pengumuman publik |
| `potensi` | Potensi desa (pertanian, pariwisata, dll) |
| `profil` | Profil desa (sejarah, visi misi, lambang, maskot, perangkat) |
### 4.2 PPID (Pejabat Pengelola Informasi dan Dokumentasi)
**Admin**: `/admin/ppid/*` | **Publik**: `/darmasaba/ppid/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `profil-ppid` | Profil pejabat PPID |
| `struktur-ppid` | Struktur organisasi PPID |
| `visi-misi-ppid` | Visi dan misi PPID |
| `daftar-informasi-publik` | Daftar informasi yang tersedia |
| `dasar-hukum` | Dasar hukum PPID |
| `permohonan-informasi-publik` | Form permohonan informasi |
| `permohonan-keberatan-informasi-publik` | Form keberatan |
| `indeks-kepuasan-masyarakat` | Survei kepuasan masyarakat (IKM) |
### 4.3 Kesehatan
**Admin**: `/admin/kesehatan/*` | **Publik**: `/darmasaba/kesehatan/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `fasilitas-kesehatan` | Data puskesmas, klinik, dokter |
| `posyandu` | Manajemen posyandu |
| `program-kesehatan` | Program kesehatan desa |
| `info-wabah-penyakit` | Informasi wabah |
| `penanganan-darurat` | Prosedur penanganan darurat |
| `kontak-darurat` | Kontak darurat kesehatan |
| `data-kesehatan-warga` | Statistik kesehatan warga |
| `artikel-kesehatan` | Artikel kesehatan |
### 4.4 Ekonomi
**Admin**: `/admin/ekonomi/*` | **Publik**: `/darmasaba/ekonomi/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `APBDes` | Anggaran Pendapatan dan Belanja Desa (hierarki items + realisasi) |
| `PADesa-pendapatan-asli-desa` | Pendapatan asli desa |
| `demografi-pekerjaan` | Demografi pekerjaan penduduk |
| `jumlah-penduduk-miskin` | Data penduduk miskin |
| `jumlah-pengangguran` | Data pengangguran |
| `lowongan-kerja-lokal` | Lowongan kerja lokal |
| `pasar-desa` | Data pasar desa |
| `program-kemiskinan` | Program penanganan kemiskinan |
| `sektor-unggulan-desa` | Sektor unggulan ekonomi |
| `Struktur-Organisasi-Dan-Sk-Pengurus-BumDes` | Struktur BUMDes |
### 4.5 Kependudukan
**Admin**: `/admin/kependudukan/*` | **Publik**: `/darmasaba/kependudukan/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `data-banjar` | Data banjar (unit wilayah tradisional Bali) |
| `distribusi-agama` | Distribusi agama penduduk |
| `distribusi-umur` | Distribusi umur penduduk |
| `migrasi-penduduk` | Data migrasi (masuk/keluar) |
### 4.6 Pendidikan
**Admin**: `/admin/pendidikan/*` | **Publik**: `/darmasaba/pendidikan/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `beasiswa-desa` | Program beasiswa |
| `bimbingan-belajar-desa` | Bimbingan belajar |
| `data-pendidikan` | Data statistik pendidikan |
| `info-sekolah` | Informasi sekolah |
| `pendidikan-non-formal` | Pendidikan non-formal |
| `perpustakaan-digital` | Perpustakaan digital |
| `program-pendidikan-anak` | Program pendidikan anak |
### 4.7 Keamanan
**Admin**: `/admin/keamanan/*` | **Publik**: `/darmasaba/keamanan/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `keamanan-lingkungan-pecalang-patwal` | Keamanan lingkungan (pecalang Bali) |
| `kontak-darurat` | Kontak darurat keamanan |
| `laporan-publik` | Laporan publik |
| `pencegahan-kriminalitas` | Pencegahan kriminalitas |
| `polsek-terdekat` | Data polsek terdekat |
| `tips-keamanan` | Tips keamanan |
### 4.8 Lingkungan
**Admin**: `/admin/lingkungan/*` | **Publik**: `/darmasaba/lingkungan/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `data-lingkungan-desa` | Data lingkungan desa |
| `edukasi-lingkungan` | Edukasi lingkungan |
| `gotong-royong` | Kegiatan gotong royong |
| `konservasi-adat-bali` | Konservasi adat Bali |
| `pengelolaan-sampah-bank-sampah` | Bank sampah |
| `program-penghijauan` | Program penghijauan |
### 4.9 Inovasi
**Admin**: `/admin/inovasi/*` | **Publik**: `/darmasaba/inovasi/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `ajukan-ide-inovatif` | Form pengajuan ide inovatif |
| `desa-digital-smart-village` | Program desa digital |
| `info-teknologi-tepat-guna` | Info teknologi tepat guna |
| `kolaborasi-inovasi` | Kolaborasi inovasi |
| `layanan-online-desa` | Layanan online desa |
| `program-kreatif-desa` | Program kreatif desa |
### 4.10 Musik Desa
**Admin**: `/admin/musik/*` | **Publik**: `/darmasaba/musik/*`
- Manajemen audio dan cover musik desa
- Fixed player bar di halaman publik
- Context provider untuk state pemutar musik
### 4.11 Landing Page
**Admin**: `/admin/landing-page/*`
| Sub-modul | Fungsi |
|-----------|--------|
| `desa-anti-korupsi` | Konten anti-korupsi |
| `prestasi-desa` | Prestasi yang diraih |
| `sdgs-desa` | SDGs (Sustainable Development Goals) |
| `profil-landing-page` | Profil dan media sosial |
### 4.12 User & Role
**Admin**: `/admin/user&role/*`
- Manajemen pengguna admin
- Manajemen role dan permission
- Manajemen menu akses
---
## 5. Database Schema
### 5.1 Overview
Database menggunakan **PostgreSQL** dengan **Prisma ORM** (versi 6.3.1).
Schema terdiri dari **2413 baris** dengan **100+ model**.
### 5.2 Model Utama
#### FileStorage
Model sentral untuk semua file (gambar, dokumen, audio):
```prisma
model FileStorage {
id String @id @default(cuid())
name String @unique
realName String
path String
mimeType String
category String // "image" / "document" / "audio" / "other"
link String
isActive Boolean @default(true)
// Relasi ke 50+ model lain (Berita, PotensiDesa, GalleryFoto, dll)
}
```
#### AppMenu & AppMenuChild
Menu navigasi aplikasi:
```prisma
model AppMenu {
id String @id @default(cuid())
name String @unique
link String
isActive Boolean @default(true)
AppMenuChild AppMenuChild[]
}
```
#### User & Role (Autentikasi Admin)
- `User` - Data pengguna admin
- `Role` - Role/peran pengguna
- `Menu` - Menu akses per role
#### Modul Desa
- `Berita` - Berita desa (dengan featured image & gallery)
- `GalleryFoto` / `GalleryVideo` - Galeri media
- `Layanan` - Layanan desa
- `Pengumuman` - Pengumuman
- `PotensiDesa` - Potensi desa
- `ProfileDesaImage` - Gambar profil desa
- `ProfilPerbekel` - Profil perbekel (kepala desa)
- `PejabatDesa` - Pejabat desa
- `Penghargaan` - Penghargaan
- `PrestasiDesa` - Prestasi
- `MediaSosial` - Media sosial desa
#### Modul PPID
- `StrukturPPID` - Struktur organisasi
- `PosisiOrganisasiPPID` - Posisi dengan hierarki
- `PegawaiPPID` - Data pegawai
- `ProfilePPID` - Profil PPID
- `VisiMisiPPID` - Visi misi
- `DasarHukumPPID` - Dasar hukum
- `DaftarInformasiPublik` - Daftar informasi
- `PermohonanInformasiPublik` - Permohonan informasi
- `FormulirPermohonanKeberatan` - Formulir keberatan
- `IndeksKepuasanMasyarakat` - IKM
- `Responden` + lookup tables - Data responden IKM
#### Modul Kesehatan
- `Puskesmas` - Data puskesmas
- `Posyandu` - Data posyandu
- `ProgramKesehatan` - Program kesehatan
- `FasilitasKesehatan` - Fasilitas
- `InfoWabahPenyakit` - Info wabah
- `PenangananDarurat` - Penanganan darurat
- `KontakDarurat` - Kontak darurat
- `ArtikelKesehatan` - Artikel
#### Modul Ekonomi
- `APBDes` & `APBDesItem` - Anggaran desa (hierarki tree structure)
- `RealisasiItem` - Realisasi anggaran (multiple per item)
- `PasarDesa` - Pasar desa
- `PegawaiBumDes` - Pegawai BUMDes
- `StrukturBumDes` - Struktur BUMDes
- `DemografiPekerjaan` - Demografi pekerjaan
- `JumlahPendudukMiskin` - Data kemiskinan
- `JumlahPengangguran` - Data pengangguran
- `LowonganKerjaLokal` - Lowongan kerja
- `ProgramKemiskinan` - Program kemiskinan
- `SektorUnggulanDesa` - Sektor unggulan
- `PendapatanAsli` - Pendapatan asli desa
#### Modul Kependudukan
- `DataBanjar` - Data banjar
- `DistribusiAgama` - Distribusi agama
- `DistribusiUmur` - Distribusi umur
- `MigrasiPenduduk` - Migrasi
#### Modul Pendidikan
- `InfoSekolah` - Data sekolah
- `BeasiswaDesa` - Beasiswa
- `BimbinganBelajar` - Bimbingan belajar
- `PendidikanNonFormal` - Pendidikan non-formal
- `DataPerpustakaan` - Perpustakaan
#### Modul Keamanan
- `KeamananLingkungan` - Keamanan lingkungan
- `MenuTipsKeamanan` - Tips keamanan
- `PencegahanKriminalitas` - Pencegahan kriminalitas
- `PolsekTerdekat` - Polsek terdekat
- `LaporanPublik` - Laporan publik
#### Modul Lingkungan
- `DataLingkunganDesa` - Data lingkungan
- `KonservasiAdatBali` - Konservasi adat
- `BankSampah` - Bank sampah
- `ProgramPenghijauan` - Penghijauan
- `GotongRoyong` - Gotong royong
- `EdukasiLingkungan` - Edukasi
#### Modul Inovasi
- `ProgramInovasi` - Program inovasi
- `DesaDigital` - Desa digital
- `InfoTekno` - Info teknologi
- `KolaborasiInovasi` + `MitraKolaborasi` - Kolaborasi
- `LayananOnlineDesa` - Layanan online
- `ProgramKreatifDesa` - Program kreatif
- `Ajukan` - Pengajuan ide
#### Modul Musik
- `MusikDesa` - Musik desa
- `audioFile` -> FileStorage
- `coverImage` -> FileStorage
#### Landing Page
- `DesaAntiKorupsi` + `KategoriDesaAntiKorupsi`
- `SdgsDesa` - SDGs
- `PrestasiDesa` + `KategoriPrestasiDesa`
- `MediaSosial`
- `LandingPage_Layanan`
#### APBDes (Struktur Hierarki)
```prisma
model APBDesItem {
kode String // "4", "4.1", "4.1.2"
uraian String // Nama item
anggaran Float // Anggaran dalam Rupiah
tipe String? // "pendapatan" | "belanja" | "pembiayaan"
level Int // 1, 2, 3
parentId String? // Self-referencing untuk tree
children APBDesItem[]
totalRealisasi Float @default(0) // Auto-calculated
selisih Float @default(0) // totalRealisasi - anggaran
persentase Float @default(0) // (totalRealisasi / anggaran) * 100
realisasiItems RealisasiItem[]
}
```
### 5.3 Pola Umum Model
Hampir semua model mengikuti pola:
```prisma
model Contoh {
id String @id @default(cuid())
// ... fields
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? @default(now()) // Soft delete
isActive Boolean @default(true) // Soft delete flag
}
```
---
## 6. API Routes
### 6.1 Struktur API
Semua API routes ditangani oleh **Elysia.js** di `/src/app/api/[[...slugs]]/route.ts`
### 6.2 API Groups
| Prefix | Modul | Contoh Endpoints |
|--------|-------|------------------|
| `/api/layanan` | Layanan | `GET /api/layanan` |
| `/api/potensi` | Potensi | `GET /api/potensi` |
| `/api/desa/*` | Desa | CRUD berita, gallery, profil, dll |
| `/api/ppid/*` | PPID | CRUD struktur, profil, permohonan |
| `/api/kesehatan/*` | Kesehatan | CRUD puskesmas, posyandu, dll |
| `/api/ekonomi/*` | Ekonomi | CRUD APBDes, BUMDes, demografi |
| `/api/kependudukan/*` | Kependudukan | CRUD banjar, demografi |
| `/api/pendidikan/*` | Pendidikan | CRUD sekolah, beasiswa |
| `/api/keamanan/*` | Keamanan | CRUD keamanan, kontak darurat |
| `/api/lingkungan/*` | Lingkungan | CRUD data lingkungan |
| `/api/inovasi/*` | Inovasi | CRUD program inovasi |
| `/api/landing-page/*` | Landing Page | CRUD konten landing page |
| `/api/user/*` | User | CRUD user admin |
| `/api/user/role/*` | Role | CRUD role & permission |
| `/api/search` | Search | Pencarian global |
| `/api/file-storage/*` | File Storage | CRUD file storage |
| `/api/img/:name` | Image | GET gambar dengan resize |
| `/api/upl-img` | Upload | Upload multiple images |
| `/api/upl-img-single` | Upload | Upload single image |
| `/api/upl-csv` | Upload | Upload CSV files |
| `/api/utils/version` | Utils | GET versi aplikasi |
### 6.3 API Documentation
Swagger UI tersedia di: **`/api/docs`**
### 6.4 API Route Lainnya
| Route | Fungsi |
|-------|--------|
| `/api/health` | Health check endpoint |
| `/api/news` | API berita (standalone) |
| `/api/subscribe` | Subscription email |
| `/api/tts` | Text-to-Speech (ElevenLabs) |
| `/api/admin/*` | API khusus admin |
| `/api/auth/*` | API autentikasi |
---
## 7. Halaman Admin
### 7.1 Struktur
Admin dashboard berada di `/admin` dengan route group `(dashboard)`.
| Section | Path | Fungsi |
|---------|------|--------|
| **Dashboard** | `/admin` | Dashboard utama |
| **Autentikasi** | `/admin/auth` | Login admin |
| **Desa** | `/admin/desa/*` | Berita, gallery, profil, layanan, penghargaan, pengumuman, potensi |
| **PPID** | `/admin/ppid/*` | Profil, struktur, visi-misi, daftar informasi, dasar hukum, permohonan, IKM |
| **Kesehatan** | `/admin/kesehatan/*` | Puskesmas, posyandu, program kesehatan, wabah, kontak darurat |
| **Ekonomi** | `/admin/ekonomi/*` | APBDes, PAD, demografi, pengangguran, kemiskinan, BUMDes, pasar desa |
| **Kependudukan** | `/admin/kependudukan/*` | Banjar, distribusi agama, distribusi umur, migrasi |
| **Pendidikan** | `/admin/pendidikan/*` | Sekolah, beasiswa, bimbingan belajar, perpustakaan digital |
| **Keamanan** | `/admin/keamanan/*` | Keamanan lingkungan, kontak darurat, pencegahan kriminalitas, polsek |
| **Lingkungan** | `/admin/lingkungan/*` | Data lingkungan, konservasi, bank sampah, penghijauan, gotong royong |
| **Inovasi** | `/admin/inovasi/*` | Ide inovatif, desa digital, teknologi tepat guna, kolaborasi |
| **Musik** | `/admin/musik/*` | Manajemen musik desa |
| **Landing Page** | `/admin/landing-page/*` | Anti-korupsi, prestasi, SDGs, media sosial |
| **User & Role** | `/admin/user&role/*` | Manajemen user dan role |
| **Images** | `/admin/images/*` | Manajemen gambar |
| **CSV** | `/admin/csv/*` | Upload/import CSV |
### 7.2 Komponen Admin Shared
- `AdminThemeProvider.tsx` - Theme provider untuk dark/light mode
- `DarkModeToggle.tsx` - Toggle dark mode
- `UnifiedSurface.tsx` - Komponen surface/card unified
- `UnifiedTypography.tsx` - Tipografi unified
---
## 8. Halaman Publik
### 8.1 Struktur
Halaman publik berada di `/darmasaba` dengan layout yang mencakup Navbar, Footer, dan Fixed Music Player.
| Halaman | Path | Konten |
|---------|------|--------|
| **Landing Page

842
STRUKTUR.md Normal file
View File

@@ -0,0 +1,842 @@
# Dokumentasi Struktur Proyek Desa Darmasaba
## 1. Ringkasan Proyek
**Desa Darmasaba** adalah aplikasi web manajemen desa digital untuk Desa Darmasaba, Kabupaten Badung, Bali. Aplikasi ini berfungsi sebagai platform layanan publik digital yang mencakup informasi pemerintahan, layanan kesehatan, keamanan, pendidikan, ekonomi, lingkungan, dan inovasi desa.
### Tech Stack
| Kategori | Teknologi |
|----------|-----------|
| **Framework** | Next.js 15 (App Router) |
| **Language** | TypeScript (strict mode) |
| **Runtime** | Bun |
| **Backend API** | Elysia.js (high-performance HTTP server) |
| **Database** | PostgreSQL |
| **ORM** | Prisma 6.3.1 |
| **UI Framework** | Mantine UI v7-v8 |
| **State Management** | Jotai + Valtio + SWR |
| **Authentication** | iron-session + JWT (@elysiajs/jwt) |
| **File Storage** | Seafile (self-hosted) |
| **Text Editor** | Tiptap (Rich text editor) |
| **Charts** | Recharts + Chart.js |
| **Maps** | Leaflet + react-leaflet |
| **Testing** | Vitest (unit) + Playwright (E2E) |
| **Styling** | Mantine + PostCSS + Framer Motion |
| **Deployment** | Docker + GHCR + Portainer + GitHub Actions |
| **Version** | 0.1.11 |
---
## 2. Struktur Direktori
```
desa-darmasaba/
├── .github/workflows/ # GitHub Actions CI/CD
│ ├── docker-publish.yml # Auto build & push saat tag v*
│ ├── publish.yml # Manual build & push ke GHCR
│ ├── re-pull.yml # Manual re-pull di Portainer
│ └── script/ # Shell scripts untuk deploy
├── prisma/
│ ├── schema.prisma # Database schema (2413 baris, 100+ model)
│ └── seed.ts # Database seeder (400+ baris)
│ └── _seeder_list/ # Seed data per modul
├── public/ # Static assets
│ └── assets/
│ └── images/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── _com/ # Global components (SplashScreen, WebVitals)
│ │ ├── admin/ # ADMIN DASHBOARD
│ │ │ ├── (dashboard)/ # Route group dashboard
│ │ │ │ ├── desa/ # - Berita, Gallery, Layanan, dll
│ │ │ │ ├── ppid/ # - Informasi publik, struktur, dasar hukum
│ │ │ │ ├── kesehatan/ # - Fasilitas, posyandu, puskesmas, wabah
│ │ │ │ ├── ekonomi/ # - APBDes, pasar desa, BUMDes, dll
│ │ │ │ ├── kependudukan/ # - Banjar, agama, umur, migrasi
│ │ │ │ ├── pendidikan/ # - Sekolah, beasiswa, perpustakaan
│ │ │ │ ├── keamanan/ # - Keamanan lingkungan, polsek, dll
│ │ │ │ ├── lingkungan/ # - Sampah, penghijauan, gotong royong
│ │ │ │ ├── inovasi/ # - Desa digital, kolaborasi, dll
│ │ │ │ ├── landing-page/ # - Profil, prestasi, anti-korupsi
│ │ │ │ ├── musik/ # - Musik desa
│ │ │ │ ├── user&role/ # - Manajemen user & role
│ │ │ │ └── _com/ # - Shared admin components
│ │ │ ├── auth/ # Login OTP untuk admin
│ │ │ ├── csv/ # Demo CSV upload
│ │ │ └── layout.tsx # Admin shell (AppShell Mantine)
│ │ ├── api/ # ELYSIA.JS API SERVER
│ │ │ ├── [[...slugs]]/ # Catch-all route -> Elysia handler
│ │ │ │ ├── route.ts # - Main Elysia server export
│ │ │ │ └── _lib/ # - Domain route modules
│ │ │ │ ├── desa.ts
│ │ │ │ ├── ppid.ts
│ │ │ │ ├── kesehatan.ts
│ │ │ │ ├── ekonomi.ts
│ │ │ │ ├── keamanan.ts
│ │ │ │ ├── inovasi.ts
│ │ │ │ ├── lingkungan.ts
│ │ │ │ ├── pendidikan.ts
│ │ │ │ ├── kependudukan.ts
│ │ │ │ ├── landing_page.ts
│ │ │ │ ├── user/ # - User & Role management
│ │ │ │ ├── fileStorage/
│ │ │ │ ├── search/
│ │ │ │ ├── auth/
│ │ │ │ ├── upl-img.ts, upl-img-single.ts
│ │ │ │ ├── upl-csv.ts, upl-csv-single.ts
│ │ │ │ └── img.ts, img-del.ts, imgs.ts
│ │ │ ├── auth/ # Auth endpoints (login, logout, me)
│ │ │ └── ... # Other API routes
│ │ ├── darmasaba/ # PUBLIC-FACING WEBSITE
│ │ │ ├── _com/ # Shared components (Navbar, Footer, etc)
│ │ │ ├── (pages)/ # Public pages route group
│ │ │ │ ├── desa/ # - Profil, berita, gallery, layanan
│ │ │ │ ├── ppid/ # - PPID public pages
│ │ │ │ ├── kesehatan/ # - Health info pages
│ │ │ │ ├── ekonomi/ # - Economy pages
│ │ │ │ ├── kependudukan/
│ │ │ │ ├── pendidikan/
│ │ │ │ ├── keamanan/
│ │ │ │ ├── lingkungan/
│ │ │ │ ├── inovasi/
│ │ │ │ ├── musik/
│ │ │ │ └── module/ # - External module links
│ │ │ └── (tambahan)/ # Additional pages
│ │ ├── login/ # Login page
│ │ ├── registrasi/ # Registration page
│ │ ├── waiting-room/ # Waiting room (inactive users)
│ │ ├── terms-of-service/
│ │ ├── layout.tsx # Root layout (MantineProvider, ViewTransitions)
│ │ └── page.tsx # Homepage redirect
│ ├── components/
│ │ └── admin/ # Admin shared components
│ │ ├── AdminThemeProvider.tsx
│ │ ├── DarkModeToggle.tsx
│ │ ├── UnifiedSurface.tsx
│ │ └── UnifiedTypography.tsx
│ ├── con/ # Constants & configuration
│ │ ├── colors.ts # Color palette definitions
│ │ ├── images.ts
│ │ ├── navbar-list-menu.ts
│ │ ├── router.ts # Route mapping
│ │ └── sosmed.ts
│ ├── context/ # React contexts
│ │ └── MusicContext.tsx # Music player context
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utility libraries
│ │ ├── router/
│ │ ├── api-auth.ts # API authentication helpers
│ │ ├── api-fetch.ts # API fetch wrapper
│ │ ├── EnvStringParse.ts
│ │ ├── prisma.ts # Prisma client singleton
│ │ ├── seafile-auth-service.ts
│ │ └── session.ts # iron-session helper
│ ├── state/ # Global state (Jotai/Valtio)
│ │ ├── darkModeStore.ts
│ │ ├── state-layanan.ts
│ │ ├── state-list-image.ts
│ │ └── state-nav.ts
│ ├── store/ # Additional stores
│ │ └── authStore.ts # Auth state (Jotai)
│ ├── types/ # TypeScript type definitions
│ └── utils/ # Utility functions
│ └── themeTokens.ts # Dark/light theme tokens
├── uploads/ # Local upload directory (images/files)
├── Dockerfile # Multi-stage Docker build (Bun)
├── docker-entrypoint.sh # Entry script (migrate + start)
├── next.config.ts # Next.js configuration
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript configuration
├── biome.json # Biome linter config
├── eslint.config.mjs # ESLint config
├── NOTE.md # Deployment notes
├── QWEN.md # Project memory & workflow
└── AGENTS.md # Agent coding guidelines
```
---
## 3. Arsitektur
### Pola Arsitektur: Full-Stack Monolith dengan App Router
```
Browser
|
+-- Next.js 15 (App Router) -- Server Components + Client Components
|
+-- /darmasaba/* -> Public pages (SSR/CSR)
+-- /admin/* -> Admin dashboard (protected)
+-- /api/* -> Elysia.js API server
|
+-- Elysia Server (src/app/api/[[...slugs]]/route.ts)
|
+-- CORS enabled
+-- Swagger docs di /api/docs
+-- Static file serving (/api/uploads)
+-- Domain modules: Desa, PPID, Kesehatan, Ekonomi, dll
+-- Image upload handlers
|
+-- Prisma ORM --> PostgreSQL
+-- Seafile API --> File Storage
```
### Key Architectural Decisions:
1. **Next.js 15 App Router**: Menggunakan React Server Components sebagai default, dengan `"use client"` untuk interaktivitas
2. **Elysia.js di dalam API Routes**: Catch-all route `[[...slugs]]` meneruskan semua request ke Elysia handler
3. **Route Groups**: `(dashboard)` dan `(pages)` untuk organisasi tanpa mempengaruhi URL path
4. **Multi-tenant Ready**: Role-based access control dengan dynamic navbar berdasarkan roleId
5. **File Uploads**: Local uploads + Seafile integration untuk distributed storage
---
## 4. Modul Domain
### A. PPID (Pejabat Pengelola Informasi dan Dokumentasi)
**Lokasi**: `src/app/admin/(dashboard)/ppid/` dan `src/app/darmasaba/(pages)/ppid/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Profil PPID | Profil pejabat pengelola informasi |
| Struktur PPID | Struktur organisasi PPID dengan hierarki |
| Visi & Misi PPID | Visi dan misi PPID desa |
| Daftar Informasi Publik | Katalog informasi publik yang tersedia |
| Dasar Hukum | Regulasi dan dasar hukum PPID |
| Permohonan Informasi Publik | Form permohonan informasi (NIK, kontak, jenis) |
| Permohonan Keberatan | Formulir keberatan informasi |
| Indeks Kepuasan Masyarakat | Survey kepuasan dengan grafik demografis |
### B. Desa (Landing Page & Umum)
**Lokasi**: `src/app/admin/(dashboard)/desa/` dan `src/app/darmasaba/(pages)/desa/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Profil Desa | Sejarah, visi-misi, lambang, maskot |
| Profil Perbekel | Biodata, pengalaman, program unggulan perbekel |
| Perbekel dari Masa ke Masa | Historis perbekel per periode |
| Berita | Artikel berita dengan kategori & multi-image |
| Gallery | Foto dan video galeri |
| Pengumuman | Pengumuman desa dengan kategori |
| Potensi Desa | Potensi desa dengan kategori |
| Layanan Desa | Surat keterangan, ajukan permohonan |
| Penghargaan | Prestasi dan penghargaan desa |
| Desa Anti Korupsi | Transparansi anti-korupsi |
| SDGs Desa | Sustainable Development Goals desa |
| APBDes | Anggaran desa dengan hierarki item & realisasi |
| Prestasi Desa | Katalog prestasi |
### C. Kesehatan
**Lokasi**: `src/app/admin/(dashboard)/kesehatan/` dan `src/app/darmasaba/(pages)/kesehatan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Fasilitas Kesehatan | Info rumah sakit/klinik (jam, dokter, tarif) |
| Puskesmas | Data puskesmas dengan jam operasional & kontak |
| Posyandu | Jadwal dan informasi posyandu |
| Program Kesehatan | Program-program kesehatan desa |
| Penanganan Darurat | Prosedur penanganan darurat |
| Kontak Darurat | Kontak emergency dengan WhatsApp |
| Info Wabah Penyakit | Informasi wabah penyakit |
| Artikel Kesehatan | Artikel kesehatan lengkap |
| Data Kesehatan Warga | Statistik kesehatan warga |
| Kelahiran & Kematian | Data vital statistik |
| Grafik Kepuasan | Grafik kepuasan layanan kesehatan |
### D. Ekonomi
**Lokasi**: `src/app/admin/(dashboard)/ekonomi/` dan `src/app/darmasaba/(pages)/ekonomi/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Pasar Desa | Katalog pasar desa dengan produk & rating |
| Struktur BUMDes | Organisasi BUMDes dengan pengurus |
| APBDes (PADesa) | Pendapatan Asli Desa |
| Program Kemiskinan | Program dan statistik kemiskinan |
| Sektor Unggulan | Sektor ekonomi unggulan desa |
| Lowongan Kerja Lokal | Info lowongan pekerjaan |
| Demografi Pekerjaan | Distribusi pekerjaan penduduk |
| Jumlah Pengangguran | Statistik pengangguran |
| Penduduk Usia Kerja Menganggur | Analisis pengangguran by usia & pendidikan |
| Jumlah Penduduk Miskin | Tren kemiskinan tahunan |
### E. Kependudukan
**Lokasi**: `src/app/admin/(dashboard)/kependudukan/` dan `src/app/darmasaba/(pages)/kependudukan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Data Banjar | Data penduduk per banjar |
| Distribusi Agama | Statistik agama penduduk |
| Distribusi Umur | Piramida umur penduduk |
| Migrasi Penduduk | Data migrasi masuk/keluar |
| Dinamika Penduduk | Kelahiran, kematian, migrasi per tahun |
### F. Pendidikan
**Lokasi**: `src/app/admin/(dashboard)/pendidikan/` dan `src/app/darmasaba/(pages)/pendidikan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Info Sekolah & PAUD | Data sekolah per jenjang (TK, SD, SMP, SMA) |
| Beasiswa Desa | Program beasiswa & pendaftar |
| Program Pendidikan Anak | Program pendidikan anak |
| Bimbingan Belajar | Informasi bimbingan belajar |
| Pendidikan Non Formal | Tempat & program non-formal |
| Perpustakaan Digital | Katalog buku & peminjaman |
| Data Pendidikan | Statistik pendidikan |
### G. Keamanan
**Lokasi**: `src/app/admin/(dashboard)/keamanan/` dan `src/app/darmasaba/(pages)/keamanan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Keamanan Lingkungan (Pecalang/Patwal) | Sistem keamanan tradisional Bali |
| Polsek Terdekat | Data polsek dengan layanan & map |
| Kontak Darurat | Kontak darurat keamanan |
| Pencegahan Kriminalitas | Info pencegahan kriminal |
| Laporan Publik | Laporan masyarakat dengan tracking status |
| Tips Keamanan | Tips dan panduan keamanan |
### H. Lingkungan
**Lokasi**: `src/app/admin/(dashboard)/lingkungan/` dan `src/app/darmasaba/(pages)/lingkungan/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Pengelolaan Sampah | Bank sampah & pengelolaan |
| Program Penghijauan | Program penghijauan desa |
| Data Lingkungan | Data lingkungan desa |
| Gotong Royong | Kegiatan gotong royong |
| Edukasi Lingkungan | Edukasi lingkungan hidup |
| Konservasi Adat Bali | Tri Hita Karana & konservasi adat |
### I. Inovasi
**Lokasi**: `src/app/admin/(dashboard)/inovasi/` dan `src/app/darmasaba/(pages)/inovasi/`
| Sub-modul | Deskripsi |
|-----------|-----------|
| Desa Digital (Smart Village) | Transformasi digital desa |
| Program Kreatif Desa | Program kreatif & inovatif |
| Kolaborasi Inovasi | Kolaborasi dengan mitra |
| Info Teknologi Tepat Guna | Info teknologi untuk desa |
| Ajukan Ide Inovatif | Form pengajuan ide dari warga |
| Layanan Online Desa | Layanan administrasi online |
### J. Musik Desa
**Lokasi**: `src/app/admin/(dashboard)/musik/` dan `src/app/darmasaba/(pages)/musik/`
Model `MusikDesa` dengan audio file, cover image, genre, dan durasi. Dilengkapi dengan `FixedPlayerBar` di layout publik.
### K. User & Role (Admin)
**Lokasi**: `src/app/admin/(dashboard)/user&role/`
- **Role-based Access Control**: Role dengan permission JSON
- **User Session Management**: Multiple sessions per user dengan JWT
- **OTP Authentication**: Login dengan nomor telepon + OTP
- **Menu Access Control**: Dynamic navbar berdasarkan menu akses user
---
## 5. Database Schema (Prisma)
Schema terdiri dari **2413 baris** dengan **100+ model** dan **berbagai enum**. Berikut model-model utama:
### Core Models
| Model | Keterangan |
|-------|-----------|
| `FileStorage` | Central file storage untuk semua uploaded files |
| `AppMenu` / `AppMenuChild` | Menu navigasi aplikasi |
| `User` / `Role` / `UserSession` / `UserMenuAccess` | Sistem autentikasi & otorisasi |
| `KodeOtp` | OTP codes untuk login |
### Landing Page & Desa
| Model | Keterangan |
|-------|-----------|
| `PejabatDesa` | Pejabat desa dengan foto |
| `ProfilPerbekel` | Profil perbekel (biodata, pengalaman, program) |
| `PerbekelDariMasaKeMasa` | Historis perbekel |
| `Berita` / `KategoriBerita` | Berita desa |
| `PotensiDesa` / `KategoriPotensi` | Potensi desa |
| `Pengumuman` / `CategoryPengumuman` | Pengumuman |
| `GalleryFoto` / `GalleryVideo` | Gallery media |
| `Penghargaan` | Penghargaan desa |
| `APBDes` / `APBDesItem` / `RealisasiItem` | Anggaran dengan realisasi |
| `DesaAntiKorupsi` / `KategoriDesaAntiKorupsi` | Transparansi |
| `SdgsDesa` | SDGs desa |
| `PrestasiDesa` / `KategoriPrestasiDesa` | Prestasi |
| `MusikDesa` | Musik desa |
### PPID
| Model | Keterangan |
|-------|-----------|
| `StrukturPPID` / `PosisiOrganisasiPPID` / `PegawaiPPID` | Struktur organisasi |
| `VisiMisiPPID` | Visi misi |
| `ProfilePPID` | Profil pejabat |
| `DasarHukumPPID` | Regulasi |
| `DaftarInformasiPublik` | Katalog informasi |
| `PermohonanInformasiPublik` | Permohonan + lookup tables |
| `FormulirPermohonanKeberatan` | Keberatan |
| `IndeksKepuasanMasyarakat` + grafik breakdown | Survey kepuasan |
### Kesehatan
| Model | Keterangan |
|-------|-----------|
| `FasilitasKesehatan` | Fasilitas lengkap (dokter, tarif, prosedur) |
| `Puskesmas` / `JamOperasional` / `KontakPuskesmas` | Puskesmas |
| `Posyandu` | Pos pelayanan terpadu |
| `ProgramKesehatan` | Program kesehatan |
| `ArtikelKesehatan` | Artikel lengkap (gejala, pencegahan, P3K, dll) |
| `PenangananDarurat` / `KontakDarurat` | Darurat |
| `InfoWabahPenyakit` | Wabah |
| `DataKematian_Kelahiran` / `Kelahiran` / `Kematian` | Vital statistik |
| `GrafikKepuasan` | Kepuasan |
### Ekonomi
| Model | Keterangan |
|-------|-----------|
| `PasarDesa` / `KategoriProduk` / `KategoriToPasar` | Pasar desa |
| `StrukturBumDes` / `PosisiOrganisasiBumDes` / `PegawaiBumDes` | BUMDes |
| `ProgramKemiskinan` / `StatistikKemiskinan` | Kemiskinan |
| `SektorUnggulanDesa` | Sektor unggulan |
| `LowonganPekerjaan` | Lowongan |
| `DataDemografiPekerjaan` | Demografi pekerjaan |
| `DetailDataPengangguran` | Pengangguran |
| `GrafikJumlahPendudukMiskin` | Tren kemiskinan |
### Kependudukan
| Model | Keterangan |
|-------|-----------|
| `DataBanjar` | Data per banjar |
| `DistribusiAgama` | Distribusi agama |
| `DistribusiUmur` | Distribusi umur |
| `MigrasiPenduduk` | Migrasi (MASUK/KELUAR) |
| `DinamikaPenduduk` | Dinamika tahunan |
### Pendidikan
| Model | Keterangan |
|-------|-----------|
| `JenjangPendidikan` / `Lembaga` / `Siswa` / `Pengajar` | Data sekolah |
| `BeasiswaPendaftar` | Beasiswa (dengan enum lengkap) |
| `DataPerpustakaan` / `KategoriBuku` / `PeminjamanBuku` | Perpustakaan |
| `DataPendidikan` | Statistik |
### Keamanan
| Model | Keterangan |
|-------|-----------|
| `KeamananLingkungan` | Keamanan lingkungan |
| `PolsekTerdekat` / `LayananPolsek` / `LayananToPolsek` | Polsek |
| `KontakDaruratKeamanan` / `KontakItem` | Kontak darurat |
| `PencegahanKriminalitas` | Pencegahan |
| `LaporanPublik` / `PenangananLaporanPublik` (enum StatusLaporan) | Laporan |
| `Pelapor` | Pelapor |
| `MenuTipsKeamanan` | Tips |
### Lingkungan
| Model | Keterangan |
|-------|-----------|
| `PengelolaanSampah` | Pengelolaan sampah |
| `KeteranganBankSampahTerdekat` | Bank sampah |
| `ProgramPenghijauan` | Penghijauan |
| `DataLingkunganDesa` | Data lingkungan |
| `KegiatanDesa` / `KategoriKegiatan` | Gotong royong |
| `FilosofiTriHita` / `BentukKonservasiBerdasarkanAdat` | Konservasi Bali |
### Inovasi
| Model | Keterangan |
|-------|-----------|
| `DesaDigital` | Smart village |
| `ProgramKreatif` | Program kreatif |
| `KolaborasiInovasi` / `MitraKolaborasi` | Kolaborasi |
| `InfoTekno` | Teknologi tepat guna |
| `AjukanIdeInovatif` | Ide dari warga |
| `AdministrasiOnline` / `JenisLayanan` | Layanan online |
| `PengaduanMasyarakat` / `JenisPengaduan` | Pengaduan |
---
## 6. API Routes
Semua API ditangani oleh **Elysia.js** di `src/app/api/[[...slugs]]/route.ts`:
| Endpoint Group | Prefix | Deskripsi |
|---------------|--------|-----------|
| **File Storage** | `/api/file-storage` | CRUD file storage |
| **Landing Page** | `/api/landing-page` | Profil, prestasi, anti-korupsi, SDGs, APBDes |
| **Desa** | `/api/desa` | Berita, gallery, potensi, pengumuman, layanan |
| **PPID** | `/api/ppid` | Semua endpoint PPID |
| **Kesehatan** | `/api/kesehatan` | Fasilitas, puskesmas, posyandu, artikel, wabah |
| **Ekonomi** | `/api/ekonomi` | Pasar desa, BUMDes, APBDes, pengangguran |
| **Keamanan** | `/api/keamanan` | Keamanan, polsek, laporan, kriminalitas |
| **Lingkungan** | `/api/lingkungan` | Sampah, penghijauan, gotong royong |
| **Pendidikan** | `/api/pendidikan` | Sekolah, beasiswa, perpustakaan |
| **Kependudukan** | `/api/kependudukan` | Banjar, agama, umur, migrasi |
| **Inovasi** | `/api/inovasi` | Desa digital, kolaborasi, pengaduan |
| **User** | `/api/admin/user` | CRUD user |
| **Role** | `/api/admin/role` | CRUD role |
| **Search** | `/api/search` | Global search |
| **Utils** | `/api/utils/version` | Version info |
### Utility Endpoints
| Endpoint | Method | Deskripsi |
|----------|--------|-----------|
| `/api/img/:name` | GET | Serve image dengan resize |
| `/api/img/:name` | DELETE | Delete image |
| `/api/imgs` | GET | List images dengan pagination |
| `/api/upl-img` | POST | Upload multiple images |
| `/api/upl-img-single` | POST | Upload single image |
| `/api/upl-csv` | POST | Upload CSV multiple |
| `/api/upl-csv-single` | POST | Upload single CSV |
### Auth Endpoints
| Endpoint | Method | Deskripsi |
|----------|--------|-----------|
| `/api/auth/login` | POST | Login dengan OTP |
| `/api/auth/logout` | POST | Logout |
| `/api/auth/me` | GET | Get current user |
**Swagger Documentation**: Tersedia di `/api/docs`
---
## 7. Halaman Admin
Admin dashboard menggunakan **Mantine AppShell** dengan sidebar navigasi dinamis berbasis role.
### Route Group: `/admin`
| Section | Path | Deskripsi |
|---------|------|-----------|
| **Landing Page** | `/admin/landing-page/` | Profil desa, prestasi, anti-korupsi, SDGs, media sosial |
| **Desa** | `/admin/desa/` | Berita, gallery, layanan, penghargaan, pengumuman, potensi, profil |
| **PPID** | `/admin/ppid/` | 8 sub-modul PPID lengkap |
| **Kesehatan** | `/admin/kesehatan/` | 8 sub-modul kesehatan |
| **Ekonomi** | `/admin/ekonomi/` | 10 sub-modul ekonomi |
| **Kependudukan** | `/admin/kependudukan/` | 4 sub-modul kependudukan |
| **Pendidikan** | `/admin/pendidikan/` | 7 sub-modul pendidikan |
| **Keamanan** | `/admin/keamanan/` | 6 sub-modul keamanan |
| **Lingkungan** | `/admin/lingkungan/` | 6 sub-modul lingkungan |
| **Inovasi** | `/admin/inovasi/` | 6 sub-modul inovasi |
| **Musik** | `/admin/musik/` | Manajemen musik desa |
| **User & Role** | `/admin/user&role/` | Manajemen user, role, menu access |
### Fitur Admin:
- **Role-based Dynamic Navbar**: Navbar berubah berdasarkan roleId user
- **Dark Mode Toggle**: Tema gelap/terang
- **OTP Login**: Login dengan nomor telepon + kode OTP
- **Session Management**: Multiple sessions per user dengan JWT tokens
- **CSV Upload**: Import data via CSV
- **Image Upload**: Upload dengan preview dan management
- **Rich Text Editor**: Tiptap untuk konten HTML
### Role-Based Redirect:
| roleId | Role | Default Redirect |
|--------|------|-----------------|
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
---
## 8. Halaman Publik
Public website di `/darmasaba/` dengan layout yang mencakup **Navbar**, **Footer**, dan **Fixed Music Player Bar**.
### Route Group: `/darmasaba`
| Section | Path | Deskripsi |
|---------|------|-----------|
| **Home** | `/darmasaba` | Landing page utama |
| **Desa** | `/darmasaba/desa` | Profil, berita, gallery, layanan, pengumuman, potensi |
| **PPID** | `/darmasaba/ppid` | 7 sub-halaman PPID publik |
| **Kesehatan** | `/darmasaba/kesehatan` | Info kesehatan publik |
| **Ekonomi** | `/darmasaba/ekonomi` | Info ekonomi desa |
| **Kependudukan** | `/darmasaba/kependudukan` | Data kependudukan |
| **Pendidikan** | `/darmasaba/pendidikan` | Info pendidikan |
| **Keamanan** | `/darmasaba/keamanan` | Info keamanan |
| **Lingkungan** | `/darmasaba/lingkungan` | Info lingkungan |
| **Inovasi** | `/darmasaba/inovasi` | Info inovasi |
| **Musik** | `/darmasaba/musik` | Musik desa |
| **Module** | `/darmasaba/module/*` | Link ke modul eksternal (DAVES, MANGAN, Bicara-Darma, BARES, dll) |
### Fitur Publik:
- **Fixed Music Player Bar**: Player musik yang selalu tampil di bottom
- **Global Search**: Pencarian global
- **News Reader**: Notifikasi berita modern
- **View Transitions**: Smooth page transitions
- **Responsive Design**: Mobile-first dengan Mantine breakpoints
---
## 9. Komponen Utama
### Admin Components (`src/components/admin/`)
| Komponen | Deskripsi |
|----------|-----------|
| `AdminThemeProvider.tsx` | Theme provider untuk admin |
| `DarkModeToggle.tsx` | Toggle dark/light mode |
| `UnifiedSurface.tsx` | Consistent surface/card component |
| `UnifiedTypography.tsx` | Consistent typography system |
### Public Shared Components (`src/app/darmasaba/_com/`)
| Komponen | Deskripsi |
|----------|-----------|
| `Navbar.tsx` | Main navigation bar |
| `NavbarMainMenu.tsx` | Main menu dengan kategori |
| `NavbarSubMenu.tsx` | Submenu dropdown |
| `Footer.tsx` | Footer dengan info desa |
| `FixedPlayerBar.tsx` | Music player bar fixed di bottom |
| `LoadDataFirstClient.tsx` | Client-side data preloader |
| `globalSearch.tsx` | Global search component |
| `NewsReader.tsx` | News notification reader |
| `ModernNewsNotification.tsx` | News toast notifications |
### Global Components (`src/app/_com/`)
| Komponen | Deskripsi |
|----------|-----------|
| `SpashScreen.tsx` | Splash screen on load |
| `WebVitals.tsx` | Web Vitals monitoring |
---
## 10. State Management
Proyek menggunakan **multi-layer state management**:
| Library | Penggunaan | Lokasi |
|---------|-----------|--------|
| **Jotai** | Auth state (`authStore`) | `src/store/authStore.ts` |
| **Valtio** | Dark mode, layanan, image list, nav state | `src/state/*.ts` |
| **SWR** | Server state fetching & caching | Digunakan di components |
| **React Context** | Music player context | `src/app/context/MusicContext.tsx` |
| **React useState** | Local component state | Di components |
### State Files:
```
src/state/
darkModeStore.ts -- Valtio proxy untuk dark mode
state-layanan.ts -- State layanan desa
state-list-image.ts -- State list image untuk upload
state-nav.ts -- State navigasi
src/store/
authStore.ts -- Jotai atom untuk auth user state
```
---
## 11. Autentikasi
Sistem autentikasi menggunakan **OTP (One-Time Password)** via WhatsApp/Telepon dengan **iron-session** untuk session management.
### Flow Autentikasi:
1. User memasukkan **nomor telepon** di `/login`
2. Sistem mengirim **kode OTP** via WhatsApp Server
3. OTP disimpan di model `KodeOtp`
4. User memasukkan kode OTP
5. Jika valid, session dibuat dengan **iron-session** + **JWT token**
6. Session disimpan di `UserSession` model dengan expiry
### Session Structure:
```typescript
// src/lib/session.ts
type SessionData = {
user?: {
id: string;
name: string;
roleId: number;
menuIds?: string[] | null;
isActive?: boolean;
};
};
```
### Role-Based Access:
| roleId | Role | Default Redirect |
|--------|------|-----------------|
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
### Authorization:
- **UserMenuAccess**: Mapping user ke menu yang boleh diakses
- **Dynamic Navbar**: Navbar dirender berdasarkan `menuIds` user
- **Inactive Users**: Dialihkan ke `/waiting-room`
---
## 12. Deployment
### Docker Setup
**Dockerfile** menggunakan **multi-stage build** dengan base image `oven/bun:1-debian`:
```
Stage 1: Builder
- Install dependencies (bun install --frozen-lockfile)
- Generate Prisma client
- Build Next.js (bun run build)
Stage 2: Runner
- Copy .next, node_modules, public, prisma, src/lib, tsconfig.json
- Non-root user (nextjs:nodejs)
- Volume /app/uploads untuk file uploads
- Port 3000
```
### Entry Point (`docker-entrypoint.sh`):
```bash
bunx prisma migrate deploy # Run migrations
exec bun start # Start Next.js production server
```
### CI/CD dengan GitHub Actions
Terdapat **3 workflow**:
| Workflow | Trigger | Fungsi |
|----------|---------|--------|
| `docker-publish.yml` | Push tag `v*` | Auto build & push ke GHCR |
| `publish.yml` | Manual (workflow_dispatch) | Build & push ke GHCR dengan input `stack_env` + `tag` |
| `re-pull.yml` | Manual (workflow_dispatch) | Re-pull image di Portainer dengan input `stack_name` + `stack_env` |
### Deployment Workflow (Sequential):
```
1. Update version di package.json (semver)
2. Commit perubahan
3. Push ke branch target (stg/prod)
4. Trigger publish.yml:
gh workflow run publish.yml --ref main -f stack_env=stg -f tag=<version>
5. Tunggu sampai publish selesai (status: completed)
6. Trigger re-pull.yml:
gh workflow run re-pull.yml --ref main -f stack_name=desa-darmasaba -f stack_env=stg
7. Verifikasi di Portainer
```
**PENTING**: `publish.yml` dan `re-pull.yml` TIDAK boleh dijalankan bersamaan (race condition).
### Environments:
- **dev**: Development
- **stg**: Staging (`desa-darmasaba-stg.wibudev.com`)
- **prod**: Production
### Notification:
- Telegram notification via `notify.sh` script setelah setiap workflow
---
## 13. Scripts
| Script | Command | Deskripsi |
|--------|---------|-----------|
| `dev` | `next dev` | Development server |
| `build` | `next build` | Production build |
| `start` | `next start` | Production server |
| `test:api` | `vitest run` | Run API unit tests |
| `test:e2e` | `playwright test` | Run E2E tests |
| `test` | `bun run test:api && bun run test:e2e` | Run all tests |
| `seed` | `bun run prisma/seed.ts` | Seed database |
| `prisma:generate` | `bunx prisma generate` | Generate Prisma client |
| `prisma:push` | `bunx prisma db push` | Push schema to database |
| `prisma:studio` | `bunx prisma studio` | Open Prisma Studio GUI |
| `gen:api` | *(empty)* | Generate API types (placeholder) |
### Prisma Seed Configuration:
```json
// package.json
{
"prisma": {
"seed": "bun run prisma/seed.ts"
}
}
```
---
## 14. Environment Variables
File: `.env.example`
| Variable | Deskripsi | Contoh |
|----------|-----------|--------|
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/desa-darmasaba` |
| `SEAFILE_TOKEN` | Seafile API token | `your_seafile_token` |
| `SEAFILE_REPO_ID` | Seafile repository ID | `your_repo_id` |
| `SEAFILE_URL` | Seafile instance URL | `https://seafile.example.com` |
| `SEAFILE_PUBLIC_SHARE_TOKEN` | Token untuk public share | `your_share_token` |
| `WIBU_UPLOAD_DIR` | Upload directory path | `uploads` |
| `WA_SERVER_TOKEN` | WhatsApp server token | `your_wa_token` |
| `NEXT_PUBLIC_BASE_URL` | Base URL aplikasi | `/` (relative) |
| `EMAIL_USER` | Email untuk notifikasi | `your_email@gmail.com` |
| `EMAIL_PASS` | Email app password | `your_app_password` |
| `BASE_TOKEN_KEY` | JWT secret key | `your_jwt_secret` |
| `BOT_TOKEN` | Telegram bot token | `your_bot_token` |
| `CHAT_ID` | Telegram chat ID | `your_chat_id` |
| `SESSION_PASSWORD` | iron-session password (min 32 chars) | `secure_32_char_password` |
| `ELEVENLABS_API_KEY` | ElevenLabs API (TTS - optional) | `your_elevenlabs_key` |
---
## 15. Layanan Eksternal
### PostgreSQL
- **Provider**: PostgreSQL via Prisma ORM
- **Schema**: `public`
- **Connection**: Via `DATABASE_URL` environment variable
- **Migrations**: `prisma migrate deploy` di docker entrypoint
### Seafile (File Storage)
- **Tipe**: Self-hosted file sync & share
- **Penggunaan**: Storage untuk images, documents, audio files
- **Integrasi**: `src/lib/seafile-auth-service.ts`
- **CDN**: URL generation untuk public sharing
- **Config**: Token, repo ID, base URL
### WhatsApp Server
- **Penggunaan**: Kirim OTP codes saat login
- **Config**: `WA_SERVER_TOKEN`
### Telegram Bot
- **Penggunaan**: Notifikasi deployment & sistem
- **Config**: `BOT_TOKEN` + `CHAT_ID`
- **Integration**: `notify.sh` script di GitHub Actions
### ElevenLabs (Optional)
- **Penggunaan**: Text-to-Speech (TTS) features
- **Config**: `ELEVENLABS_API_KEY`
### Email (Nodemailer)
- **Penggunaan**: Notifikasi email untuk subscription/pengumuman
- **Config**: `EMAIL_USER` + `EMAIL_PASS`
- **Provider**: Gmail (app password)
---
## Ringkasan Cepat
| Aspek | Detail |
|-------|--------|
| **Framework** | Next.js 15 (App Router) + Elysia.js |
| **Database** | PostgreSQL + Prisma (100+ models) |
| **Auth** | OTP + iron-session + JWT |
| **Storage** | Seafile + local uploads |
| **UI** | Mantine UI + Tiptap + Framer Motion |
| **State** | Jotai + Valtio + SWR |
| **Deploy** | Docker + GHCR + Portainer + GitHub Actions |
| **Runtime** | Bun |
| **Testing** | Vitest + Playwright |
| **Version** | 0.1.11 |

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());

3
ai.sh Normal file
View File

@@ -0,0 +1,3 @@
export ANTHROPIC_API_KEY=sk-user-nico
export ANTHROPIC_BASE_URL=https://claude-local.wibudev.com
export ANTHROPIC_MODEL=claude-sonnet-4-6

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

13
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
echo "🔄 Running database migrations..."
cd /app
bunx prisma migrate deploy || {
echo "❌ Migration failed!"
exit 1
}
echo "✅ Migrations completed successfully"
echo "🚀 Starting application..."
exec bun start

1
eror.md Normal file

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,11 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"@typescript-eslint/no-explicit-any": "warn",
},
},
];
export default eslintConfig;

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,13 +1,15 @@
{
"name": "desa-darmasaba",
"version": "0.1.5",
"version": "0.1.20",
"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"
@@ -21,6 +23,7 @@
"@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",
@@ -28,9 +31,10 @@
"@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",
@@ -41,61 +45,93 @@
"@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",
"dompurify": "^3.3.1",
"dotenv": "^17.2.3",
"elysia": "^1.3.5",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^7.1.0",
"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.23.5",
"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",
"minio": "^8.0.7",
"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",
"primeicons": "^7.0.0",
"primereact": "^10.9.6",
"prisma": "^6.3.1",
"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"
}
"typescript": "^5",
"vitest": "^4.0.18"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

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

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