Files
desa-darmasaba/QC/DESA/summary-qc-pengumuman-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

20 KiB

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

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:

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:

// 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

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:

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:

# 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

<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:

<Pagination
  total={totalPages}
  value={page}
  onChange={(newPage) => {
    load(newPage, 10, debouncedSearch);  // ✅ Include search parameter
  }}
/>

Note: Pastikan function load menerima parameter search:

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

// 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:

// 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:

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

// 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:

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

<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:

<TableTd colSpan={3}>  // ✅ Match column count

7. State Management - Copy-Paste Error Message

File: src/app/admin/(dashboard)/_state/desa/pengumuman.ts

// 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:

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

<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:

<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

<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:

<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

<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:

// 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

// 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

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:

// 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

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:

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

// ❌ 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:

// 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

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:

// 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

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:

// 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:

# 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:

// 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:

# 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


📈 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