Files
desa-darmasaba/QC/PPID/QC-DASAR-HUKUM-PPID-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 - Dasar Hukum PPID Module

Scope: Preview Dasar Hukum, Edit Dasar Hukum dengan Rich Text Editor
Date: 2026-02-23
Status: Secara umum sudah baik, ada beberapa improvement yang diperlukan


📊 OVERVIEW

Aspect Schema API UI Admin State Management Overall
Dasar Hukum PPID ⚠️ Ada issue Baik Baik Baik 🟡 Perlu fix

YANG SUDAH BAIK

1. UI/UX Design

  • Preview layout yang clean dengan logo desa
  • Responsive design (mobile & desktop)
  • Loading states dengan Skeleton
  • Empty state handling yang informatif
  • Edit button yang prominent
  • Divider visual yang jelas antara Judul dan Content

2. Rich Text Editor (Tiptap)

  • Full-featured editor dengan toolbar lengkap (reuse dari PPIDTextEditor)
  • Extensions: Bold, Italic, Underline, Highlight, Link, dll
  • Text alignment (left, center, justify, right)
  • Heading levels (H1-H4)
  • Lists (bullet & ordered)
  • Blockquote, code, superscript, subscript
  • Undo/Redo
  • Sticky toolbar untuk UX yang lebih baik
  • Dynamic import dengan ssr: false untuk menghindari hydration issues!

3. Form Component Structure

  • Reusable PPIDTextEditor component (shared dengan Visi Misi)
  • Proper TypeScript typing
  • Controlled components dengan onChange handler
  • SSR handling yang proper dengan dynamic import

Code Example ( EXCELLENT):

// edit/page.tsx - Line ~13-17
const PPIDTextEditor = dynamic(
  () => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
  { ssr: false }  // ✅ Disable SSR untuk avoid hydration mismatch
);

Verdict: EXCELLENT - Proper SSR handling!


4. State Management

  • Proper typing dengan Prisma types
  • Loading state management dengan finally block
  • Error handling yang comprehensive
  • ApiFetch consistency - Semua operasi pakai ApiFetch!
  • Zod validation untuk form data

Code Example ( EXCELLENT):

// state file - Line ~20-45
findById: {
  data: null as DasarHukumForm | null,
  loading: false,
  initialize() {
    stateDasarHukumPPID.findById.data = {
      id: '',
      judul: '',
      content: '',
    } as DasarHukumForm;
  },
  async load(id: string) {
    try {
      stateDasarHukumPPID.findById.loading = true; // ✅ Start loading
      const res = await ApiFetch.api.ppid.dasarhukumppid["find-by-id"].get({
        query: { id },
      });
      if (res.status === 200) {
        stateDasarHukumPPID.findById.data = res.data?.data ?? null;
      }
    } catch (error) {
      console.error((error as Error).message);
      toast.error("Terjadi kesalahan saat mengambil data dasar hukum");
    } finally {
      stateDasarHukumPPID.findById.loading = false; // ✅ Stop loading
    }
  },
}

Verdict: SANGAT BAIK - State management sudah konsisten dengan ApiFetch!


5. Edit Form - Original Data Tracking

  • Original data state untuk reset form
  • Load data existing dengan benar
  • Reset form mengembalikan ke data original
  • Rich text content handling yang proper

Code Example ( GOOD):

// edit/page.tsx - Line ~20-45
const [formData, setFormData] = useState({ judul: '', content: '' });
const [originalData, setOriginalData] = useState({
  judul: '',
  content: '',
});

// Initialize from global state
useEffect(() => {
  if (dasarHukumState.findById.data) {
    setFormData({
      judul: dasarHukumState.findById.data.judul ?? '',
      content: dasarHukumState.findById.data.content ?? '',
    });
    setOriginalData({
      judul: dasarHukumState.findById.data.judul ?? '',
      content: dasarHukumState.findById.data.content ?? '',
    });
  }
}, [dasarHukumState.findById.data]);

// Line ~65 - Handle reset
const handleResetForm = () => {
  setFormData({
    judul: originalData.judul,
    content: originalData.content,
  });
  toast.info("Form dikembalikan ke data awal");
};

Verdict: BAIK - Original data tracking sudah implementasi dengan baik!


6. Rich Text Validation

  • Custom validation function untuk rich text content
  • Check empty content setelah remove HTML tags
  • Validation untuk kedua fields (judul & content)

Code Example ( GOOD):

