Files
desa-darmasaba/QC/PPID/QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.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

24 KiB

QC Summary - Permohonan Informasi Publik PPID Module

Scope: List Permohonan Informasi Publik, Detail Permohonan
Date: 2026-02-23
Status: Secara umum sudah baik, ada beberapa improvement yang diperlukan


📊 OVERVIEW

Aspect Schema API UI Admin State Management Overall
Permohonan Informasi Publik ⚠️ Ada issue Baik Baik ⚠️ Ada issue 🟡 Perlu fix

YANG SUDAH BAIK

1. UI/UX Design

  • Preview layout yang clean dengan responsive design
  • Loading states dengan Skeleton
  • Empty state handling yang informatif dengan icon
  • Search functionality dengan debounce (1000ms)
  • Pagination yang konsisten
  • Desktop table + mobile cards responsive
  • Icon integration (User, ID, Phone, Info) untuk visual clarity

2. Table & Card Layout

  • Fixed layout table untuk consistency
  • Column headers dengan icon yang descriptive
  • Row numbering otomatis (index + 1)
  • Text truncation dengan lineClamp untuk long text
  • Mobile card view dengan proper information hierarchy

Code Example ( GOOD):

// page.tsx - Line ~130-180
<Table highlightOnHover
  layout="fixed" // ✅ PENTING - consistent column widths
  withColumnBorders={false}>
  <TableThead>
    <TableTr>
      <TableTh fz="sm" fw={600} ta="center" w={60}>No</TableTh>
      <TableTh fz="sm" fw={600}>
        <Group gap={5}>
          <IconUser size={16} />
          Nama
        </Group>
      </TableTh>
      <TableTh fz="sm" fw={600}>
        <Group gap={5}>
          <IconId size={16} />
          NIK
        </Group>
      </TableTh>
      // ...
    </TableTr>
  </TableThead>

Verdict: BAIK - Table layout dengan icon yang helpful!


3. State Management

  • Proper typing dengan Prisma types
  • Loading state management dengan finally block
  • Error handling yang comprehensive
  • ApiFetch consistency untuk create & findMany!
  • Zod validation untuk form data dengan specific rules
  • Separate proxy states untuk related data (jenisInformasi, caraMemperoleh, dll)

Code Example ( GOOD):

// state file - Line ~110-150
findMany: {
  data: null as Prisma.PermohonanInformasiPublikGetPayload<{...}>[] | null,
  page: 1,
  totalPages: 1,
  total: 0,
  loading: false,
  search: "",
  load: async (page = 1, limit = 10, search = "") => {
    statepermohonanInformasiPublik.findMany.loading = true; // ✅ Start loading
    statepermohonanInformasiPublik.findMany.page = page;
    statepermohonanInformasiPublik.findMany.search = search;
    try {
      const query: any = { page, limit };
      if (search) query.search = search;

      const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get({ query });

      if (res.status === 200 && res.data?.success) {
        statepermohonanInformasiPublik.findMany.data = res.data.data || [];
        statepermohonanInformasiPublik.findMany.total = res.data.total || 0;
        statepermohonanInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
      }
    } catch (error) {
      console.error("Error loading permohonan:", error);
      statepermohonanInformasiPublik.findMany.data = [];
      // ...
    } finally {
      statepermohonanInformasiPublik.findMany.loading = false; // ✅ Stop loading
    }
  },
}

Verdict: BAIK - State management sudah proper dengan ApiFetch!


4. Zod Schema Validation

  • Comprehensive validation untuk semua fields
  • Specific error messages untuk setiap field
  • Phone number length validation (3-15 chars)
  • NIK length validation (3-16 chars)
  • Email format validation
  • Required field validation untuk dropdowns

Code Example ( EXCELLENT):

