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)
This commit is contained in:
658
QC/DESA/summary-qc-potensi-desa.md
Normal file
658
QC/DESA/summary-qc-potensi-desa.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# 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`
|
||||
|
||||
```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:**
|
||||
```prisma
|
||||
model PotensiDesa {
|
||||
name String @unique // ✅ Add unique constraint
|
||||
// ...
|
||||
}
|
||||
|
||||
model KategoriPotensi {
|
||||
nama String @unique // ✅ Add unique constraint
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Migration Required:**
|
||||
```bash
|
||||
bunx prisma db push
|
||||
# atau
|
||||
bunx prisma migrate dev --name add_unique_constraints
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Schema - `kategoriId` Nullable Seharusnya Required
|
||||
|
||||
**File:** `prisma/schema.prisma`
|
||||
|
||||
```prisma
|
||||
model PotensiDesa {
|
||||
kategoriId String? // ❌ Nullable, seharusnya required
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** Potensi desa bisa dibuat tanpa kategori, tidak masuk akal secara bisnis.
|
||||
|
||||
**Solusi:**
|
||||
```prisma
|
||||
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`
|
||||
|
||||
```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:**
|
||||
```prisma
|
||||
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`
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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`
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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`:**
|
||||
```typescript
|
||||
<TableTd dangerouslySetInnerHTML={{ __html: item.nama }} />
|
||||
```
|
||||
|
||||
**`list-potensi/page.tsx`:**
|
||||
```typescript
|
||||
<TableTd dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- User bisa inject malicious script melalui rich text editor
|
||||
- XSS attack bisa mencuri session atau data sensitif
|
||||
|
||||
**Solusi:**
|
||||
```typescript
|
||||
// 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):**
|
||||
```typescript
|
||||
// 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):
|
||||
```bash
|
||||
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`
|
||||
|
||||
```typescript
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10); // ❌ Tidak ada search parameter
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**Dampak:** Saat ganti halaman, search query hilang.
|
||||
|
||||
**Solusi:**
|
||||
```typescript
|
||||
<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`
|
||||
|
||||
```typescript
|
||||
<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:**
|
||||
```typescript
|
||||
<TableTd colSpan={3}> // ✅ Match column count
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. UI - Alert Instead of Toast
|
||||
|
||||
**File:** `src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx`
|
||||
|
||||
```typescript
|
||||
if (!nama.trim()) {
|
||||
alert('Nama kategori potensi wajib diisi'); // ❌ Browser alert
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** Browser alert blocking, UX buruk, tidak konsisten dengan page lain.
|
||||
|
||||
**Solusi:**
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
potensiState.kategoriPotensi.findMany.load();
|
||||
load(page, 10, debouncedSearch);
|
||||
}, [page, debouncedSearch]); // ❌ Missing potensiState
|
||||
```
|
||||
|
||||
**Dampak:** ESLint warning, potential stale closure.
|
||||
|
||||
**Solusi:**
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
<Dropzone
|
||||
accept={{ "image/*": [] }} // ❌ Terlalu general
|
||||
// ...
|
||||
>
|
||||
```
|
||||
|
||||
**Dampak:** User bisa upload format image aneh yang tidak didukung browser.
|
||||
|
||||
**Solusi:**
|
||||
```typescript
|
||||
<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`
|
||||
|
||||
```typescript
|
||||
const isDetailPage = segments.length >= 5; // ❌ Magic number
|
||||
```
|
||||
|
||||
**Dampak:** Tidak jelas maksudnya, brittle jika ada perubahan route structure.
|
||||
|
||||
**Solusi:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
// 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`
|
||||
|
||||
```typescript
|
||||
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:
|
||||
```bash
|
||||
# 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:
|
||||
```bash
|
||||
bun add dompurify
|
||||
bun add -D @types/dompurify
|
||||
```
|
||||
|
||||
Usage:
|
||||
```typescript
|
||||
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:
|
||||
```bash
|
||||
# 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](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
|
||||
- [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
|
||||
- [Mantine Table Documentation](https://mantine.dev/core/table/)
|
||||
- [React Toastify Documentation](https://fkhadra.github.io/react-toastify/)
|
||||
|
||||
---
|
||||
|
||||
**Dibuat oleh:** QC Automation
|
||||
**Review Status:** ⏳ Menunggu Review Developer
|
||||
**Next Review:** Setelah implementasi fixes
|
||||
Reference in New Issue
Block a user