// edit/page.tsx - Line ~25-35
const isRichTextEmpty = (content: string) => {
  // Remove HTML tags and check if the resulting text is empty
  const plainText = content.replace(/<[^>]*>/g, '').trim();
  return plainText === '' || content.trim() === '<p></p>' || content.trim() === '<p><br></p>';
};

const isFormValid = () => {
  return (
    !isRichTextEmpty(formData.judul) &&
    !isRichTextEmpty(formData.content)
  );
};

Verdict: EXCELLENT - Rich text validation yang comprehensive!


⚠️ ISSUES & SARAN PERBAIKAN

🔴 CRITICAL

1. Schema - deletedAt Default Value SALAH

Lokasi: prisma/schema.prisma (line 385)

Masalah:

model DasarHukumPPID {
  id        String   @id @default(cuid())
  judul     String   @db.Text
  content   String   @db.Text
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  deletedAt DateTime  @default(now())  // ❌ SALAH - selalu punya default value
  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

Contoh Issue:

// Record baru dibuat
CREATE DasarHukumPPID {
  judul: "Judul 1",
  content: "Content 1",
  // deletedAt otomatis ter-set ke now() ❌
  // isActive: true ✅
}

// Query untuk data aktif (seharusnya return data ini)
prisma.dasarHukumPPID.findMany({
  where: { deletedAt: null, isActive: true }
})
// ❌ Return kosong! Karena deletedAt sudah ter-set

Rekomendasi: Fix schema:

model DasarHukumPPID {
  id        String    @id @default(cuid())
  judul     String    @db.Text
  content   String    @db.Text
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime? @default(null)  // ✅ Nullable, null = not deleted
  isActive  Boolean   @default(true)
}

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


2. HTML Injection Risk - dangerouslySetInnerHTML

Lokasi: page.tsx (preview page)

Masalah:

// Line ~65-75
<Title
  order={3}
  ta="center"
  lh={{ base: 1.15, md: 1.1 }}
  fw="bold"
  c={colors['blue-button']}
  dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}  // ❌ No sanitization
  style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
/>

// Line ~80-90 (Content)
<Text
  dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}  // ❌ No sanitization
  style={{
    wordBreak: 'break-word',
    whiteSpace: 'normal',
    fontSize: '1rem',
    lineHeight: 1.55,
    textAlign: 'justify',
  }}
/>

Risk:

  • XSS attack jika admin input script malicious
  • Bisa inject iframe, script tag, dll
  • Security vulnerability

Rekomendasi: Gunakan DOMPurify atau library sanitization:

import DOMPurify from 'dompurify';

// Sanitize sebelum render
const sanitizedJudul = DOMPurify.sanitize(listDasarHukum.findById.data.judul);
const sanitizedContent = DOMPurify.sanitize(listDasarHukum.findById.data.content);

<Title
  dangerouslySetInnerHTML={{ __html: sanitizedJudul }}
  // ...
/>

<Text
  dangerouslySetInnerHTML={{ __html: sanitizedContent }}
  // ...
/>

Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya <p>, <ul>, <li>, <strong>, dll).

Priority: 🔴 HIGH (Security concern)
Effort: Low


3. Missing Delete/Hard Delete Protection

Lokasi: page.tsx, edit/page.tsx

Masalah:

  • Tidak ada tombol delete untuk Dasar Hukum (correct - single record)
  • GOOD: Single record pattern yang benar
  • ⚠️ ISSUE: Tidak ada konfirmasi sebelum update (direct save)

Issue: User bisa accidentally save changes tanpa konfirmasi.

Rekomendasi: Add confirmation dialog sebelum save:

const handleSubmit = () => {
  // Check if data has changed
  if (formData.judul === originalData.judul && formData.content === originalData.content) {
    toast.info('Tidak ada perubahan');
    return;
  }

  // Show confirmation
  const confirmed = window.confirm('Apakah Anda yakin ingin mengubah Dasar Hukum PPID?');
  if (!confirmed) return;

  // Then save...
};

Priority: 🔴 Medium
Effort: Low


🟡 MEDIUM

4. Console.log di Production

Lokasi: src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts

Masalah:

// Line ~40
console.error((error as Error).message);

// Line ~65
console.error((error as Error).message);

Rekomendasi: Gunakan conditional logging:

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

Priority: 🟡 Low
Effort: Low


5. Missing Loading State di Submit Button

Lokasi: edit/page.tsx

Masalah:

// Line ~130-140
<Button
  onClick={handleSubmit}
  radius="md"
  size="md"
  disabled={!isFormValid() || isSubmitting}
  // ...