// state file - Line ~8-22
const templateForm = z.object({
  name: z.string().min(3, "Nama minimal 3 karakter"),
  nik: z
    .string()
    .min(3, "NIK minimal 3 karakter")
    .max(16, "NIK maksimal 16 angka"), // ✅ Specific validation
  notelp: z
    .string()
    .min(3, "Nomor Telepon minimal 3 karakter")
    .max(15, "Nomor Telepon maksimal 15 angka"), // ✅ Specific validation
  alamat: z.string().min(3, "Alamat minimal 3 karakter"),
  email: z.string().min(3, "Email minimal 3 karakter"),
  jenisInformasiDimintaId: z.string().nonempty(), // ✅ Required dropdown
  caraMemperolehInformasiId: z.string().nonempty(), // ✅ Required dropdown
  caraMemperolehSalinanInformasiId: z.string().nonempty(), // ✅ Required dropdown
});

Verdict: EXCELLENT - Validation yang comprehensive!


  • Separate proxy states untuk dropdown data
  • JenisInformasiDiminta, CaraMemperolehInformasi, CaraMemperolehSalinanInformasi
  • Proper typing dengan Prisma types
  • ApiFetch consistency untuk load dropdown data

Code Example ( GOOD):

// state file - Line ~24-40
const jenisInformasiDiminta = proxy({
  findMany: {
    data: null as Prisma.JenisInformasiDimintaGetPayload<{ omit: { isActive: true } }>[] | null,
    async load() {
      const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();
      if (res.status === 200) {
        jenisInformasiDiminta.findMany.data = res.data?.data ?? [];
      }
    },
  },
});

Verdict: BAIK - Related data management yang proper!


⚠️ ISSUES & SARAN PERBAIKAN

🔴 CRITICAL

1. Schema - deletedAt Default Value SALAH (MULTIPLE MODELS)

Lokasi: prisma/schema.prisma (line 435-467)

Masalah:

model PermohonanInformasiPublik {
  // ...
  deletedAt  DateTime  @default(now())  // ❌ SALAH - selalu punya default value
  isActive   Boolean   @default(true)
}

model JenisInformasiDiminta {
  // ...
  deletedAt  DateTime  @default(now())  // ❌ SALAH
  isActive   Boolean   @default(true)
}

model CaraMemperolehInformasi {
  // ...
  deletedAt  DateTime  @default(now())  // ❌ SALAH
  isActive   Boolean   @default(true)
}

model CaraMemperolehSalinanInformasi {
  // ...
  deletedAt  DateTime  @default(now())  // ❌ SALAH
  isActive   Boolean   @default(true)
}

Dampak:

  • LOGIC ERROR! Setiap record baru langsung punya deletedAt value (timestamp creation)
  • Soft delete tidak berfungsi dengan benar
  • Query dengan where: { deletedAt: null } tidak akan pernah return data
  • Data yang "dihapus" vs data "aktif" tidak bisa dibedakan
  • 4 models affected! (PermohonanInformasiPublik + 3 related models)

Rekomendasi: Fix semua schema:

model PermohonanInformasiPublik {
  // ...
  deletedAt  DateTime? @default(null)  // ✅ Nullable, null = not deleted
  isActive   Boolean   @default(true)
}

