# 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 ``` **`list-potensi/page.tsx`:** ```typescript ``` **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 ``` **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 || ''; }; {stripHtml(item.deskripsi)} ``` --- ## 🟡 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 { load(newPage, 10); // ❌ Tidak ada search parameter }} /> ``` **Dampak:** Saat ganti halaman, search query hilang. **Solusi:** ```typescript { load(newPage, 10, search); // ✅ Include search }} /> ``` --- ### 9. UI - colSpan Mismatch **File:** `src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx` ```typescript Nama Dibuat Aksi {/* 3 kolom */} {loading ? ( {/* ❌ colSpan 4, seharusnya 3 */} ) : ( // ... )} ``` **Solusi:** ```typescript // ✅ 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 ``` **Dampak:** User bisa upload format image aneh yang tidak didukung browser. **Solusi:** ```typescript ``` --- ## 🟢 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: [] });
``` ### **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": "", ... } # 3. Try delete kategori (should fail) DELETE /api/desa/kategoripotensi/del/ # 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