>
  {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>

Issue: Button tidak check dasarHukumState.update.loading dari global state.

Rekomendasi: Check both states:

disabled={!isFormValid() || isSubmitting || dasarHukumState.update.loading}
{isSubmitting || dasarHukumState.update.loading ? (
  <Loader size="sm" color="white" />
) : (
  'Simpan'
)}

Priority: 🟡 Low
Effort: Low


6. Zod Schema - Could Be More Specific

Lokasi: src/app/admin/(dashboard)/_state/ppid/dasar_hukum/dasarHukum.ts

Masalah:

// Line ~7
const templateForm = z.object({
  judul: z.string().min(3, "Judul minimal 3 karakter"), // ⚠️ Generic
  content: z.string().min(3, "Content minimal 3 karakter"), // ⚠️ Generic
});

Rekomendasi: More specific error messages:

const templateForm = z.object({
  judul: z.string().min(3, "Judul dasar hukum minimal 3 karakter"),
  content: z.string().min(3, "Konten dasar hukum minimal 3 karakter"),
});

Priority: 🟡 Low
Effort: Low


🟢 LOW (Minor Polish)

7. Missing Change Detection

Lokasi: edit/page.tsx

Masalah:

// Line ~75-85
const handleSubmit = () => {
  try {
    setIsSubmitting(true);
    if (dasarHukumState.findById.data) {
      // Update global state hanya saat submit
      const updated = { ...dasarHukumState.findById.data, ...formData };
      dasarHukumState.update.save(updated);
    }
    router.push('/admin/ppid/dasar-hukum');
  } catch (error) {
    console.error("Error updating dasar hukum:", error);
    toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
  } finally {
    setIsSubmitting(false);
  }
};

Issue: Tidak ada check apakah data sudah berubah. User bisa save tanpa perubahan.

Rekomendasi: Add change detection:

const handleSubmit = () => {
  // Check if data has changed
  if (formData.judul === originalData.judul && formData.content === originalData.content) {
    toast.info('Tidak ada perubahan');
    return;
  }

  try {
    setIsSubmitting(true);
    // ... rest of save logic
  }
};

Priority: 🟢 Low
Effort: Low


8. Editor - Duplicate useEffect

Lokasi: PPIDTextEditor.tsx (shared component)

Masalah:

// Line ~30-35 (di PPIDTextEditor.tsx)
const editor = useEditor({
  extensions: [...],
  immediatelyRender: false,
  content: initialContent, // ✅ Set content directly
  onUpdate: ({editor}) => {
    onChange(editor.getHTML()) // ✅ Handle changes
  }
});

// Line ~37-42
useEffect(() => {
  if (editor && initialContent !== editor.getHTML()) {
    editor.commands.setContent(initialContent || '<p></p>');
  }
}, [initialContent, editor]);

Issue: Ada useEffect tambahan untuk set content, padahal sudah ada di useEditor. Bisa menyebabkan double content update.

Rekomendasi: Simplify - remove useEffect:

const editor = useEditor({
  extensions: [...],
  immediatelyRender: false,
  content: initialContent || '<p></p>', // ✅ Set content directly
  onUpdate: ({editor}) => {
    onChange(editor.getHTML())
  },
});

// Remove useEffect completely

Priority: 🟢 Low
Effort: Low (perlu update shared component)


9. Missing Error Boundary

Lokasi: edit/page.tsx

Masalah:

  • Tidak ada error boundary untuk handle unexpected errors
  • Jika editor gagal load, tidak ada fallback UI

Rekomendasi: Add error boundary:

if (dasarHukumState.findById.error) {
  return (
    <Alert icon={<IconAlertCircle />} color="red">
      <Text fw="bold">Error</Text>
      <Text>{dasarHukumState.findById.error}</Text>
    </Alert>
  );
}

Priority: 🟢 Low
Effort: Low


10. Preview Page - Title Order Inconsistency

Lokasi: page.tsx

Masalah:

// Line ~40
<Title order={3} ...>Preview Dasar Hukum PPID</Title>

// Line ~65
<Title order={3} ... dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }} />

Issue: Title hierarchy agak confusing. Page title dan content title sama-sama order 3.

Rekomendasi: Samakan hierarchy:

// Page title: order={2}
// Content title (judul): order={3}

Priority: 🟢 Low
Effort: Low


11. Missing Toast Success After Save

Lokasi: edit/page.tsx

Masalah:

// Line ~75-90
const handleSubmit = () => {
  try {
    setIsSubmitting(true);
    if (dasarHukumState.findById.data) {
      const updated = { ...dasarHukumState.findById.data, ...formData };
      dasarHukumState.update.save(updated);
    }
    router.push('/admin/ppid/dasar-hukum');  // ✅ Redirect tanpa toast
  } catch (error) {
    console.error("Error updating dasar hukum:", error);
    toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
  } finally {
    setIsSubmitting(false);
  }
};