model JenisInformasiDiminta {
  // ...
  deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

model CaraMemperolehInformasi {
  // ...
  deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

model CaraMemperolehSalinanInformasi {
  // ...
  deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

Priority: 🔴 CRITICAL
Effort: Medium (perlu migration untuk 4 models)
Impact: HIGH (data integrity & soft delete logic)


2. State Management - Fetch Pattern Inconsistency

Lokasi: src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts

Masalah: Ada 2 pattern berbeda untuk fetch API:

// ❌ Pattern 1: ApiFetch (create, findMany, dropdowns)
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(form);
const res = await ApiFetch.api.ppid.permohonaninformasipublik["find-many"].get({ query });
const res = await ApiFetch.api.ppid.permohonaninformasipublik.jenisInformasi["find-many"].get();

// ❌ Pattern 2: fetch manual (findUnique)
const res = await fetch(`/api/ppid/permohonaninformasipublik/${id}`);

Dampak:

  • Code consistency buruk
  • Sulit maintenance
  • Type safety tidak konsisten
  • Duplikasi logic error handling

Rekomendasi: Gunakan ApiFetch untuk semua operasi:

// ✅ Unified pattern
async load(id: string) {
  try {
    const res = await ApiFetch.api.ppid.permohonaninformasipublik[id].get();
    
    if (res.data?.success) {
      statepermohonanInformasiPublik.findUnique.data = res.data.data;
    } else {
      toast.error(res.data?.message || "Gagal memuat data");
    }
  } catch (error) {
    console.error("Error:", error);
    toast.error("Gagal memuat data");
  }
}

Priority: 🔴 High
Effort: Medium (refactor di findUnique method)


3. Console.log di Production

Lokasi: src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts

Masalah:

// Line ~70
console.log(caraMemperolehSalinanInformasi); // ❌ Debug log

// Line ~160
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);

// Line ~165
console.error("Error loading permohonan keberatan informasi:", error);

// Line ~185
console.error("Failed to fetch program inovasi:", res.statusText);

// Line ~188
console.error("Error fetching program inovasi:", error);

Rekomendasi: Gunakan conditional logging:

if (process.env.NODE_ENV === 'development') {
  console.error("Error:", error);
}

Priority: 🟡 Low
Effort: Low


4. Missing Delete/Hard Delete Protection

Lokasi: page.tsx, [id]/page.tsx

Masalah:

  • Tidak ada tombol delete untuk Permohonan Informasi (correct - read-only data)
  • GOOD: Read-only pattern yang benar untuk data permohonan
  • ⚠️ ISSUE: Tidak ada fitur untuk mark sebagai "processed" atau "completed"

Issue: User tidak bisa update status permohonan (pending → processed → completed).

Rekomendasi: Add status management:

// Add to schema
model PermohonanInformasiPublik {
  // ...
  status String @default("pending") // pending, processed, completed
  processedAt DateTime?
  processedBy String?
}
// Add action buttons di detail page
<Group>
  <Button color="yellow" onClick={() => updateStatus("processed")}>
    Mark as Processed
  </Button>
  <Button color="green" onClick={() => updateStatus("completed")}>
    Mark as Completed
  </Button>
</Group>

Priority: 🔴 Medium
Effort: Medium (perlu schema change + UI update)


🟡 MEDIUM

5. Type Safety - Any Usage

Lokasi: src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts

Masalah:

// Line ~145
const query: any = { page, limit }; // ❌ Using 'any'
if (search) query.search = search;

Rekomendasi: Gunakan typed query:

// Define type
interface FindManyQuery {
  page: number | string;
  limit?: number | string;
  search?: string;
}

// Use typed query
const query: FindManyQuery = { page, limit };
if (search) query.search = search;

Priority: 🟡 Medium
Effort: Low


6. Error Message Tidak Konsisten

Lokasi: Multiple places

Masalah:

// Line ~160
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
// ⚠️ Wrong module name - ini "permohonan informasi publik" bukan "keberatan"

// Line ~165
console.error("Error loading permohonan keberatan informasi:", error);
// ⚠️ Same issue

// Line ~185
console.error("Failed to fetch program inovasi:", res.statusText);
// ⚠️ Wrong module name - ini "permohonan informasi" bukan "program inovasi"

// Line ~188
console.error("Error fetching program inovasi:", error);
// ⚠️ Same issue

Issue: Copy-paste error dari module lain!

Rekomendasi: Fix error messages:

console.error("Failed to load permohonan informasi publik:", res.data?.message);
console.error("Error loading permohonan informasi publik:", error);
console.error("Failed to fetch permohonan informasi:", res.statusText);
console.error("Error fetching permohonan informasi:", error);

Priority: 🟡 Medium
Effort: Low


Lokasi: page.tsx

Masalah:

// Line ~250-260
<Pagination
  value={page}
  onChange={(newPage) => {
    load(newPage, 10);  // ⚠️ Missing search parameter
    window.scrollTo(0, 0);
  }}
  total={totalPages}
  // ...
/>

Issue: Saat ganti page, search query hilang.

Rekomendasi: Include search:

onChange={(newPage) => {
  load(newPage, 10, debouncedSearch);  // ✅ Include search
  window.scrollTo(0, 0);
}}

Priority: 🟡 Low
Effort: Low


🟢 LOW (Minor Polish)

8. Missing Loading State di Detail Page

Lokasi: [id]/page.tsx

Masalah:

// Line ~20-25
useShallowEffect(() => {
  state.findUnique.load(params?.id as string)
}, [params?.id])

if (!state.findUnique.data) {
  return (
    <Stack py={10}>
      <Skeleton height={500} radius="md" />
    </Stack>
  )
}

Issue: Skeleton ditampilkan untuk semua kondisi (loading, error, not found).

Rekomendasi: Add proper loading state:

if (state.findUnique.loading) {
  return (
    <Stack py={10}>
      <Skeleton height={500} radius="md" />
    </Stack>
  );
}

if (!state.findUnique.data) {
  return (
    <Alert icon={<IconAlertCircle />} color="red">
      Data tidak ditemukan
    </Alert>
  );
}

Priority: 🟢 Low
Effort: Low


9. Duplicate Error Logging

Lokasi: page.tsx, state file

Masalah:

// page.tsx - Line ~160-165
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
console.error("Error loading permohonan keberatan informasi:", error);

// state file - Line ~185-188
console.error("Failed to fetch program inovasi:", res.statusText);
console.error("Error fetching program inovasi:", error);

Rekomendasi: Cukup satu logging yang informatif:

console.error('Failed to load Permohonan Informasi Publik:', err);

Priority: 🟢 Low
Effort: Low


10. Search Placeholder Tidak Spesifik

Lokasi: page.tsx

Masalah:

// Line ~70, 110
<TextInput
  placeholder={"Cari nama..."}  // ⚠️ Generic
  // ...
/>

Rekomendasi: Lebih spesifik:

placeholder={"Cari nama pemohon..."}

Priority: 🟢 Low
Effort: Low


11. Missing Data Relationships di Detail Page

Lokasi: [id]/page.tsx

Masalah:

// Line ~60-90
<Box>
  <Text fz="lg" fw="bold" mb={4}>Jenis Informasi</Text>
  <Text fz="md" c="dimmed">{data.jenisInformasiDiminta?.name || '-'}</Text>
</Box>

<Box>
  <Text fz="lg" fw="bold" mb={4}>Cara Akses Informasi</Text>
  <Text fz="md" c="dimmed">{data.caraMemperolehInformasi?.name || '-'}</Text>
</Box>

<Box>
  <Text fz="lg" fw="bold" mb={4}>Cara Akses Salinan Informasi</Text>
  <Text fz="md" c="dimmed">{data.caraMemperolehSalinanInformasi?.name || '-'}</Text>
</Box>

Issue: Tidak menampilkan data alamat yang ada di schema.

Rekomendasi: Add missing field:

<Box>
  <Text fz="lg" fw="bold" mb={4}>Alamat</Text>
  <Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box>

Priority: 🟢 Low
Effort: Low


12. Unused Console.log

Lokasi: src/app/admin/(dashboard)/_state/ppid/permohonan_informasi_publik/permohonanInformasiPublik.ts

Masalah:

// Line ~70
console.log(caraMemperolehSalinanInformasi); // ❌ Debug log yang tidak terpakai

Rekomendasi: Remove:

// Remove this line completely

Priority: 🟢 Low
Effort: Low


13. Missing Empty State Icon di Mobile

Lokasi: page.tsx

Masalah:

// Line ~60-75 (Desktop empty state)
<Stack align="center" py="xl" ta="center">
  <IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
  <Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
    {search
      ? 'Tidak ditemukan data yang sesuai dengan pencarian'
      : 'Belum ada permohonan yang tercatat'
    }
  </Text>
</Stack>

// Line ~120-130 (Mobile - missing icon)
<Stack align="center" py={{ base: 'xl', md: 'xl' }}>
  <IconInfoCircle size={40} stroke={1.5} color={colors['blue-button']} />
  // ✅ Icon ada di sini juga
  <Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
    Belum ada permohonan informasi yang tercatat
  </Text>
</Stack>

Verdict: SUDAH BENAR - Icon ada di kedua empty states!

Priority: 🟢 None
Effort: None


📋 RINGKASAN ACTION ITEMS

Priority Issue Module Impact Effort Status
🔴 P0 Schema deletedAt default SALAH (4 models) Schema CRITICAL Medium MUST FIX
🔴 P0 Fetch method inconsistency State Medium Medium Perlu refactor
🔴 P1 Missing status management UI/Schema Medium Medium Should add
🟡 M Console.log in production State Low Low Optional
🟡 M Type safety (any usage) State Low Low Optional
🟡 M Error message inconsistency (copy-paste) State Low Low Should fix
🟡 M Pagination missing search param UI Low Low Should fix
🟢 L Missing loading state di detail page UI Low Low Optional
🟢 L Duplicate error logging UI/State Low Low Optional
🟢 L Search placeholder tidak spesifik UI Low Low Optional
🟢 L Missing alamat field di detail page UI Low Low Optional
🟢 L Unused console.log State Low Low Optional

KESIMPULAN

Overall Quality: 🟢 BAIK (7.5/10)

Strengths:

  1. UI/UX clean & responsive
  2. Table layout dengan icon yang helpful
  3. Search functionality dengan debounce
  4. Empty state handling yang informatif
  5. Zod validation comprehensive dengan specific rules
  6. Related data management proper (dropdowns)
  7. State management dengan ApiFetch untuk create & findMany
  8. Loading state management dengan finally block
  9. Mobile cards responsive

Critical Issues:

  1. ⚠️ Schema deletedAt default SALAH - 4 models affected (CRITICAL)
  2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
  3. ⚠️ Missing status management untuk permohonan (pending → processed → completed)

Areas for Improvement:

  1. ⚠️ Fix schema deletedAt untuk 4 models dari @default(now()) ke @default(null) dengan nullable
  2. ⚠️ Refactor fetch methods untuk gunakan ApiFetch consistently
  3. ⚠️ Add status management untuk tracking status permohonan
  4. ⚠️ Fix error messages (copy-paste error dari module lain)
  5. ⚠️ Improve type safety dengan remove any usage

Recommended Next Steps:

  1. 🔴 CRITICAL: Fix schema deletedAt untuk 4 models - 1 jam (perlu migration)
  2. 🔴 HIGH: Refactor findUnique ke ApiFetch - 30 menit
  3. 🔴 HIGH: Add status management - 1 jam (schema + UI)
  4. 🟡 MEDIUM: Fix error messages (copy-paste) - 10 menit
  5. 🟢 LOW: Add pagination search param - 10 menit
  6. 🟢 LOW: Polish minor issues - 30 menit

📈 COMPARISON WITH OTHER MODULES

Module Fetch Pattern State Validation Schema Status Mgmt Overall
Profil ⚠️ Mixed ⚠️ Good Good ⚠️ deletedAt N/A 🟢
Desa Anti Korupsi ⚠️ Mixed ⚠️ Good Good ⚠️ deletedAt N/A 🟢
SDGs Desa ⚠️ Mixed ⚠️ Good Good ⚠️ deletedAt N/A 🟢
APBDes ⚠️ Mixed ⚠️ Good Good Good N/A 🟢
Prestasi Desa ⚠️ Mixed ⚠️ Good Good WRONG N/A 🟢
PPID Profil ⚠️ Mixed Best Good WRONG N/A 🟢
Struktur PPID ⚠️ Mixed Good Good ⚠️ Inconsistent Active/Non-active 🟢
Visi Misi PPID 100% ApiFetch! Best Good WRONG N/A 🟢
Dasar Hukum PPID 100% ApiFetch! Best Good WRONG N/A 🟢
Permohonan Informasi ⚠️ Mixed ⚠️ Good Best 4 models WRONG Missing 🟡

Permohonan Informasi PPID Highlights:

  • Best validation - Comprehensive Zod schema dengan specific rules
  • Related data management - Separate proxy states untuk dropdowns
  • Icon integration - Table headers dengan icon yang helpful
  • ⚠️ 4 models affected - deletedAt issue (most affected module!)
  • ⚠️ Missing status management - No workflow tracking
  • ⚠️ Copy-paste errors - Error messages dari module lain

🎯 UNIQUE FEATURES OF PERMOHONAN INFORMASI MODULE

Most Complex Data Structure:

  1. 3 related dropdown models - JenisInformasi, CaraMemperoleh, CaraMemperolehSalinan
  2. Comprehensive validation - Phone length, NIK length, email format
  3. Icon integration - User, ID, Phone, Info icons di table headers
  4. Auto-increment nomor - Automatic numbering system
  5. Missing status workflow - Should have pending → processed → completed

Best Practices:

  1. Validation comprehensive - Best Zod schema dengan specific rules
  2. Related data management - Separate proxy states
  3. Icon integration - Visual clarity di table headers
  4. Loading state management - Proper dengan finally block

Critical Issues:

  1. 4 models dengan deletedAt SALAH - Most affected module!
  2. Fetch pattern inconsistency - findUnique pakai fetch manual
  3. Missing status workflow - No tracking untuk permohonan status
  4. Copy-paste error messages - Dari module lain

Catatan: Permohonan Informasi PPID adalah MODULE DENGAN VALIDATION TERBAIK tapi juga MODULE DENGAN PALING BANYAK MODEL AFFECTED oleh deletedAt issue (4 models!). Module ini butuh status management workflow untuk tracking status permohonan.

Unique Strengths:

  1. Best validation - Comprehensive Zod schema
  2. Related data management - 3 dropdown models handled properly
  3. Icon integration - Visual clarity
  4. Auto-increment nomor - Automatic numbering

Priority Action:

🔴 FIX INI SEKARANG (1 JAM + MIGRATION):
File: prisma/schema.prisma
Line: 435-467

model PermohonanInformasiPublik {
  // ...
- deletedAt  DateTime  @default(now())
+ deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

model JenisInformasiDiminta {
  // ...
- deletedAt  DateTime  @default(now())
+ deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

model CaraMemperolehInformasi {
  // ...
- deletedAt  DateTime  @default(now())
+ deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

model CaraMemperolehSalinanInformasi {
  // ...
- deletedAt  DateTime  @default(now())
+ deletedAt  DateTime? @default(null)
  isActive   Boolean   @default(true)
}

# Lalu jalankan:
bunx prisma db push
# atau
bunx prisma migrate dev --name fix_deletedat_permohonan_informasi
🔴 ADD STATUS MANAGEMENT (1 JAM):
File: prisma/schema.prisma

model PermohonanInformasiPublik {
  // ...
+ status       String    @default("pending") // pending, processed, completed
+ processedAt  DateTime?
+ processedBy  String?
}

Setelah fix critical issues, module ini PRODUCTION-READY dengan BEST VALIDATION! 🎉


Permohonan Informasi PPID Module adalah BEST PRACTICE untuk:

  1. Comprehensive validation - Zod schema dengan specific rules (phone, NIK length)
  2. Related data management - Separate proxy states untuk dropdowns
  3. Icon integration - Visual clarity di table headers
  4. Auto-increment numbering - Automatic nomor urut

Modules lain bisa belajar dari Permohonan Informasi:

  • ALL MODULES: Use specific validation rules (min/max length)
  • MODULES WITH DROPDOWNS: Separate proxy states untuk related data
  • ALL MODULES: Icon integration untuk visual clarity
  • ALL MODULES: Auto-increment untuk numbering systems

File Location: QC/PPID/QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md 📄