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)
15 KiB
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(ModelPotensiDesa&KategoriPotensi)
🔴 HIGH PRIORITY ISSUES
1. Schema - Tidak Ada Unique Constraint pada name dan nama
File: prisma/schema.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:
model PotensiDesa {
name String @unique // ✅ Add unique constraint
// ...
}
model KategoriPotensi {
nama String @unique // ✅ Add unique constraint
// ...
}
Migration Required:
bunx prisma db push
# atau
bunx prisma migrate dev --name add_unique_constraints
2. Schema - kategoriId Nullable Seharusnya Required
File: prisma/schema.prisma
model PotensiDesa {
kategoriId String? // ❌ Nullable, seharusnya required
// ...
}
Dampak: Potensi desa bisa dibuat tanpa kategori, tidak masuk akal secara bisnis.
Solusi:
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
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:
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
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:
// 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
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:
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:
<TableTd dangerouslySetInnerHTML={{ __html: item.nama }} />
list-potensi/page.tsx:
<TableTd dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
Dampak:
- User bisa inject malicious script melalui rich text editor
- XSS attack bisa mencuri session atau data sensitif
Solusi:
// 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):
// 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):
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
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10); // ❌ Tidak ada search parameter
}}
/>
Dampak: Saat ganti halaman, search query hilang.
Solusi:
<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
<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:
<TableTd colSpan={3}> // ✅ Match column count
10. UI - Alert Instead of Toast
File: src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx
if (!nama.trim()) {
alert('Nama kategori potensi wajib diisi'); // ❌ Browser alert
return;
}
Dampak: Browser alert blocking, UX buruk, tidak konsisten dengan page lain.
Solusi:
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
useEffect(() => {
potensiState.kategoriPotensi.findMany.load();
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]); // ❌ Missing potensiState
Dampak: ESLint warning, potential stale closure.
Solusi:
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
<Dropzone
accept={{ "image/*": [] }} // ❌ Terlalu general
// ...
>
Dampak: User bisa upload format image aneh yang tidak didukung browser.
Solusi:
<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
const isDetailPage = segments.length >= 5; // ❌ Magic number
Dampak: Tidak jelas maksudnya, brittle jika ada perubahan route structure.
Solusi:
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:
// 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:
// 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
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
deletedAtdanisActive - ✅ 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
namedannamadi schema - Make
kategoriIdrequired di schema - Add length constraints (@db.VarChar)
- Fix delete kategori dengan relation check
- Add
isActivefilter 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:
# 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:
bun add dompurify
bun add -D @types/dompurify
Usage:
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:
# 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
- DOMPurify Documentation
- Mantine Table Documentation
- React Toastify Documentation
Dibuat oleh: QC Automation
Review Status: ⏳ Menunggu Review Developer
Next Review: Setelah implementasi fixes