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

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 (Model PotensiDesa & 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 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:

# 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


Dibuat oleh: QC Automation
Review Status: Menunggu Review Developer
Next Review: Setelah implementasi fixes