Files
desa-darmasaba/QC/DESA/summary-qc-potensi-desa.md
nico 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

659 lines
15 KiB
Markdown

# 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