Issue: Toast success ada di state update.save(), tapi user mungkin tidak lihat karena langsung redirect.

Rekomendasi: Add toast before redirect atau wait untuk toast selesai:

const handleSubmit = async () => {
  try {
    setIsSubmitting(true);
    if (dasarHukumState.findById.data) {
      const updated = { ...dasarHukumState.findById.data, ...formData };
      await dasarHukumState.update.save(updated);
      toast.success("Dasar Hukum berhasil diperbarui!");
      setTimeout(() => {
        router.push('/admin/ppid/dasar-hukum');
      }, 1000); // Wait 1 second for toast to show
    }
  } catch (error) {
    console.error("Error updating dasar hukum:", error);
    toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
  } finally {
    setIsSubmitting(false);
  }
};

Priority: 🟢 Low
Effort: Low


12. SSR Dynamic Import - Good but Could Add Loading

Lokasi: edit/page.tsx

Masalah:

// Line ~13-17
const PPIDTextEditor = dynamic(
  () => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
  { ssr: false }  // ✅ Good
);

Issue: Tidak ada loading state untuk dynamic import. Jika editor lambat load, user lihat kosong.

Rekomendasi: Add loading option:

const PPIDTextEditor = dynamic(
  () => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
  { 
    ssr: false,
    loading: () => (
      <Center py={40}>
        <Loader size="sm" />
        <Text ml="md">Loading editor...</Text>
      </Center>
    )
  }
);

Priority: 🟢 Low
Effort: Low


📋 RINGKASAN ACTION ITEMS

Priority Issue Module Impact Effort Status
🔴 P0 Schema deletedAt default SALAH Schema CRITICAL Medium MUST FIX
🔴 P0 HTML injection risk UI HIGH (Security) Low Should fix
🔴 P1 Missing delete confirmation UI Medium Low Should fix
🟡 M Console.log in production State Low Low Optional
🟡 M Missing loading state di submit button UI Low Low Should fix
🟡 M Zod schema error messages State Low Low Optional
🟢 L Missing change detection Edit UI Low Low Optional
🟢 L Duplicate useEffect di editor Editor Low Low Optional
🟢 L Missing error boundary UI Low Low Optional
🟢 L Title order inconsistency UI Low Low Optional
🟢 L Missing toast success timing UI Low Low Optional
🟢 L SSR loading state UI Low Low Optional

KESIMPULAN

Overall Quality: 🟢 BAIK (8.5/10) - CLEAN & SIMPLE!

Strengths:

  1. UI/UX clean & responsive
  2. Rich Text Editor full-featured (Tiptap, shared component)
  3. Dynamic import dengan ssr: false - Proper SSR handling!
  4. State management BEST PRACTICES - 100% ApiFetch!
  5. Edit form reset sudah benar (original data tracking)
  6. Rich text validation comprehensive (check empty content)
  7. Error handling comprehensive
  8. Loading state management dengan finally block
  9. Reusable component (PPIDTextEditor shared dengan Visi Misi)

Critical Issues:

  1. ⚠️ Schema deletedAt default SALAH - Logic error untuk soft delete (CRITICAL)
  2. ⚠️ HTML injection risk - dangerouslySetInnerHTML tanpa sanitization (HIGH Security)
  3. ⚠️ Missing confirmation sebelum save (Medium UX)

Areas for Improvement:

  1. ⚠️ Fix schema deletedAt dari @default(now()) ke @default(null) dengan nullable
  2. ⚠️ Fix HTML injection dengan DOMPurify atau backend validation
  3. ⚠️ Add confirmation dialog sebelum save
  4. ⚠️ Add change detection untuk avoid unnecessary saves
  5. ⚠️ Fix loading state di submit button

Recommended Next Steps:

  1. 🔴 CRITICAL: Fix schema deletedAt - 30 menit (perlu migration)
  2. 🔴 HIGH: Fix HTML injection dengan DOMPurify - 30 menit
  3. 🟡 MEDIUM: Add confirmation dialog - 15 menit
  4. 🟢 LOW: Add change detection - 15 menit
  5. 🟢 LOW: Add SSR loading state - 10 menit
  6. 🟢 LOW: Polish minor issues - 30 menit

📈 COMPARISON WITH OTHER MODULES

Module Fetch Pattern State Edit Reset Rich Text SSR Handling HTML Injection deletedAt Overall
Profil ⚠️ Mixed ⚠️ Good Good None N/A ⚠️ Present ⚠️ Issue 🟢
Desa Anti Korupsi ⚠️ Mixed ⚠️ Good Good Present ⚠️ None ⚠️ Present ⚠️ Issue 🟢
SDGs Desa ⚠️ Mixed ⚠️ Good Good None N/A N/A ⚠️ Issue 🟢
APBDes ⚠️ Mixed ⚠️ Good Good None N/A N/A Good 🟢
Prestasi Desa ⚠️ Mixed ⚠️ Good Good Present ⚠️ None ⚠️ Present WRONG 🟢
PPID Profil ⚠️ Mixed Best Excellent Best ⚠️ None ⚠️ Present WRONG 🟢
Struktur PPID ⚠️ Mixed Good Good Present ⚠️ None ⚠️ Present ⚠️ Inconsistent 🟢
Visi Misi PPID 100% ApiFetch! Best Good Present ⚠️ None ⚠️ Present WRONG 🟢
Dasar Hukum PPID 100% ApiFetch! Best Good Present EXCELLENT ⚠️ Present WRONG 🟢

Dasar Hukum PPID Highlights:

  • 100% ApiFetch - NO fetch manual sama sekali!
  • SSR Handling - Dynamic import dengan ssr: false (UNIQUE!)
  • Reusable component - Share PPIDTextEditor dengan Visi Misi
  • Simple & clean - No unnecessary complexity
  • ⚠️ Same deletedAt issue seperti modul PPID lain

🎯 UNIQUE FEATURES OF DASAR HUKUM PPID MODULE

Simplest & Cleanest Module:

  1. 100% ApiFetch consistency - NO fetch manual sama sekali!
  2. SSR Handling - Dynamic import dengan ssr: false (UNIQUE!)
  3. Reusable component - Share PPIDTextEditor dengan Visi Misi
  4. Simple single record pattern - Only 2 fields (judul, content)
  5. Rich text validation - Check empty content

Best Practices:

  1. API consistency - 100% ApiFetch
  2. SSR handling - Best practice untuk Next.js
  3. Loading state management proper (dengan finally block)
  4. Rich text validation comprehensive
  5. Original data tracking untuk reset form
  6. Component reusability - Share editor component

Critical Issues:

  1. Schema deletedAt SALAH - Same issue seperti modul PPID lain
  2. HTML injection risk - Same issue seperti modul dengan rich text lain

Catatan: Dasar Hukum PPID adalah MODULE PALING CLEAN bersama Visi Misi PPID dengan codebase paling simple dan 100% PAKAI ApiFetch (no fetch manual sama sekali!). Module ini juga SATU-SATUNYA MODULE yang punya proper SSR handling dengan dynamic import!

Unique Strengths:

  1. 100% ApiFetch - Best API consistency
  2. SSR Handling - Best practice untuk Next.js (UNIQUE!)
  3. Component reusability - Share editor component
  4. Simple & clean - No unnecessary complexity
  5. Rich text validation - Most comprehensive

Priority Action:

🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
File: prisma/schema.prisma
Line: 385

model DasarHukumPPID {
  id        String    @id @default(cuid())
  judul     String    @db.Text
  content   String    @db.Text
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
- 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_dasarhukum_ppid
🔴 FIX HTML INJECTION (30 MENIT):
File: page.tsx
+ import DOMPurify from 'dompurify';

// Line ~65
- dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.judul }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.judul) }}

// Line ~80
- dangerouslySetInnerHTML={{ __html: listDasarHukum.findById.data.content }}
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listDasarHukum.findById.data.content) }}

Setelah fix critical issues, module ini PRODUCTION-READY dan bisa jadi REFERENCE untuk SSR HANDLING & API CONSISTENCY! 🎉


Dasar Hukum PPID Module adalah BEST PRACTICE untuk:

  1. API consistency - 100% ApiFetch, NO fetch manual!
  2. SSR handling - Dynamic import dengan ssr: false
  3. Simple state management - Clean, straightforward
  4. Rich text validation - Check empty content pattern
  5. Component reusability - Share editor component

Modules lain bisa belajar dari Dasar Hukum PPID:

  • ALL MODULES: Use ApiFetch consistently (NO fetch manual!)
  • ALL MODULES WITH RICH TEXT: Use dynamic import dengan ssr: false
  • ALL MODULES: Keep it simple (avoid unnecessary complexity)
  • Rich Text Modules: Implement empty content validation
  • ALL MODULES: Share reusable components

File Location: QC/PPID/QC-DASAR-HUKUM-PPID-MODULE.md 📄