Compare commits
366 Commits
join
...
nico/18-fe
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c0abd7f08 | |||
| ef7763f01c | |||
| a319484907 | |||
| 458797ae38 | |||
| 1ddc1d7eac | |||
| aa354992e7 | |||
| d43b07c2ef | |||
| 9678e6979b | |||
| b35874b120 | |||
| b69df2454e | |||
| df198c320a | |||
| f550e29a75 | |||
| bb7384f1e5 | |||
| df154806f7 | |||
| 25000d0b0f | |||
| bbd52fb6f5 | |||
| 358ff14efe | |||
| 6c36a15290 | |||
| da585dde99 | |||
| 8afbaabd91 | |||
| f0425cfc47 | |||
| c2ad515366 | |||
| d9ce4aac6d | |||
| 3fcfec22fb | |||
| 6ca1e032a6 | |||
| 78c55a8a71 | |||
| 17b20e0d40 | |||
| 184854d273 | |||
| 903dc74cca | |||
| 503da91ce6 | |||
| daaed8089b | |||
| f436aa2ef0 | |||
| 50bc54ceca | |||
| f0f201c853 | |||
| 29065cb3e2 | |||
| bf20cd55e8 | |||
| af60bcd6fc | |||
| dc8793e3ae | |||
| c8484357cb | |||
| 342e9bbc65 | |||
| f6f77d9e35 | |||
| a00481152c | |||
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 | |||
| dcb8017594 | |||
| ec3ad12531 | |||
| dad44c0537 | |||
| 867dce42f0 | |||
| 7bb17ddf22 | |||
| a4069d3cba | |||
| ffe5e6dd9f | |||
| dcf195f54f | |||
| c03a6b3aed | |||
| 1bb9f239db | |||
| a213ff7d37 | |||
| 0018bdc251 | |||
| 83fb39a957 | |||
| 7238692dd0 | |||
| 8b50139d79 | |||
| 066180fc0e | |||
| 67f29aabef | |||
| dbf7c34228 | |||
| 036fc86fed | |||
| 2cecec733e | |||
| c64a2e5457 | |||
| 757911d7dd | |||
| 54232e4465 | |||
| 29a9a59bca | |||
| 2fb3666e57 | |||
| e30b27f7a4 | |||
| e941ed3893 | |||
| ace5aff1b6 | |||
| 716db0adca | |||
| a291bdfb51 | |||
| 0dff8f3254 | |||
| 78b8aa74cd | |||
| a0537810e8 | |||
| b3c169a2d4 | |||
| 2608a5ffdd | |||
| 6c32f3ebdb | |||
| 0feeb4de93 | |||
| 9622eb5a9a | |||
| 417a8937f5 | |||
| db8909b9ed | |||
| f66a46f645 | |||
| fb57698dc9 | |||
| d128313e71 | |||
| 7b4bb1e58e | |||
| 0befe6a3f2 | |||
| a6663bbcee | |||
| ed371bd0d9 | |||
| f82c7b86e0 | |||
| b5d6585cd5 | |||
| aa98359ef7 | |||
| 0ff0d5234a | |||
| 827c1c191a | |||
| fb596f9033 | |||
| 9055b40769 | |||
| bbf13c1cf7 | |||
| 75bf0652b1 | |||
| 0b574406e2 | |||
| ccf39bc778 | |||
| 3c21f7742c | |||
| a158241c0b | |||
| 80c5dc6361 | |||
| 8ad38fc907 | |||
| d601b2fee3 | |||
| cee0957e07 | |||
| 5c66eccf23 | |||
| f7fd9be255 | |||
| 8a6d8ed8db | |||
| 63054cedf0 | |||
| c2f1ab8179 | |||
| 295d6f7d63 | |||
| dbd56a1493 | |||
| 2a26db6e17 | |||
| 33fc472472 | |||
| d8fa56d923 | |||
| cac146471a | |||
| 3e4a7a1c0a | |||
| b5c044df6e | |||
| 0fc47c28ff | |||
| 8e25c91e85 | |||
| 068d8b1077 | |||
| 9f72e94557 | |||
| 79ad39fc55 | |||
| 39e1e7b575 | |||
| 4ceea5203f | |||
| a5d841bb6b | |||
| 6a7bd386ae | |||
| a9d98895bb | |||
| 75475dc62e | |||
| b39800a475 | |||
| 797713ef49 | |||
| 8817b937b1 | |||
| 2adf60f9eb | |||
| fa9601e126 | |||
| 7ae83788b4 | |||
| 22ec8d942d | |||
| 9f9a0fb451 | |||
| b6d6583e77 | |||
| a8fd715822 | |||
| f9530c32eb | |||
| f15ef5a275 | |||
| 3a726a3334 | |||
| b21e1f0c2e | |||
| f63249327d | |||
| bb8dab05ba | |||
| 3081e426bd | |||
| 8a275c2a32 | |||
| 8469ebd2e1 | |||
| 760ba4b6d2 | |||
| 20d4c90e60 | |||
| fafbb12a08 | |||
| 01aa0da5cc | |||
| b580978f8e | |||
| 1c01397c0d | |||
| 90a6605efd | |||
| c22d865283 | |||
| 49067f0218 | |||
| d79425d529 | |||
| 4491d23bea | |||
| 1e154ced86 | |||
| bcc51aec12 | |||
| 8d15563f15 | |||
| d7a592c635 | |||
| 5e137ba658 | |||
| c99416c7f8 | |||
| 212e2db1fb | |||
| b8a45bc451 | |||
| 0777b00a7d | |||
| a035039b2c | |||
| a6832cad40 | |||
| a1d55e2b0a | |||
| c1583c21b1 | |||
| 2fe8b8ce1a | |||
| 5cbf7810bc | |||
| b3bf6b0327 | |||
| a65529cb23 | |||
| afc7bced44 | |||
| 0ac9fa1f53 | |||
| d4af56b508 | |||
| b62c4be30a | |||
| ab887c30e6 | |||
| 8e76a83d14 | |||
| a2b68ec78b | |||
| 0e55462adc | |||
| 73ae198158 | |||
| 9d14bb0c56 | |||
| 1cdff53c56 | |||
| 54312e9486 | |||
| 024d5517fa | |||
| 4e61695649 | |||
| c11cc421a4 | |||
| 0109886e00 | |||
| 50e8999205 | |||
| e2e1672c80 | |||
| ac0eb926eb | |||
| b24bcd8019 | |||
| 5601e59922 | |||
| a25cfe8b8a | |||
| b745bd4623 | |||
| bdf751ec3d | |||
| 1bc6dd8dbf | |||
| 88a10538a7 | |||
| d4efcacf1b | |||
| 9b2201ea57 | |||
| 80a7df663e | |||
| 9dfcda7687 | |||
| e2f75ff3ad | |||
| f05a096633 | |||
| 9c55869aa6 | |||
| 6e5d45fa20 | |||
| e5373b4823 | |||
| 928cd048c0 | |||
| 41f54772e9 | |||
| cd343badb2 | |||
| 4025771a4d | |||
| 7439eb7687 | |||
| 49a1084099 | |||
| cde6c91cd4 | |||
| 55433128a9 | |||
| e8ad74d118 | |||
| 99c1fd1004 | |||
| 03c0523194 | |||
| ae328f40a0 | |||
| 6e109ffe00 | |||
| c4aea568e9 | |||
| 1c8104ee69 | |||
| 4baffe95f3 | |||
| cb52701f47 | |||
| 2bc9b2f3c6 | |||
| 7b2b306849 | |||
| d328f64d86 | |||
| 119275b95c | |||
| 124dfb8160 | |||
| 46c79b8ded | |||
| d105293149 | |||
| adcbe3aa3d | |||
| bffe648802 | |||
| 7e95d5fbb4 | |||
| 2725c2c064 | |||
| be189df37c | |||
| c0b941395d | |||
| a2e25a3e3a | |||
| d86824a943 | |||
| c823462a47 | |||
| 4f97c01501 | |||
| 0fd47e3e94 | |||
| b92a974dcd | |||
| 10361770b4 | |||
| aec2f5094a | |||
| 72d39b020a | |||
| 51d67736ef | |||
| 406c6f3c9f | |||
| 1c5e4410c4 | |||
| 4724b7473d | |||
| 32a75bcb01 | |||
| c5fc4f4cea | |||
| 9f39eb41ab | |||
| 81ea18cb07 | |||
| dd7ce6943d | |||
| ee10f339e9 | |||
| 02462b2c19 | |||
| 41181d4cb3 | |||
| 6d5b8dcf64 | |||
| 924be5b11b | |||
| 21085ce342 | |||
| 88784f00f6 | |||
| 4f6cc66b7c | |||
| 456342851b | |||
| 4683034cd7 | |||
| 37de71a75a | |||
| 27fa7ac0fc | |||
| fc08b2e790 | |||
| 4a5524ce88 | |||
| 899883ca2a | |||
| 10ecc13ad7 | |||
| 58f538425c | |||
| fa922c7127 | |||
| 45acdba93f | |||
| d2f53ff69b | |||
| 40f0294595 | |||
| cb8d561467 | |||
| 85a0cb6d56 | |||
| 6ed0246cea | |||
| af726043bd | |||
| f4888b53ab | |||
| f7437708c0 | |||
| 7bf5ee69d5 | |||
| e03b071b00 | |||
| 8ded234991 | |||
| 1462e1d256 | |||
| a1c2821153 | |||
| 9f66b037f9 | |||
| 9e725e2eea | |||
| e4b7418ed3 | |||
| 6d312b7a28 | |||
| 41ae92774d | |||
| 46748205fd | |||
| 5e74447056 | |||
| c9d0ea2a97 | |||
| 7d58513e33 | |||
| f56c5b3532 | |||
| 06622c49e8 | |||
| a1e7fddbed | |||
| 423ad0e2ba | |||
| 084435500f | |||
| 4f2c565b2e | |||
| c4adc9bb22 | |||
| 5037009c40 | |||
| 9d572f82c3 | |||
| 452692f314 | |||
| 8f2b9665a9 | |||
| 77f99a7c8f | |||
| d88f168258 | |||
| f9bd2cea11 | |||
| 5010677bc8 | |||
| 5734e5d9a7 | |||
| 7af3fbff2d | |||
| 3654629bde | |||
| 34ca736dda | |||
| 02738104b5 | |||
| 92de697ae0 | |||
| cf6a5422ec | |||
| ee9368e911 | |||
| cab86eb02f | |||
| d1e39ae7f9 | |||
| d3a43c72ab | |||
| f5d68d4982 | |||
| 2844132ea0 | |||
| e5889ca903 | |||
| 3b61f54509 | |||
| 8a34a122d0 | |||
| 795c79dd5f | |||
| fbe0c19d22 | |||
| a8556aacb7 | |||
| f8914ab78f | |||
| 8f3ee2f831 | |||
| ea7de13d28 | |||
| ddf0ca62c4 | |||
| e9d94cfa83 | |||
| 3f5d607e83 | |||
| d575c9c792 | |||
| 9a6f8dc7f6 | |||
| 7b0fb9332e | |||
| cdfbbb412c | |||
| 1138fe14d0 | |||
| 4bd9ef6acf | |||
| 4824e4e848 | |||
| f8cdd3abdd | |||
| 8b26a91ce9 | |||
| 64bc739496 | |||
| e33db73d65 | |||
| 1bb808da3b | |||
| 50a46e2ca7 | |||
| 40b49c83ae | |||
| 525b4a8474 | |||
| e9aab942c6 | |||
| e082e8ce75 | |||
| a106fe658f | |||
| 1347afb7d1 | |||
| 27bd00e773 |
65
.gemini/hooks/telegram-notify.ts
Executable file
65
.gemini/hooks/telegram-notify.ts
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bun
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
// Fungsi untuk mencari string terpanjang dalam objek (biasanya balasan AI)
|
||||
function findLongestString(obj: any): string {
|
||||
let longest = "";
|
||||
const search = (item: any) => {
|
||||
if (typeof item === "string") {
|
||||
if (item.length > longest.length) longest = item;
|
||||
} else if (Array.isArray(item)) {
|
||||
item.forEach(search);
|
||||
} else if (item && typeof item === "object") {
|
||||
Object.values(item).forEach(search);
|
||||
}
|
||||
};
|
||||
search(obj);
|
||||
return longest;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const inputRaw = readFileSync(0, "utf-8");
|
||||
if (!inputRaw) return;
|
||||
const input = JSON.parse(inputRaw);
|
||||
|
||||
// DEBUG: Lihat struktur asli di console terminal (stderr)
|
||||
console.error("DEBUG KEYS:", Object.keys(input));
|
||||
|
||||
const BOT_TOKEN = process.env.BOT_TOKEN;
|
||||
const CHAT_ID = process.env.CHAT_ID;
|
||||
|
||||
const sessionId = input.session_id || "unknown";
|
||||
|
||||
// Cari teks secara otomatis di seluruh objek JSON
|
||||
let finalText = findLongestString(input.response || input);
|
||||
|
||||
if (!finalText || finalText.length < 5) {
|
||||
finalText =
|
||||
"Teks masih gagal diekstraksi. Struktur: " +
|
||||
Object.keys(input).join(", ");
|
||||
}
|
||||
|
||||
const message =
|
||||
`✅ *Gemini Task Selesai*\n\n` +
|
||||
`🆔 Session: \`${sessionId}\` \n\n` +
|
||||
`🧠 Output:\n${finalText.substring(0, 3500)}`;
|
||||
|
||||
await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: CHAT_ID,
|
||||
text: message,
|
||||
parse_mode: "Markdown",
|
||||
}),
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||
} catch (err) {
|
||||
console.error("Hook Error:", err);
|
||||
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
17
.gemini/settings.json
Normal file
17
.gemini/settings.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"hooks": {
|
||||
"AfterAgent": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "telegram-notify",
|
||||
"type": "command",
|
||||
"command": "bun $GEMINI_PROJECT_DIR/.gemini/hooks/telegram-notify.ts",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -41,6 +41,9 @@ next-env.d.ts
|
||||
# uploads
|
||||
/uploads
|
||||
|
||||
# download
|
||||
/download
|
||||
|
||||
# cache
|
||||
/cache
|
||||
|
||||
@@ -48,3 +51,5 @@ next-env.d.ts
|
||||
|
||||
.env.*
|
||||
|
||||
*.tar.gz
|
||||
|
||||
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
||||
167
AGENTS.md
Normal file
167
AGENTS.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file contains essential information for agentic coding agents working in the desa-darmasaba repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Desa Darmasaba is a Next.js 15 application for village management services in Badung, Bali. It uses:
|
||||
- **Framework**: Next.js 15 with App Router
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Styling**: Mantine UI components with custom CSS
|
||||
- **Backend**: Elysia.js API server integrated with Next.js
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **State Management**: Jotai for global state
|
||||
- **Authentication**: JWT with iron-session
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Start production server
|
||||
npm start
|
||||
|
||||
# Database seeding
|
||||
bun run prisma/seed.ts
|
||||
|
||||
# Linting (ESLint)
|
||||
npx eslint .
|
||||
|
||||
# Type checking
|
||||
npx tsc --noEmit
|
||||
|
||||
# Prisma operations
|
||||
npx prisma generate
|
||||
npx prisma db push
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
Currently no test framework is configured. When adding tests:
|
||||
- Set up test scripts in package.json
|
||||
- Consider Jest or Vitest for unit testing
|
||||
- Use Playwright for E2E testing
|
||||
- Update this section with specific test commands
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Imports
|
||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||
- Group imports: external libraries first, then internal modules
|
||||
- Keep import statements organized and remove unused imports
|
||||
|
||||
```typescript
|
||||
// External libraries
|
||||
import { useState } from 'react'
|
||||
import { Button, Stack } from '@mantine/core'
|
||||
|
||||
// Internal modules
|
||||
import ApiFetch from '@/lib/api-fetch'
|
||||
import { MyComponent } from '@/components/my-component'
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- Strict mode enabled (`"strict": true`)
|
||||
- Target: ES2017
|
||||
- Module resolution: bundler
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
|
||||
### Naming Conventions
|
||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
||||
- **Variables/Functions**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE
|
||||
- **Database Models**: PascalCase (Prisma convention)
|
||||
|
||||
### Error Handling
|
||||
- Use try-catch blocks for async operations
|
||||
- Implement proper error boundaries in React components
|
||||
- Log errors appropriately without exposing sensitive data
|
||||
- Use Zod for runtime validation and type safety
|
||||
|
||||
### API Structure
|
||||
- Backend uses Elysia.js with TypeScript
|
||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
||||
- Use treaty client for type-safe API calls
|
||||
- Follow RESTful conventions for endpoints
|
||||
- Include proper HTTP status codes and error responses
|
||||
|
||||
### Database Operations
|
||||
- Use Prisma client from `@/lib/prisma.ts`
|
||||
- Database connection includes graceful shutdown handling
|
||||
- Use transactions for complex operations
|
||||
- Implement proper error handling for database queries
|
||||
|
||||
### Component Guidelines
|
||||
- Use functional components with hooks
|
||||
- Implement proper prop types with TypeScript interfaces
|
||||
- Use Mantine components for UI consistency
|
||||
- Follow atomic design principles when possible
|
||||
- Add loading states and error states for async operations
|
||||
|
||||
### State Management
|
||||
- Use Jotai atoms for global state
|
||||
- Keep local state in components when possible
|
||||
- Use React Query (SWR) for server state caching
|
||||
- Implement optimistic updates for better UX
|
||||
|
||||
### Styling
|
||||
- Primary: Mantine UI components
|
||||
- Use Mantine theme system for customization
|
||||
- Custom CSS should be minimal and scoped
|
||||
- Follow responsive design principles
|
||||
- Use semantic HTML5 elements
|
||||
|
||||
### Environment Variables
|
||||
- Use `.env.local` for development
|
||||
- Prefix public variables with `NEXT_PUBLIC_`
|
||||
- Never commit environment files to version control
|
||||
- Use proper typing for environment variables
|
||||
|
||||
### File Organization
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js app router pages
|
||||
├── components/ # Reusable React components
|
||||
├── lib/ # Utility functions and configurations
|
||||
├── state/ # Jotai atoms and state management
|
||||
├── types/ # TypeScript type definitions
|
||||
└── con/ # Constants and static data
|
||||
```
|
||||
|
||||
### Security Practices
|
||||
- Validate all user inputs with Zod schemas
|
||||
- Use JWT tokens for authentication
|
||||
- Implement proper CORS configuration
|
||||
- Never expose database credentials or API keys
|
||||
- Use HTTPS in production
|
||||
- Implement rate limiting for sensitive endpoints
|
||||
|
||||
### Performance Considerations
|
||||
- Use Next.js Image optimization
|
||||
- Implement proper caching strategies
|
||||
- Use React.memo for expensive components
|
||||
- Optimize bundle size with dynamic imports
|
||||
- Use Prisma query optimization
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Always run type checking before committing: `npx tsc --noEmit`
|
||||
2. Run linting to catch style issues: `npx eslint .`
|
||||
3. Test database changes with `npx prisma db push`
|
||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||
5. Check environment variables are properly configured
|
||||
6. Verify responsive design on different screen sizes
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The application uses a custom Elysia.js server integrated with Next.js API routes
|
||||
- Image uploads are handled through `/api/upl-img-single` endpoint
|
||||
- Database seeding is done with Bun runtime
|
||||
- The app supports Indonesian locale (id_ID) for SEO and content
|
||||
- CORS is configured to allow cross-origin requests during development
|
||||
62
GEMINI.md
Normal file
62
GEMINI.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Project: Desa Darmasaba
|
||||
|
||||
## Project Overview
|
||||
|
||||
The `desa-darmasaba` project is a Next.js (version 15+) application developed with TypeScript. It serves as an official platform for Desa Darmasaba (a village in Badung, Bali), offering various public services, news, and detailed village profiles.
|
||||
|
||||
**Key Technologies:**
|
||||
|
||||
* **Frontend Framework:** Next.js (v15+) with React (v19+)
|
||||
* **Language:** TypeScript
|
||||
* **UI Library:** Mantine UI
|
||||
* **Database ORM:** Prisma (v6+)
|
||||
* **Database:** PostgreSQL (as configured in `prisma/schema.prisma`)
|
||||
* **API Framework:** Elysia (used for API routes, as seen in dependencies)
|
||||
* **State Management:** Potentially Jotai and Valtio (listed in dependencies)
|
||||
* **Image Processing:** Sharp
|
||||
* **Package Manager:** Likely Bun, given `bun.lockb` and the `prisma:seed` script.
|
||||
|
||||
The application architecture follows the Next.js App Router structure, with comprehensive data models defined in `prisma/schema.prisma` covering various domains like public information, health, security, economy, innovation, environment, and education. It also includes configurations for image handling and caching.
|
||||
|
||||
## Building and Running
|
||||
|
||||
This project uses `bun` as the package manager. Ensure Bun is installed to run these commands.
|
||||
|
||||
* **Install Dependencies:**
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
* **Development Server:**
|
||||
Runs the Next.js development server.
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
* **Build for Production:**
|
||||
Builds the Next.js application for production deployment.
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
* **Start Production Server:**
|
||||
Starts the Next.js application in production mode.
|
||||
```bash
|
||||
bun run start
|
||||
```
|
||||
|
||||
* **Database Seeding:**
|
||||
Executes the Prisma seeding script to populate the database.
|
||||
```bash
|
||||
bun run prisma:seed
|
||||
```
|
||||
|
||||
## Development Conventions
|
||||
|
||||
* **Coding Language:** TypeScript is strictly enforced.
|
||||
* **Frontend Framework:** Next.js App Router for page and component structuring.
|
||||
* **UI/UX:** Adherence to Mantine UI component library for consistent styling and user experience.
|
||||
* **Database Interaction:** Prisma ORM is used for all database operations, with a PostgreSQL database.
|
||||
* **Linting:** ESLint is configured with `next/core-web-vitals` and `next/typescript` to maintain code quality and adherence to Next.js and TypeScript best practices.
|
||||
* **Styling:** PostCSS is used, with `postcss-preset-mantine` and `postcss-simple-vars` defining Mantine-specific breakpoints and other CSS variables.
|
||||
* **Imports:** Absolute imports are configured using `@/*` which resolves to the `src/` directory.
|
||||
763
QC/Landing-Page/QC-APBDES-MODULE.md
Normal file
763
QC/Landing-Page/QC-APBDES-MODULE.md
Normal file
@@ -0,0 +1,763 @@
|
||||
# QC Summary - APBDes Module
|
||||
|
||||
**Scope:** List APBDes, Create, Edit, Detail
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa critical issues yang perlu diperbaiki
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Aspect | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| APBDes | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK
|
||||
|
||||
### **1. UI/UX Consistency**
|
||||
- ✅ Responsive design (desktop table + mobile cards)
|
||||
- ✅ Loading states dengan Skeleton
|
||||
- ✅ Search dengan debounce (1000ms)
|
||||
- ✅ Pagination konsisten
|
||||
- ✅ Empty state handling yang informatif
|
||||
- ✅ Modal konfirmasi hapus
|
||||
|
||||
### **2. File Upload Handling**
|
||||
- ✅ Dual upload: Gambar + Dokumen
|
||||
- ✅ Dropzone dengan preview (image + iframe untuk dokumen)
|
||||
- ✅ Validasi format (gambar: JPEG/PNG/WEBP, dokumen: PDF/DOC/DOCX)
|
||||
- ✅ Validasi ukuran file (max 5MB untuk gambar, 10MB untuk dokumen di edit)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk validasi typed
|
||||
- ✅ isFormValid() check sebelum submit
|
||||
- ✅ Error toast dengan pesan spesifik
|
||||
- ✅ Button disabled saat invalid/loading
|
||||
- ✅ Type number input untuk tahun
|
||||
|
||||
### **4. Complex Feature - APBDes Items**
|
||||
- ✅ Hierarchical items dengan level (1, 2, 3)
|
||||
- ✅ Tipe classification (pendapatan, belanja, pembiayaan)
|
||||
- ✅ Auto-calculation: selisih & persentase
|
||||
- ✅ Add/remove items dynamic
|
||||
- ✅ Table preview dengan badge color coding
|
||||
- ✅ Indentasi visual berdasarkan level
|
||||
|
||||
### **5. Edit Form - Original Data Tracking**
|
||||
- ✅ Original data state untuk reset form
|
||||
- ✅ Load data existing dengan benar
|
||||
- ✅ Preview image & dokumen dari data lama
|
||||
- ✅ Reset form mengembalikan ke data original
|
||||
- ✅ File replacement logic (upload baru jika ada perubahan)
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// Line ~95-130 - Load data & save original
|
||||
const data = await apbdesState.edit.load(id);
|
||||
|
||||
setOriginalData({
|
||||
tahun: data.tahun || new Date().getFullYear(),
|
||||
imageId: data.imageId || '',
|
||||
fileId: data.fileId || '',
|
||||
imageUrl: data.image?.link || '',
|
||||
fileUrl: data.file?.link || '',
|
||||
});
|
||||
|
||||
// Set form dengan data lama (termasuk imageId dan fileId)
|
||||
apbdesState.edit.form = {
|
||||
tahun: data.tahun || new Date().getFullYear(),
|
||||
imageId: data.imageId || '', // ✅ Preserve old ID
|
||||
fileId: data.fileId || '', // ✅ Preserve old ID
|
||||
items: (data.items || []).map(...),
|
||||
};
|
||||
|
||||
// Line ~270 - Handle reset
|
||||
const handleReset = () => {
|
||||
apbdesState.edit.form = {
|
||||
tahun: originalData.tahun,
|
||||
imageId: originalData.imageId, // ✅ Restore old ID
|
||||
fileId: originalData.fileId, // ✅ Restore old ID
|
||||
items: [...apbdesState.edit.form.items],
|
||||
};
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setPreviewDoc(originalData.fileUrl || null);
|
||||
setImageFile(null);
|
||||
setDocFile(null);
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
|
||||
|
||||
---
|
||||
|
||||
### **6. Schema Design**
|
||||
- ✅ Proper relations: APBDes ↔ FileStorage (image & file)
|
||||
- ✅ Self-relation untuk hierarchical items (parentId → children)
|
||||
- ✅ Indexing untuk performa (kode, level, apbdesId)
|
||||
- ✅ Soft delete support (deletedAt, isActive)
|
||||
- ✅ Nullable deletedAt yang benar (`DateTime? @default(null)`)
|
||||
|
||||
**Schema Example (✅ GOOD):**
|
||||
```prisma
|
||||
model APBDes {
|
||||
id String @id @default(cuid())
|
||||
tahun Int?
|
||||
name String?
|
||||
deskripsi String?
|
||||
jumlah String?
|
||||
items APBDesItem[]
|
||||
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
||||
imageId String?
|
||||
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
||||
fileId String?
|
||||
deletedAt DateTime? // ✅ Nullable, no default
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model APBDesItem {
|
||||
id String @id @default(cuid())
|
||||
kode String
|
||||
uraian String
|
||||
anggaran Float
|
||||
realisasi Float
|
||||
selisih Float // ✅ Formula di komentar
|
||||
persentase Float
|
||||
tipe String? // ✅ Nullable untuk level 1
|
||||
level Int
|
||||
parentId String?
|
||||
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
||||
children APBDesItem[] @relation("APBDesItemParent")
|
||||
apbdesId String
|
||||
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
||||
|
||||
@@index([kode])
|
||||
@@index([level])
|
||||
@@index([apbdesId])
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Schema design sudah solid.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Formula Selisih - SALAH di State, BENAR di Schema/API**
|
||||
|
||||
**Lokasi:**
|
||||
- `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts` (line 36)
|
||||
- Schema komentar di `prisma/schema.prisma` (line 210)
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// ❌ SALAH di state (line 36)
|
||||
function normalizeItem(item: Partial<...>): z.infer<typeof ApbdesItemSchema> {
|
||||
const anggaran = item.anggaran ?? 0;
|
||||
const realisasi = item.realisasi ?? 0;
|
||||
|
||||
// ❌ WRONG FORMULA
|
||||
const selisih = anggaran - realisasi; // positif = sisa anggaran
|
||||
|
||||
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
|
||||
|
||||
return { ... };
|
||||
}
|
||||
```
|
||||
|
||||
```prisma
|
||||
// ✅ BENAR di schema komentar (line 210)
|
||||
model APBDesItem {
|
||||
// ...
|
||||
realisasi Float
|
||||
selisih Float // ✅ realisasi - anggaran (komentar benar)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- **Data salah!** Selisih positif/negatif terbalik
|
||||
- Jika realisasi > anggaran (over budget), seharusnya **negatif** tapi jadi **positif**
|
||||
- Jika realisasi < anggaran (under budget/sisa), seharusnya **positif** tapi jadi **negatif**
|
||||
- Color coding di UI (green/red) juga terbalik!
|
||||
|
||||
**Contoh:**
|
||||
```
|
||||
Anggaran: Rp 100.000.000
|
||||
Realisasi: Rp 120.000.000 (over budget!)
|
||||
|
||||
❌ Formula sekarang: selisih = 100M - 120M = -20M (negatif)
|
||||
UI show: merah (over budget) ✅ TAPI karena negatif
|
||||
|
||||
✅ Seharusnya: selisih = 120M - 100M = +20M (positif)
|
||||
UI show: merah (over budget) ✅ Karena positif
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix formula di state:
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT FORMULA
|
||||
const selisih = realisasi - anggaran; // positif = over budget, negatif = under budget
|
||||
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
|
||||
```
|
||||
|
||||
**Priority:** 🔴 **CRITICAL**
|
||||
**Effort:** Low (1 line fix)
|
||||
**Impact:** **HIGH** (data integrity issue)
|
||||
|
||||
---
|
||||
|
||||
#### **2. State Management - Inconsistency Fetch Pattern**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
|
||||
|
||||
**Masalah:** Ada 3 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany, delete, edit.load, edit.update)
|
||||
const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
|
||||
const res = await ApiFetch.api.landingpage.apbdes["findMany"].get({ query });
|
||||
const res = await (ApiFetch.api.landingpage.apbdes as any)["del"][id].delete();
|
||||
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
|
||||
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique)
|
||||
const response = await fetch(`/api/landingpage/apbdes/${id}`);
|
||||
const res = await response.json();
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
- Console.log debugging tertinggal di production
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
async load(id: string) {
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.apbdes[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
this.data = res.data.data;
|
||||
} else {
|
||||
this.data = null;
|
||||
this.error = res.data?.message || "Gagal memuat detail APBDes";
|
||||
toast.error(this.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("FindUnique error:", error);
|
||||
this.data = null;
|
||||
this.error = "Gagal memuat detail APBDes";
|
||||
toast.error(this.error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di findUnique)
|
||||
|
||||
---
|
||||
|
||||
#### **3. Console.log Debugging di Production**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~175-177
|
||||
const url = `/api/landingpage/apbdes/${id}`;
|
||||
console.log("🌐 Fetching:", url); // ❌ Debug log
|
||||
|
||||
const response = await fetch(url);
|
||||
const res = await response.json();
|
||||
|
||||
console.log("📦 Response:", res); // ❌ Debug log
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Performance impact (I/O operation)
|
||||
- Security risk (expose API structure)
|
||||
- Log pollution di production
|
||||
- Unprofessional
|
||||
|
||||
**Rekomendasi:** Remove atau gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
// ✅ Remove completely (recommended)
|
||||
// Atau gunakan conditional logging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log("🌐 Fetching:", url);
|
||||
console.log("📦 Response:", res);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Type Safety - Any Usage di Edit Methods**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~215
|
||||
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
// Line ~245
|
||||
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Type safety hilang
|
||||
- Autocomplete tidak bekerja
|
||||
- Runtime errors tidak terdeteksi di compile time
|
||||
- Refactoring sulit
|
||||
|
||||
**Rekomendasi:** Define typed API client:
|
||||
|
||||
```typescript
|
||||
// Define proper types
|
||||
interface APBDesAPI {
|
||||
[id: string]: {
|
||||
get: () => Promise<ApiResponse<APBDesData>>;
|
||||
put: (data: APBDesForm) => Promise<ApiResponse<APBDesData>>;
|
||||
};
|
||||
del: {
|
||||
[id: string]: {
|
||||
delete: () => Promise<ApiResponse<void>>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Use typed client
|
||||
const res = await ApiFetch.api.landingpage.apbdes[id].get();
|
||||
// No more `as any`
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Medium (perlu setup types)
|
||||
|
||||
---
|
||||
|
||||
#### **5. Edit Form - Items Tidak Di-Restore Saat Reset**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~270-285
|
||||
const handleReset = () => {
|
||||
apbdesState.edit.form = {
|
||||
tahun: originalData.tahun,
|
||||
imageId: originalData.imageId,
|
||||
fileId: originalData.fileId,
|
||||
items: [...apbdesState.edit.form.items], // ⚠️ Keep MODIFIED items
|
||||
};
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
**Issue:** Saat reset, items yang sudah di-modified (added/removed) tidak di-restore ke original. User expect reset = kembali ke data awal sepenuhnya.
|
||||
|
||||
**Rekomendasi:** Save original items dan restore saat reset:
|
||||
|
||||
```typescript
|
||||
// Add to originalData state
|
||||
const [originalData, setOriginalData] = useState({
|
||||
tahun: 0,
|
||||
imageId: '',
|
||||
fileId: '',
|
||||
imageUrl: '',
|
||||
fileUrl: '',
|
||||
items: [] as ItemForm[], // ✅ Save original items
|
||||
});
|
||||
|
||||
// Load data
|
||||
setOriginalData({
|
||||
tahun: data.tahun || new Date().getFullYear(),
|
||||
imageId: data.imageId || '',
|
||||
fileId: data.fileId || '',
|
||||
imageUrl: data.image?.link || '',
|
||||
fileUrl: data.file?.link || '',
|
||||
items: (data.items || []).map((item: any) => ({...})), // ✅ Save
|
||||
});
|
||||
|
||||
// Reset
|
||||
const handleReset = () => {
|
||||
apbdesState.edit.form = {
|
||||
tahun: originalData.tahun,
|
||||
imageId: originalData.imageId,
|
||||
fileId: originalData.fileId,
|
||||
items: [...originalData.items], // ✅ Restore original items
|
||||
};
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **6. Zod Schema - Error Message Tidak Akurat**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~10
|
||||
const ApbdesItemSchema = z.object({
|
||||
kode: z.string().min(1, "Kode wajib diisi"), // ✅ OK
|
||||
uraian: z.string().min(1, "Uraian wajib diisi"), // ✅ OK
|
||||
anggaran: z.number().min(0), // ⚠️ No custom message
|
||||
realisasi: z.number().min(0), // ⚠️ No custom message
|
||||
// ...
|
||||
});
|
||||
|
||||
// Line ~17
|
||||
const ApbdesFormSchema = z.object({
|
||||
tahun: z.number().int().min(2000, "Tahun tidak valid"), // ⚠️ Generic
|
||||
imageId: z.string().min(1, "Gambar wajib diunggah"), // ✅ OK
|
||||
fileId: z.string().min(1, "File wajib diunggah"), // ✅ OK
|
||||
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"), // ✅ OK
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:** Error messages tidak konsisten, beberapa generic beberapa spesifik.
|
||||
|
||||
**Rekomendasi:** Standardisasi error messages:
|
||||
|
||||
```typescript
|
||||
const ApbdesItemSchema = z.object({
|
||||
kode: z.string().min(1, "Kode wajib diisi"),
|
||||
uraian: z.string().min(1, "Uraian wajib diisi"),
|
||||
anggaran: z.number().min(0, "Anggaran tidak boleh negatif"),
|
||||
realisasi: z.number().min(0, "Realisasi tidak boleh negatif"),
|
||||
selisih: z.number(),
|
||||
persentase: z.number(),
|
||||
level: z.number().int().min(1).max(3, "Level harus antara 1-3"),
|
||||
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
|
||||
});
|
||||
|
||||
const ApbdesFormSchema = z.object({
|
||||
tahun: z.number().int().min(2000, "Tahun minimal 2000").max(2100, "Tahun maksimal 2100"),
|
||||
imageId: z.string().min(1, "Gambar wajib diunggah"),
|
||||
fileId: z.string().min(1, "Dokumen wajib diunggah"),
|
||||
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"),
|
||||
});
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Console.log di Production (UI Components)**
|
||||
|
||||
**Lokasi:** Multiple UI files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~220
|
||||
console.error('Update error:', err);
|
||||
|
||||
// create/page.tsx - Line ~120
|
||||
console.error("Gagal submit:", error);
|
||||
|
||||
// detail/page.tsx - Line ~40
|
||||
console.error('Error loading APBDes:', error);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Update error:', err);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **8. Mobile Layout - Title Order Inconsistency**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~170 (Mobile)
|
||||
<Title order={2} size="lg" lh={1.2}>
|
||||
Daftar APBDes
|
||||
</Title>
|
||||
|
||||
// Line ~70 (Desktop - inside Paper)
|
||||
<Title order={4} size="lg" lh={1.2}>
|
||||
Daftar APBDes
|
||||
</Title>
|
||||
```
|
||||
|
||||
**Issue:** Mobile pakai `order={2}` (heading besar), desktop `order={4}`. Seharusnya konsisten.
|
||||
|
||||
**Rekomendasi:** Samakan:
|
||||
```typescript
|
||||
<Title order={4} size="lg" lh={1.2}>
|
||||
Daftar APBDes
|
||||
</Title>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **9. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~30
|
||||
<HeaderSearch
|
||||
title="APBDes"
|
||||
placeholder="Cari APBDes..." // ⚠️ Generic
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Rekomendasi:** Lebih spesifik:
|
||||
```typescript
|
||||
placeholder='Cari nama atau tahun APBDes...'
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Duplicate Comment**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~28-29
|
||||
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
|
||||
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
|
||||
// ^ Duplicate line
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low (remove duplicate)
|
||||
|
||||
---
|
||||
|
||||
#### **11. Inconsistent Button Label**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// create/page.tsx - Line ~270
|
||||
<Button ...>Simpan</Button>
|
||||
|
||||
// edit/page.tsx - Line ~340
|
||||
<Button ...>Simpan Perubahan</Button>
|
||||
|
||||
// Should be consistent: "Simpan" atau "Simpan Perubahan"
|
||||
```
|
||||
|
||||
**Rekomendasi:** Standardisasi:
|
||||
```typescript
|
||||
// Create: "Simpan"
|
||||
// Edit: "Simpan Perubahan" (lebih descriptive untuk edit)
|
||||
// OR both: "Simpan"
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Missing Search Feature in Pagination**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~250
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10); // ⚠️ Missing search parameter
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Issue:** Saat ganti page, search query hilang.
|
||||
|
||||
**Rekomendasi:** Include search:
|
||||
```typescript
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, debouncedSearch); // ✅ Include search
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **13. Edit Page - Document Max Size Inconsistency**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~230 (Image)
|
||||
maxSize={5 * 1024 ** 2} // 5MB
|
||||
|
||||
// Line ~250 (Document)
|
||||
maxSize={10 * 1024 ** 2} // 10MB
|
||||
```
|
||||
|
||||
**Issue:** Create page maksimal 5MB untuk semua file, edit page 10MB untuk dokumen. Inconsistent.
|
||||
|
||||
**Rekomendasi:** Samakan (prefer 5MB untuk consistency):
|
||||
```typescript
|
||||
maxSize={5 * 1024 ** 2} // 5MB for both
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | **Formula selisih SALAH** | State | **CRITICAL** | Low | **MUST FIX** |
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P1 | Console.log debugging in production | State | Medium | Low | Should fix |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Medium | Optional |
|
||||
| 🟡 M | Items tidak di-restore saat reset | Edit UI | Medium | Low | Should fix |
|
||||
| 🟡 M | Zod schema error messages | State | Low | Low | Optional |
|
||||
| 🟢 L | Console.log in UI components | UI | Low | Low | Optional |
|
||||
| 🟢 L | Mobile title order inconsistency | List UI | Low | Low | Optional |
|
||||
| 🟢 L | Search placeholder tidak spesifik | List UI | Low | Low | Optional |
|
||||
| 🟢 L | Duplicate comment | State | Low | Low | Optional |
|
||||
| 🟢 L | Inconsistent button label | UI | Low | Low | Optional |
|
||||
| 🟢 L | Missing search in pagination | List UI | Low | Low | Should fix |
|
||||
| 🟢 L | Document max size inconsistency | Edit UI | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (7/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX konsisten & responsive
|
||||
2. ✅ File upload handling solid (dual upload: image + document)
|
||||
3. ✅ Form validation dengan Zod schema
|
||||
4. ✅ State management terstruktur (Valtio)
|
||||
5. ✅ **Edit form reset sudah benar** (original data tracking untuk files)
|
||||
6. ✅ Complex feature: hierarchical items dengan level & tipe
|
||||
7. ✅ Schema design solid (proper relations, indexing, soft delete)
|
||||
8. ✅ Modal konfirmasi hapus untuk user safety
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **FORMULA SELISIH SALAH** - Data integrity issue (CRITICAL)
|
||||
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
3. ⚠️ Console.log debugging tertinggal di production
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Fix formula selisih** (realisasi - anggaran, bukan anggaran - realisasi)
|
||||
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. ⚠️ **Remove console.log** debugging dari production code
|
||||
4. ⚠️ **Save & restore original items** saat reset form di edit page
|
||||
5. ⚠️ **Improve type safety** dengan remove `as any` usage
|
||||
6. ⚠️ **Standardisasi error messages** di Zod schema
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Fix formula selisih** di state (line 36) - 5 menit fix
|
||||
2. **🔴 HIGH:** Refactor findUnique ke ApiFetch - 30 menit
|
||||
3. **🔴 HIGH:** Remove console.log debugging - 10 menit
|
||||
4. **🟡 MEDIUM:** Save & restore original items - 30 menit
|
||||
5. **🟡 MEDIUM:** Improve type safety - 1-2 jam
|
||||
6. **🟢 LOW:** Polish minor issues - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | APBDes | Notes |
|
||||
|--------|--------|-------------------|-----------|--------|-------|
|
||||
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
|
||||
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | ✅ Good | APBDes paling baik |
|
||||
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | ✅ Good | All consistent |
|
||||
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
|
||||
| File Upload | ✅ Images | ✅ Documents | ✅ Images | ✅ **Dual** | APBDes paling complex |
|
||||
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | ✅ Good | Consistent |
|
||||
| Schema Design | ✅ Good | ⚠️ deletedAt issue | ⚠️ deletedAt issue | ✅ **Best** | APBDes paling solid |
|
||||
| **Data Integrity** | ✅ Good | ✅ Good | ✅ Good | ❌ **Formula WRONG** | **APBDes CRITICAL issue** |
|
||||
| Complexity | Low | Medium | Low | **High** | APBDes items hierarchy |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF APBDes MODULE
|
||||
|
||||
**Most Complex Module So Far:**
|
||||
1. **Dual file upload** (gambar + dokumen) - unique to APBDes
|
||||
2. **Hierarchical items** dengan 3 level - unique to APBDes
|
||||
3. **Auto-calculation** (selisih & persentase) - unique to APBDes
|
||||
4. **Type classification** (pendapatan, belanja, pembiayaan) - unique to APBDes
|
||||
5. **Dynamic item management** (add/remove) - unique to APBDes
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ Schema design paling solid (deletedAt nullable, proper indexing)
|
||||
2. ✅ Edit form reset paling comprehensive (preserve files & items)
|
||||
3. ✅ Validation paling thorough (Zod schema untuk items)
|
||||
|
||||
**Biggest Issue:**
|
||||
1. ❌ **Formula selisih SALAH** - critical data integrity issue yang tidak ada di modul lain
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul APBDes adalah **paling complex dan paling solid** dibanding modul lain yang sudah di-QC. Namun, ada **1 CRITICAL BUG** (formula selisih) yang harus **SEGERA DIPERBAIKI** karena menyangkut integritas data. Setelah fix critical issue, module ini production-ready dengan beberapa improvement minor yang bisa dilakukan secara incremental.
|
||||
|
||||
**Priority Action:**
|
||||
```
|
||||
🔴 FIX INI SEKARANG JUGA (5 MENIT):
|
||||
File: src/app/admin/(dashboard)/_state/landing-page/apbdes.ts
|
||||
Line: 36
|
||||
Change: const selisih = anggaran - realisasi;
|
||||
To: const selisih = realisasi - anggaran;
|
||||
```
|
||||
639
QC/Landing-Page/QC-DESA-ANTI-KORUPSI.md
Normal file
639
QC/Landing-Page/QC-DESA-ANTI-KORUPSI.md
Normal file
@@ -0,0 +1,639 @@
|
||||
# QC Summary - Desa Anti Korupsi Module
|
||||
|
||||
**Scope:** List Desa Anti Korupsi, Kategori Desa Anti Korupsi
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Module | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| List Desa Anti Korupsi | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
|
||||
| Kategori Desa Anti Korupsi | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK (COMMON)
|
||||
|
||||
### **1. UI/UX Consistency**
|
||||
- ✅ Responsive design (desktop table + mobile cards)
|
||||
- ✅ Loading states dengan Skeleton
|
||||
- ✅ Search dengan debounce (1000ms)
|
||||
- ✅ Pagination konsisten
|
||||
- ✅ Empty state handling yang informatif
|
||||
- ✅ Modal konfirmasi hapus
|
||||
|
||||
### **2. File Upload Handling** (Desa Anti Korupsi)
|
||||
- ✅ Dropzone dengan preview iframe untuk dokumen
|
||||
- ✅ Validasi format dokumen (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX)
|
||||
- ✅ Validasi ukuran file (max 5MB)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk validasi typed
|
||||
- ✅ isFormValid() check sebelum submit
|
||||
- ✅ Error toast dengan pesan spesifik
|
||||
- ✅ Button disabled saat invalid/loading
|
||||
|
||||
### **4. CRUD Operations**
|
||||
- ✅ Create dengan upload file
|
||||
- ✅ FindMany dengan pagination & search
|
||||
- ✅ FindUnique untuk detail
|
||||
- ✅ Delete dengan soft delete
|
||||
- ✅ Update dengan file replacement
|
||||
|
||||
### **5. Error Handling**
|
||||
- ✅ Try-catch di semua async operation
|
||||
- ✅ Toast error dengan pesan user-friendly
|
||||
- ✅ Console.error untuk debugging
|
||||
- ✅ Response cloning untuk error handling yang lebih baik (di kategori update)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Edit Form - File Lama Tidak Tersimpan Saat Reset**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/desa-anti-korupsi/list-desa-anti-korupsi/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~70 - Load data
|
||||
const data = await desaAntiKorupsiState.edit.load(id);
|
||||
|
||||
setFormData({
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
fileId: data.fileId, // ✅ Sudah benar
|
||||
});
|
||||
|
||||
setOriginalData({
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
fileId: data.fileId,
|
||||
fileUrl: data.file?.link || "", // ✅ Sudah benar
|
||||
});
|
||||
|
||||
// Line ~130 - Handle reset
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
deskripsi: originalData.deskripsi,
|
||||
kategoriId: originalData.kategoriId,
|
||||
fileId: originalData.fileId, // ✅ Sudah benar
|
||||
});
|
||||
setPreviewFile(originalData.fileUrl || null); // ✅ Sudah benar
|
||||
setFile(null); // ✅ Sudah benar
|
||||
};
|
||||
```
|
||||
|
||||
**Status:** ✅ **SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
|
||||
|
||||
**Verdict:** Tidak ada action needed.
|
||||
|
||||
---
|
||||
|
||||
#### **2. State Management - Inconsistency Fetch Pattern**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create operations)
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi["create"].post({...});
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
|
||||
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
|
||||
const response = await fetch(`/api/landingpage/desaantikorupsi/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi["create"].post(data);
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi[id].get();
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi[id].put(data);
|
||||
const res = await ApiFetch.api.landingpage.desaantikorupsi["del"][id].delete();
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di semua state methods)
|
||||
|
||||
---
|
||||
|
||||
#### **3. findUnique State - Tidak Ada Loading State Management**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~97 - desaAntikorupsi.findUnique.load()
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
desaAntikorupsi.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
desaAntikorupsi.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
desaAntikorupsi.findUnique.data = null;
|
||||
}
|
||||
// ❌ MISSING: finally block untuk stop loading
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** UI mungkin stuck di loading state jika ada error.
|
||||
|
||||
**Rekomendasi:** Tambahkan loading state dan finally block:
|
||||
|
||||
```typescript
|
||||
async load(id: string) {
|
||||
try {
|
||||
desaAntikorupsi.findUnique.loading = true; // ✅ Start loading
|
||||
const res = await fetch(`/api/landingpage/desaantikorupsi/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
desaAntikorupsi.findUnique.data = data.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
desaAntikorupsi.findUnique.loading = false; // ✅ Stop loading
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **4. Kategori Edit - Response Cloning Overkill**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~370 - kategoriDesaAntiKorupsi.edit.update()
|
||||
async update() {
|
||||
// ...
|
||||
const response = await fetch(...);
|
||||
|
||||
// Clone the response to avoid 'body already read' error
|
||||
const responseClone = response.clone();
|
||||
|
||||
try {
|
||||
const result = await response.json();
|
||||
// ...
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, try to get the response text
|
||||
try {
|
||||
const text = await responseClone.text();
|
||||
console.error("Error response text:", text);
|
||||
throw new Error(`Gagal memproses respons dari server: ${text}`);
|
||||
} catch (textError) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Analysis:**
|
||||
- ✅ **GOOD:** Error handling sangat thorough
|
||||
- ⚠️ **OVERKILL:** Untuk production API yang stable, ini berlebihan
|
||||
- ⚠️ **INCONSISTENT:** Module lain tidak punya error handling se-detail ini
|
||||
|
||||
**Rekomendasi:** Simplify untuk consistency:
|
||||
|
||||
```typescript
|
||||
async update() {
|
||||
try {
|
||||
kategoriDesaAntiKorupsi.edit.loading = true;
|
||||
|
||||
const response = await fetch(`/api/landingpage/kategoridak/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: this.form.name }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result?.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message || "Berhasil update");
|
||||
await kategoriDesaAntiKorupsi.findMany.load();
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error(result.message || "Gagal update");
|
||||
} catch (error) {
|
||||
console.error("Error updating:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Gagal update");
|
||||
return false;
|
||||
} finally {
|
||||
kategoriDesaAntiKorupsi.edit.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **5. HTML Injection Risk - dangerouslySetInnerHTML**
|
||||
|
||||
**Lokasi:**
|
||||
- `list-desa-anti-korupsi/[id]/page.tsx` (line ~105)
|
||||
- `list-desa-anti-korupsi/create/page.tsx` (CreateEditor component)
|
||||
- `list-desa-anti-korupsi/[id]/edit/page.tsx` (EditEditor component)
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// ❌ Direct HTML render tanpa sanitization
|
||||
<Box
|
||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal", lineHeight: 1.6 }}
|
||||
/>
|
||||
```
|
||||
|
||||
**Risk:**
|
||||
- XSS attack jika admin input script malicious
|
||||
- Bisa inject iframe, script tag, dll
|
||||
- Security vulnerability
|
||||
|
||||
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Sanitize sebelum render
|
||||
const sanitizedHtml = DOMPurify.sanitize(data.deskripsi);
|
||||
<Box
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
|
||||
|
||||
**Priority:** 🟡 Medium (**Security concern**)
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **6. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~60
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~280
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~97
|
||||
data: null as Prisma.DesaAntiKorupsiGetPayload<{...}> | null, // ✅ Typed
|
||||
|
||||
// Line ~310
|
||||
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{...}> | null, // ✅ Typed
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed data consistently:
|
||||
|
||||
```typescript
|
||||
// desaAntikorupsi.findMany
|
||||
data: null as Prisma.DesaAntiKorupsiGetPayload<{
|
||||
include: { kategori: true; file: true };
|
||||
}>[] | null,
|
||||
|
||||
// kategoriDesaAntiKorupsi.findMany
|
||||
data: null as Prisma.KategoriDesaAntiKorupsiGetPayload<{}>[] | null,
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Medium (perlu update semua reference)
|
||||
|
||||
---
|
||||
|
||||
#### **7. Console.log di Production**
|
||||
|
||||
**Lokasi:** Multiple places di state file
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~50
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// Line ~85
|
||||
console.error("Failed to load media sosial:", res.data?.message);
|
||||
|
||||
// Line ~91
|
||||
console.error("Error loading media sosial:", error);
|
||||
|
||||
// Line ~110
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
|
||||
// Line ~114
|
||||
console.error("Error fetching data:", error);
|
||||
|
||||
// ... dan banyak lagi
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
Atau gunakan logging library (winston, pino, dll) dengan levels yang jelas.
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **8. Error Message Tidak Konsisten**
|
||||
|
||||
**Lokasi:** Multiple places
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Create - Line ~40
|
||||
return toast.error("Gagal menambahkan data");
|
||||
|
||||
// Create - Line ~42
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// Delete - Line ~140
|
||||
toast.error("Terjadi kesalahan saat menghapus desa anti korupsi");
|
||||
|
||||
// Edit - Line ~190
|
||||
toast.error("Gagal memuat data");
|
||||
|
||||
// Edit update - Line ~240
|
||||
toast.error("Gagal mengupdate desa anti korupsi");
|
||||
```
|
||||
|
||||
**Rekomendasi:** Standardisasi error messages:
|
||||
|
||||
```typescript
|
||||
// Pattern: "[Action] [resource] gagal"
|
||||
toast.error("Menambahkan data gagal");
|
||||
toast.error("Menghapus data gagal");
|
||||
toast.error("Memuat data gagal");
|
||||
toast.error("Memperbarui data gagal");
|
||||
|
||||
// Atau lebih spesifik dengan context
|
||||
toast.error("Gagal menambahkan data Desa Anti Korupsi");
|
||||
toast.error("Gagal menghapus Kategori Desa Anti Korupsi");
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **9. Placeholder Search Tidak Spesifik**
|
||||
|
||||
**Lokasi:**
|
||||
- `list-desa-anti-korupsi/page.tsx`: `placeholder="Cari nama program atau kategori..."` ✅ Spesifik
|
||||
- `kategori-desa-anti-korupsi/page.tsx`: `placeholder='pencarian'` ❌ Terlalu generic
|
||||
|
||||
**Rekomendasi:**
|
||||
```typescript
|
||||
// Kategori page
|
||||
placeholder="Cari nama kategori..."
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Alert vs Toast**
|
||||
|
||||
**Lokasi:** `kategori-desa-anti-korupsi/create/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~37
|
||||
if (!stateKategori.create.form.name) {
|
||||
return alert('Nama kategori harus diisi'); // ❌ Using alert()
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan toast untuk consistency:
|
||||
```typescript
|
||||
if (!stateKategori.create.form.name) {
|
||||
return toast.warn('Nama kategori harus diisi'); // ✅ Using toast
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Component Name Mismatch**
|
||||
|
||||
**Lokasi:** `list-desa-anti-korupsi/[id]/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~17
|
||||
export default function DetailKegiatanDesa() { // ❌ Wrong name
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Rename ke yang sesuai:
|
||||
```typescript
|
||||
export default function DetailDesaAntiKorupsi() { // ✅ Correct name
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low (hanya rename)
|
||||
|
||||
---
|
||||
|
||||
#### **12. Duplicate Error Logging**
|
||||
|
||||
**Lokasi:** `list-desa-anti-korupsi/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~87
|
||||
} catch (err) {
|
||||
console.error(err); // ❌ Duplicate logging
|
||||
toast.error('Gagal memuat data Desa Anti Korupsi');
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Cukup satu logging yang informatif:
|
||||
```typescript
|
||||
} catch (err) {
|
||||
console.error('Failed to load Desa Anti Korupsi:', err);
|
||||
toast.error('Gagal memuat data Desa Anti Korupsi');
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **13. Comment Typo**
|
||||
|
||||
**Lokasi:** `kategori-desa-anti-korupsi/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~20
|
||||
// 🧠 Ambil proxy asli (bisa ditulis) & snapshot (buat render)
|
||||
const stateKategori = korupsiState.kategoriDesaAntiKorupsi;
|
||||
const snapshotKategori = useProxy(stateKategori);
|
||||
|
||||
// ❌ snapshotKategori declared but never used
|
||||
```
|
||||
|
||||
**Rekomendasi:** Remove unused variable:
|
||||
```typescript
|
||||
const stateKategori = korupsiState.kategoriDesaAntiKorupsi;
|
||||
// const snapshotKategori = useProxy(stateKategori); // ❌ Remove
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **14. Schema - deletedAt Default Value**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma`
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model DesaAntiKorupsi {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ Always has default value
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
**Issue:** `deletedAt @default(now())` berarti setiap record baru langsung punya `deletedAt` value, yang bisa membingungkan untuk soft delete logic.
|
||||
|
||||
**Rekomendasi:**
|
||||
```prisma
|
||||
model DesaAntiKorupsi {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Medium (potential logic issue)
|
||||
**Effort:** Medium (perlu migration)
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P0 | Missing loading state in findUnique | State | Medium | Low | Perlu fix |
|
||||
| 🟡 M | HTML injection risk | UI | **High (Security)** | Low | **Should fix** |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Medium | Optional |
|
||||
| 🟡 M | Response cloning overkill | State (Kategori) | Low | Low | Optional |
|
||||
| 🟢 L | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟢 L | Error message inconsistency | State | Low | Low | Optional |
|
||||
| 🟢 L | Placeholder tidak spesifik | Kategori UI | Low | Low | Optional |
|
||||
| 🟢 L | Alert vs Toast | Kategori Create | Low | Low | Optional |
|
||||
| 🟢 L | Component name mismatch | Detail page | Low | Low | Optional |
|
||||
| 🟢 L | Duplicate error logging | Edit page | Low | Low | Optional |
|
||||
| 🟢 L | Unused variable | Kategori Edit | Low | Low | Optional |
|
||||
| 🟢 M | deletedAt default value | Schema | Medium | Medium | Should fix |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (7.5/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX konsisten & responsive
|
||||
2. ✅ File upload handling solid (iframe preview untuk dokumen)
|
||||
3. ✅ Form validation dengan Zod schema
|
||||
4. ✅ State management terstruktur (Valtio)
|
||||
5. ✅ Error handling comprehensive (terutama di kategori update)
|
||||
6. ✅ **Edit form reset sudah benar** (original data tracking)
|
||||
7. ✅ Modal konfirmasi hapus untuk user safety
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Security:** HTML injection di deskripsi (prioritas)
|
||||
2. ⚠️ **Consistency:** Fetch method pattern (ApiFetch vs fetch manual)
|
||||
3. ⚠️ **Loading States:** findUnique tidak ada loading state management
|
||||
4. ⚠️ **Type Safety:** Reduce `any` usage, gunakan Prisma types
|
||||
5. ⚠️ **Schema:** deletedAt default value bisa menyebabkan logic issue
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **Fix HTML injection** dengan DOMPurify atau backend validation
|
||||
2. **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. **Add loading state** di findUnique operations
|
||||
4. **Fix deletedAt schema** untuk soft delete yang benar
|
||||
5. **Optional:** Improve type safety dengan remove `any`
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Aspect | Profil Module | Desa Anti Korupsi | Notes |
|
||||
|--------|--------------|-------------------|-------|
|
||||
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | Both perlu refactor |
|
||||
| Loading State | ⚠️ Some missing | ⚠️ Some missing | Same issue |
|
||||
| Edit Form Reset | ✅ Good | ✅ Good | Consistent |
|
||||
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
|
||||
| HTML Injection | ⚠️ Present | ⚠️ Present | Both need fix |
|
||||
| File Upload | ✅ Images | ✅ Documents | Different use case |
|
||||
| Error Handling | ✅ Good | ✅ Good (better) | DAK more thorough |
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul Desa Anti Korupsi sudah **production-ready** dengan beberapa improvements yang bisa dilakukan secara incremental. Module ini memiliki error handling yang lebih thorough dibanding module Profil, terutama di kategori update operation.
|
||||
875
QC/Landing-Page/QC-PRESTASI-DESA-MODULE.md
Normal file
875
QC/Landing-Page/QC-PRESTASI-DESA-MODULE.md
Normal file
@@ -0,0 +1,875 @@
|
||||
# QC Summary - Prestasi Desa Module
|
||||
|
||||
**Scope:** List Prestasi Desa, Kategori Prestasi Desa, Create, Edit, Detail
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Aspect | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| Prestasi Desa | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
|
||||
| Kategori Prestasi | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK
|
||||
|
||||
### **1. UI/UX Consistency**
|
||||
- ✅ Responsive design (desktop table + mobile cards)
|
||||
- ✅ Loading states dengan Skeleton
|
||||
- ✅ Search dengan debounce (1000ms)
|
||||
- ✅ Pagination konsisten
|
||||
- ✅ Empty state handling yang informatif
|
||||
- ✅ Modal konfirmasi hapus
|
||||
|
||||
### **2. File Upload Handling**
|
||||
- ✅ Dropzone dengan preview image
|
||||
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
|
||||
- ✅ Validasi ukuran file (max 5MB)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
- ✅ Preview dengan max height yang proper
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk validasi typed
|
||||
- ✅ isFormValid() check sebelum submit
|
||||
- ✅ Error toast dengan pesan spesifik
|
||||
- ✅ Button disabled saat invalid/loading
|
||||
|
||||
### **4. CRUD Operations**
|
||||
- ✅ Create dengan upload file
|
||||
- ✅ FindMany dengan pagination & search
|
||||
- ✅ FindUnique untuk detail
|
||||
- ✅ Delete dengan hard delete (via Prisma)
|
||||
- ✅ Update dengan file replacement
|
||||
|
||||
### **5. Edit Form - Original Data Tracking**
|
||||
- ✅ Original data state untuk reset form
|
||||
- ✅ Load data existing dengan benar
|
||||
- ✅ Preview image dari data lama
|
||||
- ✅ Reset form mengembalikan ke data original
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~70-95
|
||||
const data = await editState.edit.load(id);
|
||||
|
||||
setOriginalData({
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
imageId: data.imageId,
|
||||
imageUrl: data.image?.link || "",
|
||||
});
|
||||
|
||||
setFormData({
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
kategoriId: data.kategoriId,
|
||||
imageId: data.imageId,
|
||||
});
|
||||
|
||||
if (data.image?.link) setPreviewFile(data.image.link);
|
||||
|
||||
// Line ~105 - Handle reset
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
deskripsi: originalData.deskripsi,
|
||||
kategoriId: originalData.kategoriId,
|
||||
imageId: originalData.imageId,
|
||||
});
|
||||
setPreviewFile(originalData.imageUrl || null);
|
||||
setFile(null);
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
|
||||
|
||||
---
|
||||
|
||||
### **6. State Management - Good Practices**
|
||||
- ✅ Proper typing dengan Prisma types
|
||||
- ✅ Loading state management dengan finally block
|
||||
- ✅ Error handling yang comprehensive
|
||||
- ✅ Reset function untuk cleanup
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~70-95
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
prestasiDesa.findMany.loading = true; // ✅ Start loading
|
||||
prestasiDesa.findMany.page = page;
|
||||
prestasiDesa.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
prestasiDesa.findMany.data = res.data.data ?? [];
|
||||
prestasiDesa.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
prestasiDesa.findMany.data = [];
|
||||
prestasiDesa.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch prestasi desa paginated:", err);
|
||||
prestasiDesa.findMany.data = [];
|
||||
prestasiDesa.findMany.totalPages = 1;
|
||||
} finally {
|
||||
prestasiDesa.findMany.loading = false; // ✅ Stop loading
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Loading state management sudah proper.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - deletedAt Default Value SALAH**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 239-240)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model PrestasiDesa {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ SALAH - selalu punya default value
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model KategoriPrestasiDesa {
|
||||
// ...
|
||||
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:**
|
||||
```prisma
|
||||
// Record baru dibuat
|
||||
CREATE PrestasiDesa {
|
||||
name: "Prestasi 1",
|
||||
// deletedAt otomatis ter-set ke now() ❌
|
||||
// isActive: true ✅
|
||||
}
|
||||
|
||||
// Query untuk data aktif (seharusnya return data ini)
|
||||
prisma.prestasiDesa.findMany({
|
||||
where: { deletedAt: null, isActive: true }
|
||||
})
|
||||
// ❌ Return kosong! Karena deletedAt sudah ter-set
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix schema:
|
||||
```prisma
|
||||
model PrestasiDesa {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model KategoriPrestasiDesa {
|
||||
// ...
|
||||
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. State Management - Inconsistency Fetch Pattern**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany)
|
||||
const res = await ApiFetch.api.landingpage.prestasidesa["create"].post({...});
|
||||
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({query});
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
|
||||
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
|
||||
const response = await fetch(`/api/landingpage/prestasidesa/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
async load(id: string) {
|
||||
try {
|
||||
prestasiDesa.edit.loading = true;
|
||||
const res = await ApiFetch.api.landingpage.prestasidesa[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
const data = res.data.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
name: data.name,
|
||||
deskripsi: data.deskripsi,
|
||||
imageId: data.imageId,
|
||||
kategoriId: data.kategoriId,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.data?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading prestasi desa:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
|
||||
return null;
|
||||
} finally {
|
||||
prestasiDesa.edit.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di findUnique, edit, delete methods)
|
||||
|
||||
---
|
||||
|
||||
#### **3. findUnique State - Tidak Ada Loading State Management**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~110 - prestasiDesa.findUnique.load()
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
prestasiDesa.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
prestasiDesa.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
prestasiDesa.findUnique.data = null;
|
||||
}
|
||||
// ❌ MISSING: finally block untuk stop loading
|
||||
// ❌ MISSING: loading state initialization
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** UI mungkin stuck di loading state jika ada error.
|
||||
|
||||
**Rekomendasi:** Tambahkan loading state dan finally block:
|
||||
|
||||
```typescript
|
||||
async load(id: string) {
|
||||
try {
|
||||
prestasiDesa.findUnique.loading = true; // ✅ Start loading
|
||||
const res = await fetch(`/api/landingpage/prestasidesa/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
prestasiDesa.findUnique.data = data.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
prestasiDesa.findUnique.loading = false; // ✅ Stop loading
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. HTML Injection Risk - dangerouslySetInnerHTML**
|
||||
|
||||
**Lokasi:**
|
||||
- `list-prestasi-desa/page.tsx` (line ~90, 145)
|
||||
- `list-prestasi-desa/[id]/page.tsx` (line ~85)
|
||||
- `list-prestasi-desa/create/page.tsx` (CreateEditor component)
|
||||
- `list-prestasi-desa/[id]/edit/page.tsx` (EditEditor component)
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// ❌ Direct HTML render tanpa sanitization
|
||||
<Text
|
||||
lineClamp={1}
|
||||
fz="md"
|
||||
c="dimmed"
|
||||
lh={1.5}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
|
||||
/>
|
||||
```
|
||||
|
||||
**Risk:**
|
||||
- XSS attack jika admin input script malicious
|
||||
- Bisa inject iframe, script tag, dll
|
||||
- Security vulnerability
|
||||
|
||||
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Sanitize sebelum render
|
||||
const sanitizedHtml = DOMPurify.sanitize(item.deskripsi);
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
|
||||
|
||||
**Priority:** 🟡 Medium (**Security concern**)
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **5. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~73
|
||||
const query: any = { page, limit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
|
||||
// Line ~270
|
||||
const query: any = { page, limit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed query:
|
||||
|
||||
```typescript
|
||||
// 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. Console.log di Production**
|
||||
|
||||
**Lokasi:** Multiple places di state file
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~48
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// Line ~120
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
|
||||
// Line ~124
|
||||
console.error("Error fetching data:", error);
|
||||
|
||||
// Line ~300
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// ... dan banyak lagi
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Error Message Tidak Konsisten**
|
||||
|
||||
**Lokasi:** Multiple places
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Create - Line ~46
|
||||
return toast.error("Gagal menambahkan data");
|
||||
|
||||
// Create - Line ~48
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// Delete - Line ~150
|
||||
toast.error("Terjadi kesalahan saat menghapus prestasi desa");
|
||||
|
||||
// Edit - Line ~200
|
||||
toast.error("Gagal memuat data");
|
||||
|
||||
// Edit update - Line ~240
|
||||
toast.error("Gagal mengupdate prestasi desa");
|
||||
|
||||
// Toast success - Line ~235
|
||||
toast.success("Berhasil update prestasi desa");
|
||||
```
|
||||
|
||||
**Issue:**
|
||||
- Inconsistent capitalization
|
||||
- Mixed patterns ("Gagal menambahkan" vs "Terjadi kesalahan")
|
||||
- Generic messages
|
||||
|
||||
**Rekomendasi:** Standardisasi error messages:
|
||||
|
||||
```typescript
|
||||
// Pattern: "[Action] [resource] gagal" dengan proper casing
|
||||
toast.error("Menambahkan data Prestasi Desa gagal");
|
||||
toast.error("Menghapus data Prestasi Desa gagal");
|
||||
toast.error("Memuat data Prestasi Desa gagal");
|
||||
toast.error("Memperbarui data Prestasi Desa gagal");
|
||||
|
||||
// Atau lebih spesifik dengan context
|
||||
toast.error("Gagal menambahkan data Prestasi Desa");
|
||||
toast.error("Gagal menghapus Prestasi Desa");
|
||||
toast.success("Berhasil memperbarui Prestasi Desa");
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **8. Zod Schema - Error Message Tidak Akurat**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~8
|
||||
const templateprestasiDesaForm = z.object({
|
||||
name: z.string().min(1, "Judul minimal 1 karakter"), // ⚠️ "Judul" instead of "Nama"
|
||||
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), // ✅ OK
|
||||
imageId: z.string().min(1, "File minimal 1"), // ⚠️ Generic
|
||||
kategoriId: z.string().min(1, "Kategori minimal 1 karakter"), // ⚠️ "Kategori" instead of "Kategori Prestasi"
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:** User confusion saat validasi error muncul.
|
||||
|
||||
**Rekomendasi:** Fix error messages:
|
||||
|
||||
```typescript
|
||||
const templateprestasiDesaForm = z.object({
|
||||
name: z.string().min(1, "Nama prestasi wajib diisi"),
|
||||
deskripsi: z.string().min(1, "Deskripsi prestasi wajib diisi"),
|
||||
imageId: z.string().min(1, "Gambar prestasi wajib diunggah"),
|
||||
kategoriId: z.string().min(1, "Kategori prestasi wajib dipilih"),
|
||||
});
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **9. Component Name Mismatch**
|
||||
|
||||
**Lokasi:** `list-prestasi-desa/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~11
|
||||
function ListPrestasiDesa() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Line ~27
|
||||
function ListPrestasi({ search }: { search: string }) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ⚠️ Function name tidak konsisten dengan file name
|
||||
```
|
||||
|
||||
**Rekomendasi:** Rename ke yang lebih descriptive:
|
||||
```typescript
|
||||
function ListPrestasiDesaPage() {
|
||||
// ...
|
||||
}
|
||||
|
||||
function ListPrestasiDesaTable({ search }: { search: string }) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Pagination onChange Tidak Include Search**
|
||||
|
||||
**Lokasi:** `list-prestasi-desa/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~170
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={load} // ⚠️ Hanya pass page number
|
||||
total={totalPages}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Issue:** Saat ganti page, search query hilang karena `load` dipanggil hanya dengan page number.
|
||||
|
||||
**Rekomendasi:** Include search dan limit:
|
||||
```typescript
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage, 10, debouncedSearch)} // ✅ Include all params
|
||||
total={totalPages}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Mobile Pagination - load Function Tidak Lengkap**
|
||||
|
||||
**Lokasi:** `kategori-prestasi-desa/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~170 (Desktop)
|
||||
onChange={(newPage) => load(newPage)} // ⚠️ Missing limit & search
|
||||
|
||||
// Line ~200 (Mobile)
|
||||
onChange={(newPage) => load(newPage)} // ⚠️ Missing limit & search
|
||||
```
|
||||
|
||||
**Rekomendasi:** Include all params:
|
||||
```typescript
|
||||
onChange={(newPage) => load(newPage, 10, debouncedSearch)}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Duplicate Error Logging**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~100
|
||||
} catch (error) {
|
||||
console.error('Error loading prestasi desa:', error); // ❌ Duplicate
|
||||
toast.error('Gagal memuat data prestasi desa');
|
||||
}
|
||||
|
||||
// edit/page.tsx - Line ~130
|
||||
} catch (error) {
|
||||
console.error('Error updating prestasi desa:', error); // ❌ Duplicate
|
||||
toast.error('Terjadi kesalahan saat memperbarui prestasi desa');
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Cukup satu logging yang informatif:
|
||||
```typescript
|
||||
} catch (error) {
|
||||
console.error('Failed to load Prestasi Desa:', err);
|
||||
toast.error('Gagal memuat data Prestasi Desa');
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **13. Inconsistent Button Label**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// create/page.tsx - Line ~200
|
||||
<Button ...>Reset</Button>
|
||||
|
||||
// edit/page.tsx - Line ~180
|
||||
<Button ...>Batal</Button>
|
||||
|
||||
// Should be consistent: "Reset" atau "Batal"
|
||||
```
|
||||
|
||||
**Rekomendasi:** Standardisasi:
|
||||
```typescript
|
||||
// Create: "Reset"
|
||||
// Edit: "Batal" (lebih descriptive untuk cancel changes)
|
||||
// OR both: "Reset" / "Batal"
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **14. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:**
|
||||
- `list-prestasi-desa/page.tsx`: `placeholder='Cari nama prestasi...'` ✅ OK
|
||||
- `kategori-prestasi-desa/page.tsx`: `placeholder='Cari kategori prestasi...'` ✅ OK
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Placeholder sudah spesifik.
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **15. Response Clone Overkill di Kategori Edit**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/prestasi-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~370 - kategoriPrestasi.edit.update()
|
||||
const response = await fetch(...);
|
||||
const responseClone = response.clone();
|
||||
|
||||
try {
|
||||
const result = await response.json();
|
||||
// ...
|
||||
} catch (error) {
|
||||
try {
|
||||
const text = await responseClone.text();
|
||||
console.error("Error response text:", text);
|
||||
throw new Error(`Gagal memproses respons dari server: ${text}`);
|
||||
} catch (textError) {
|
||||
console.error("Error parsing response as text:", textError);
|
||||
console.error("Original error:", error);
|
||||
throw new Error("Gagal memproses respons dari server");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Analysis:**
|
||||
- ✅ **GOOD:** Error handling sangat thorough
|
||||
- ⚠️ **OVERKILL:** Untuk production API yang stable, ini berlebihan
|
||||
- ⚠️ **INCONSISTENT:** Module lain tidak punya error handling se-detail ini
|
||||
|
||||
**Rekomendasi:** Simplify untuk consistency:
|
||||
|
||||
```typescript
|
||||
async update() {
|
||||
try {
|
||||
kategoriPrestasi.edit.loading = true;
|
||||
|
||||
const response = await fetch(`/api/landingpage/kategoriprestasi/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: this.form.name }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result?.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message || "Berhasil update");
|
||||
await kategoriPrestasi.findMany.load();
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error(result.message || "Gagal update");
|
||||
} catch (error) {
|
||||
console.error("Error updating:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Gagal update");
|
||||
return false;
|
||||
} finally {
|
||||
kategoriPrestasi.edit.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P1 | Missing loading state in findUnique | State | Medium | Low | Perlu fix |
|
||||
| 🟡 M | HTML injection risk | UI | **High (Security)** | Low | **Should fix** |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Error message inconsistency | State/UI | Low | Low | Optional |
|
||||
| 🟡 M | Zod schema error messages | State | Low | Low | Should fix |
|
||||
| 🟢 L | Component name mismatch | List UI | Low | Low | Optional |
|
||||
| 🟢 L | Pagination missing search param | List UI | Low | Low | Should fix |
|
||||
| 🟢 L | Duplicate error logging | UI | Low | Low | Optional |
|
||||
| 🟢 L | Inconsistent button label | UI | Low | Low | Optional |
|
||||
| 🟢 L | Response clone overkill | State (Kategori) | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (7/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX konsisten & responsive
|
||||
2. ✅ File upload handling solid
|
||||
3. ✅ Form validation dengan Zod schema
|
||||
4. ✅ State management terstruktur (Valtio)
|
||||
5. ✅ **Edit form reset sudah benar** (original data tracking)
|
||||
6. ✅ Loading state management di findMany (dengan finally block)
|
||||
7. ✅ Modal konfirmasi hapus untuk user safety
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
|
||||
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
3. ⚠️ findUnique tidak ada loading state management
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
|
||||
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. ⚠️ **Add loading state** di findUnique operations
|
||||
4. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
|
||||
5. ⚠️ **Improve type safety** dengan remove `any` usage
|
||||
6. ⚠️ **Standardisasi error messages** di Zod schema dan toast
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
|
||||
2. **🔴 HIGH:** Refactor findUnique, edit, delete ke ApiFetch - 1 jam
|
||||
3. **🔴 HIGH:** Add loading state di findUnique - 15 menit
|
||||
4. **🟡 MEDIUM:** Fix HTML injection dengan DOMPurify - 30 menit
|
||||
5. **🟡 MEDIUM:** Improve type safety - 30 menit
|
||||
6. **🟢 LOW:** Polish minor issues - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | APBDes | Prestasi Desa | Notes |
|
||||
|--------|--------|-------------------|-----------|--------|---------------|-------|
|
||||
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
|
||||
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | ✅ Good | ⚠️ findUnique missing | Similar issue |
|
||||
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | ✅ Good | ✅ Good | All consistent |
|
||||
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
|
||||
| File Upload | ✅ Images | ✅ Documents | ✅ Images | ✅ Dual | ✅ Images | APBDes paling complex |
|
||||
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | ✅ Good | ✅ Good | Consistent |
|
||||
| **Schema deletedAt** | ⚠️ Issue | ⚠️ Issue | ⚠️ Issue | ✅ Good | ❌ **WRONG** | **Prestasi CRITICAL** |
|
||||
| HTML Injection | ⚠️ Present | ⚠️ Present | N/A | N/A | ⚠️ Present | Security concern |
|
||||
| Complexity | Low | Medium | Low | **High** | Medium | APBDes paling complex |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF PRESTASI DESA MODULE
|
||||
|
||||
**Standard Complexity:**
|
||||
1. **Single file upload** (gambar) - similar to SDGs, Profil
|
||||
2. **Kategori relation** - similar to Desa Anti Korupsi
|
||||
3. **Rich text editor** (deskripsi) - similar to Desa Anti Korupsi
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ Loading state management di findMany (dengan finally block) - better than SDGs
|
||||
2. ✅ Edit form reset comprehensive (preserve all fields)
|
||||
3. ✅ Proper typing di findMany (Prisma types)
|
||||
|
||||
**Critical Issues:**
|
||||
1. ❌ **Schema deletedAt SALAH** - sama seperti SDGs & Desa Anti Korupsi, tapi APBDes sudah benar
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul Prestasi Desa sudah **production-ready** dengan beberapa improvements yang bisa dilakukan secara incremental. Module ini memiliki struktur yang mirip dengan modul Desa Anti Korupsi (kategori relation, rich text editor, file upload).
|
||||
|
||||
**Unique Issues:**
|
||||
1. Schema deletedAt default value yang salah (sama seperti SDGs & Desa Anti Korupsi)
|
||||
2. HTML injection risk di deskripsi (sama seperti Desa Anti Korupsi)
|
||||
3. Fetch pattern inconsistency (sama seperti semua modul lain)
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 239-240, 248-249
|
||||
|
||||
model PrestasiDesa {
|
||||
// ...
|
||||
- deletedAt DateTime @default(now())
|
||||
+ deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model KategoriPrestasiDesa {
|
||||
// ...
|
||||
- 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_default
|
||||
```
|
||||
|
||||
Setelah fix critical schema issue, module ini production-ready! 🎉
|
||||
488
QC/Landing-Page/QC-PROFIL-MODULE.md
Normal file
488
QC/Landing-Page/QC-PROFIL-MODULE.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# QC Summary - Profil Landing Page Module
|
||||
|
||||
**Scope:** Media Sosial, Pejabat Desa, Program Inovasi
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement minor
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Module | Schema | API | UI Admin | Public Page | Overall |
|
||||
|--------|--------|-----|----------|-------------|---------|
|
||||
| Media Sosial | ✅ Baik | ✅ Baik | ✅ Baik | N/A | 🟢 Baik |
|
||||
| Pejabat Desa | ✅ Baik | ⚠️ Ada issue | ✅ Baik | N/A | 🟡 Perlu fix |
|
||||
| Program Inovasi | ✅ Baik | ✅ Baik | ✅ Baik | N/A | 🟢 Baik |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK (COMMON)
|
||||
|
||||
### **1. Konsistensi UI/UX**
|
||||
- ✅ Semua halaman menggunakan pattern yang sama (list → detail → edit)
|
||||
- ✅ Responsive design (desktop table + mobile cards)
|
||||
- ✅ Loading states dengan Skeleton
|
||||
- ✅ Empty state handling yang informatif
|
||||
- ✅ Search dengan debounce (1000ms)
|
||||
- ✅ Pagination konsisten di semua modul
|
||||
|
||||
### **2. File Upload Handling**
|
||||
- ✅ Dropzone dengan preview image
|
||||
- ✅ Validasi format & ukuran file (max 5MB)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
- ✅ Cleanup file state saat reset form
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk validasi typed
|
||||
- ✅ isFormValid() check sebelum submit
|
||||
- ✅ Error toast dengan pesan spesifik
|
||||
- ✅ Button disabled saat invalid/loading
|
||||
|
||||
### **4. State Management (Valtio)**
|
||||
- ✅ Proxy state untuk reaktivitas
|
||||
- ✅ Separate state per modul (programInovasi, pejabatDesa, mediaSosial)
|
||||
- ✅ Reset form function di setiap create/edit
|
||||
- ✅ Original data tracking untuk reset
|
||||
|
||||
### **5. Error Handling**
|
||||
- ✅ Try-catch di semua async operation
|
||||
- ✅ Toast error dengan pesan user-friendly
|
||||
- ✅ Console.error untuk debugging
|
||||
- ✅ Modal konfirmasi hapus
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Pejabat Desa - Edit Form Tidak Reset imageId ke Original**
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~100 - Load data
|
||||
setFormData({
|
||||
name: profileData.name || "",
|
||||
position: profileData.position || "",
|
||||
imageId: profileData.imageId || "", // ✅ Sudah benar
|
||||
});
|
||||
|
||||
// Line ~170 - Handle reset
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
position: originalData.position,
|
||||
imageId: originalData.imageId, // ✅ Sudah benar
|
||||
});
|
||||
```
|
||||
|
||||
**Status:** ✅ **SUDAH BENAR** - Tidak ada issue di sini
|
||||
|
||||
**Verdict:** Tidak ada action needed.
|
||||
|
||||
---
|
||||
|
||||
#### **2. Media Sosial - Edit Form Sudah Benar**
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/media-sosial/[id]/edit/page.tsx`
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Original data tracking sudah implementasi dengan baik:
|
||||
```typescript
|
||||
const [originalData, setOriginalData] = useState({
|
||||
name: '',
|
||||
icon: '',
|
||||
iconUrl: '',
|
||||
imageId: '',
|
||||
imageUrl: '',
|
||||
});
|
||||
|
||||
// Load data
|
||||
setOriginalData({
|
||||
...newForm,
|
||||
imageUrl: data.image?.link || '',
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
icon: originalData.icon,
|
||||
iconUrl: originalData.iconUrl,
|
||||
imageId: originalData.imageId,
|
||||
});
|
||||
```
|
||||
|
||||
**Verdict:** Tidak ada action needed.
|
||||
|
||||
---
|
||||
|
||||
#### **3. Program Inovasi - Edit Form Sudah Benar**
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/edit/page.tsx`
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
|
||||
|
||||
**Verdict:** Tidak ada action needed.
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Inconsistency: Fetch Method di State**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/profile.ts`
|
||||
|
||||
**Masalah:** Ada 3 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (programInovasi.create)
|
||||
const res = await ApiFetch.api.landingpage.programinovasi["create"].post(formData);
|
||||
|
||||
// ❌ Pattern 2: fetch manual (programInovasi.findUnique)
|
||||
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
|
||||
|
||||
// ❌ Pattern 3: fetch dengan headers (programInovasi.update)
|
||||
const response = await fetch(`/api/landingpage/programinovasi/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({...}),
|
||||
});
|
||||
|
||||
// ❌ Pattern 4: fetch dengan delete (programInovasi.delete)
|
||||
const response = await fetch(`/api/landingpage/programinovasi/del/${id}`, {
|
||||
method: "DELETE",
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅统一 pattern
|
||||
const res = await ApiFetch.api.landingpage.programinovasi["create"].post(formData);
|
||||
const res = await ApiFetch.api.landingpage.programinovasi[id].get();
|
||||
const res = await ApiFetch.api.landingpage.programinovasi[id].put(data);
|
||||
const res = await ApiFetch.api.landingpage.programinovasi["del"][id].delete();
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Low (refactor saja, tidak ada logic change)
|
||||
|
||||
---
|
||||
|
||||
#### **5. Media Sosial - Validasi IconUrl Tidak Selalu Relevan**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/media-sosial/create/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~67
|
||||
const isFormValid = () => {
|
||||
const isNameValid = stateMediaSosial.create.form.name?.trim() !== '';
|
||||
const isIconUrlValid = stateMediaSosial.create.form.iconUrl?.trim() !== ''; // ❌ Selalu required
|
||||
const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
|
||||
|
||||
return isNameValid && isIconUrlValid && isCustomIconValid;
|
||||
};
|
||||
```
|
||||
|
||||
**Scenario:**
|
||||
- User pilih icon "telephone" → iconUrl **seharusnya** required (nomor telepon)
|
||||
- User pilih icon "facebook" → iconUrl **seharusnya** required (URL profile)
|
||||
- Tapi jika user hanya mau tampil icon tanpa link → **tidak bisa**
|
||||
|
||||
**Rekomendasi:** Jadikan optional atau berikan default value:
|
||||
|
||||
```typescript
|
||||
const isFormValid = () => {
|
||||
const isNameValid = stateMediaSosial.create.form.name?.trim() !== '';
|
||||
// IconUrl optional, atau validasi berdasarkan selectedSosmed
|
||||
const isIconUrlValid = true; // atau validasi spesifik
|
||||
const isCustomIconValid = selectedSosmed !== 'custom' || file !== null;
|
||||
|
||||
return isNameValid && isCustomIconValid;
|
||||
};
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **6. Pejabat Desa - Hanya Ada 1 Data (Hardcoded ID "edit")**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/profil/pejabat-desa/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~17
|
||||
useShallowEffect(() => {
|
||||
allList.findUnique.load("edit"); // ❌ Hardcoded ID
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Tidak scalable jika nanti ada multiple pejabat desa
|
||||
- Pattern berbeda dari modul lain (yang pakai findMany)
|
||||
- Confusing untuk developer baru
|
||||
|
||||
**Rekomendasi:**
|
||||
- Jika memang hanya 1 data, tambahkan komentar:
|
||||
```typescript
|
||||
// Note: "edit" adalah special ID untuk single pejabat desa record
|
||||
// Backend akan return data pertama jika ID tidak ditemukan
|
||||
allList.findUnique.load("edit");
|
||||
```
|
||||
|
||||
- Atau gunakan pattern yang lebih clear:
|
||||
```typescript
|
||||
allList.findUnique.load("single"); // atau "default"
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low-Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Program Inovasi - HTML Injection Risk di Deskripsi**
|
||||
|
||||
**Lokasi:**
|
||||
- `src/app/admin/(dashboard)/landing-page/profil/program-inovasi/page.tsx` (line ~107)
|
||||
- `src/app/admin/(dashboard)/landing-page/profil/program-inovasi/[id]/page.tsx` (line ~105)
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// ❌ Direct HTML render tanpa sanitization
|
||||
<Text dangerouslySetInnerHTML={{ __html: item.description || '-' }}></Text>
|
||||
```
|
||||
|
||||
**Risk:**
|
||||
- XSS attack jika admin input script malicious
|
||||
- Bisa inject iframe, script tag, dll
|
||||
|
||||
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Sanitize sebelum render
|
||||
const sanitizedHtml = DOMPurify.sanitize(item.description);
|
||||
<Text dangerouslySetInnerHTML={{ __html: sanitizedHtml }}></Text>
|
||||
```
|
||||
|
||||
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, dll).
|
||||
|
||||
**Priority:** 🟡 Medium (security concern)
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **8. Inconsistency: Button Size & Styling**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:** Button styling tidak konsisten:
|
||||
|
||||
```typescript
|
||||
// Media Sosial create
|
||||
<Button size="md" ...>Simpan</Button>
|
||||
|
||||
// Program Inovasi create
|
||||
<Button size="md" ...>Simpan</Button>
|
||||
|
||||
// Pejabat Desa edit
|
||||
<Button size="md" ...>Simpan</Button>
|
||||
|
||||
// Media Sosial edit
|
||||
<Button size="md" ...>Simpan</Button>
|
||||
```
|
||||
|
||||
Tapi di detail page:
|
||||
```typescript
|
||||
// Semua detail page
|
||||
<Button size="md" ...> // ✅ Konsisten
|
||||
```
|
||||
|
||||
**Rekomendasi:** Buat konstanta untuk button size:
|
||||
```typescript
|
||||
const BUTTON_SIZE = "md";
|
||||
const BUTTON_VARIANT = "light";
|
||||
const BUTTON_RADIUS = "md";
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **9. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:** Multiple list pages
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Media Sosial
|
||||
placeholder='Cari nama media sosial atau kontak...' // ✅ Spesifik
|
||||
|
||||
// Program Inovasi
|
||||
placeholder="Cari program inovasi..." // ✅ Oke
|
||||
|
||||
// Pejabat Desa
|
||||
// ❌ Tidak ada search feature
|
||||
```
|
||||
|
||||
**Rekomendasi:** Tambahkan search feature ke Pejabat Desa jika memungkinkan, atau berikan komentar kenapa tidak ada (karena hanya 1 data).
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Loading State Tidak Selalu Akurat**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/profile.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~120 - findUnique.load untuk programInovasi
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
|
||||
// ❌ Tidak ada loading state update di sini
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
programInovasi.findUnique.data = data.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
// ❌ Tidak ada finally block untuk stop loading
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** UI mungkin stuck di loading state jika ada error.
|
||||
|
||||
**Rekomendasi:** Tambahkan finally block:
|
||||
```typescript
|
||||
async load(id: string) {
|
||||
try {
|
||||
programInovasi.findUnique.loading = true; // ✅ Start loading
|
||||
const res = await fetch(`/api/landingpage/programinovasi/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
programInovasi.findUnique.data = data.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
programInovasi.findUnique.loading = false; // ✅ Stop loading
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/profile.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~75
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~120
|
||||
data: null as Prisma.ProgramInovasiGetPayload<{...}> | null, // ✅ Typed
|
||||
|
||||
// Line ~200
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed data:
|
||||
```typescript
|
||||
data: null as Prisma.MediaSosialGetPayload<{ include: { image: true } }>[] | null
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Medium (perlu update semua reference)
|
||||
|
||||
---
|
||||
|
||||
#### **12. Console.log di Production**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Media Sosial edit page (line ~170)
|
||||
console.log("Data yang akan dikirim ke backend:", stateMediaSosial.update.form);
|
||||
|
||||
// Profile state (multiple places)
|
||||
console.log("Failed to load program inovasi:", res.statusText);
|
||||
console.log((error as Error).message);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log("Data:", stateMediaSosial.update.form);
|
||||
}
|
||||
```
|
||||
|
||||
Atau gunakan logging library (winston, pino, dll).
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🟡 M | Fetch method inconsistency | All | Medium | Low | Perlu refactor |
|
||||
| 🟡 M | IconUrl validation terlalu strict | Media Sosial | Low | Low | Perlu fix logic |
|
||||
| 🟡 M | HTML injection risk | Program Inovasi | **High (Security)** | Low | **Should fix** |
|
||||
| 🟢 L | Hardcoded ID "edit" | Pejabat Desa | Low | Low | Optional |
|
||||
| 🟢 L | Button styling inconsistency | All | Low | Low | Optional |
|
||||
| 🟢 L | Missing search feature | Pejabat Desa | Low | Low | Optional |
|
||||
| 🟢 L | Loading state inaccurate | All | Low | Low | Perlu fix |
|
||||
| 🟢 L | Type safety (any usage) | All | Low | Medium | Optional |
|
||||
| 🟢 L | Console.log in production | All | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (8/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX konsisten & responsive
|
||||
2. ✅ File upload handling sudah solid
|
||||
3. ✅ Form validation dengan Zod
|
||||
4. ✅ State management terstruktur
|
||||
5. ✅ Error handling comprehensive
|
||||
6. ✅ Edit form reset sudah benar di semua modul
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Security:** HTML injection di deskripsi Program Inovasi (prioritas)
|
||||
2. ⚠️ **Consistency:** Fetch method pattern (ApiFetch vs fetch manual)
|
||||
3. ⚠️ **Type Safety:** Reduce `any` usage, gunakan Prisma types
|
||||
4. ⚠️ **Loading States:** Pastikan selalu ada finally block
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **Fix HTML injection** dengan DOMPurify atau backend validation
|
||||
2. **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. **Add loading state cleanup** di semua async operations
|
||||
4. **Optional:** Improve type safety dengan remove `any`
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul Profil sudah **production-ready** dengan minor improvements yang bisa dilakukan secara incremental.
|
||||
651
QC/Landing-Page/QC-SDGS-DESA.md
Normal file
651
QC/Landing-Page/QC-SDGS-DESA.md
Normal file
@@ -0,0 +1,651 @@
|
||||
# QC Summary - SDGs Desa Module
|
||||
|
||||
**Scope:** List SDGs Desa, Create, Edit, Detail
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Aspect | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| SDGs Desa | ✅ Baik | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 Perlu fix |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK
|
||||
|
||||
### **1. UI/UX Consistency**
|
||||
- ✅ Responsive design (desktop table + mobile cards)
|
||||
- ✅ Loading states dengan Skeleton
|
||||
- ✅ Search dengan debounce (1000ms)
|
||||
- ✅ Pagination konsisten
|
||||
- ✅ Empty state handling yang informatif
|
||||
- ✅ Modal konfirmasi hapus
|
||||
|
||||
### **2. File Upload Handling**
|
||||
- ✅ Dropzone dengan preview image
|
||||
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
|
||||
- ✅ Validasi ukuran file (max 5MB)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk validasi typed
|
||||
- ✅ isFormValid() check sebelum submit
|
||||
- ✅ Error toast dengan pesan spesifik
|
||||
- ✅ Button disabled saat invalid/loading
|
||||
- ✅ Type number input untuk jumlah
|
||||
|
||||
### **4. CRUD Operations**
|
||||
- ✅ Create dengan upload file
|
||||
- ✅ FindMany dengan pagination & search
|
||||
- ✅ FindUnique untuk detail
|
||||
- ✅ Delete dengan hard delete (via Prisma)
|
||||
- ✅ Update dengan file replacement
|
||||
|
||||
### **5. Edit Form - Original Data Tracking**
|
||||
- ✅ Original data state untuk reset form
|
||||
- ✅ Load data existing dengan benar
|
||||
- ✅ Preview image dari data lama
|
||||
- ✅ Reset form mengembalikan ke data original
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// Line ~60-80 - Load data
|
||||
const data = await sdgsState.edit.load(id);
|
||||
|
||||
setFormData({
|
||||
name: data.name || "",
|
||||
jumlah: data.jumlah || "",
|
||||
imageId: data.imageId || "",
|
||||
});
|
||||
|
||||
setOriginalData({
|
||||
...newForm,
|
||||
imageUrl: data.image?.link || "",
|
||||
});
|
||||
|
||||
setPreviewImage(data.image?.link || null);
|
||||
|
||||
// Line ~90 - Handle reset
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
name: originalData.name,
|
||||
jumlah: originalData.jumlah,
|
||||
imageId: originalData.imageId,
|
||||
});
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setFile(null);
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Original data tracking sudah implementasi dengan baik.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. State Management - Inconsistency Fetch Pattern**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany)
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa["create"].post({...});
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa["findMany"].get({query});
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
|
||||
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
|
||||
const response = await fetch(`/api/landingpage/sdgsdesa/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa["create"].post(data);
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa[id].get();
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa[id].put(data);
|
||||
const res = await ApiFetch.api.landingpage.sdgsdesa["del"][id].delete();
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di semua state methods)
|
||||
|
||||
---
|
||||
|
||||
#### **2. findUnique State - Tidak Ada Loading State Management**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~125 - sdgsDesa.findUnique.load()
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
sdgsDesa.findUnique.data = data.data ?? null;
|
||||
} else {
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
sdgsDesa.findUnique.data = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
sdgsDesa.findUnique.data = null;
|
||||
}
|
||||
// ❌ MISSING: finally block untuk stop loading
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** UI mungkin stuck di loading state jika ada error.
|
||||
|
||||
**Rekomendasi:** Tambahkan loading state dan finally block:
|
||||
|
||||
```typescript
|
||||
async load(id: string) {
|
||||
try {
|
||||
sdgsDesa.findUnique.loading = true; // ✅ Start loading
|
||||
const res = await fetch(`/api/landingpage/sdgsdesa/${id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
sdgsDesa.findUnique.data = data.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
sdgsDesa.findUnique.loading = false; // ✅ Stop loading
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **3. findManyAll - Tidak Digunakan di UI**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~95 - findManyAll state
|
||||
findManyAll: {
|
||||
data: null as any[] | null,
|
||||
loading: false,
|
||||
load: async () => {
|
||||
// ... fetch all data tanpa pagination
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Analysis:**
|
||||
- ⚠️ **UNUSED:** Tidak ada component yang menggunakan `findManyAll`
|
||||
- ⚠️ **DEAD CODE:** Menambah bundle size tanpa manfaat
|
||||
- ⚠️ **CONFUSING:** Developer baru bisa bingung kapan pakai findMany vs findManyAll
|
||||
|
||||
**Rekomendasi:** Remove jika tidak digunakan:
|
||||
```typescript
|
||||
// ❌ Remove entire findManyAll block
|
||||
```
|
||||
|
||||
Atau jika diperlukan untuk future feature, tambahkan comment:
|
||||
```typescript
|
||||
// Reserved for future use - dropdown select without pagination
|
||||
findManyAll: { ... }
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Low-Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~58
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~96
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~118
|
||||
data: null as Prisma.SdgsDesaGetPayload<{...}> | null, // ✅ Typed
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed data consistently:
|
||||
|
||||
```typescript
|
||||
// findMany
|
||||
data: null as Prisma.SdgsDesaGetPayload<{
|
||||
include: { image: true };
|
||||
}>[] | null,
|
||||
|
||||
// findManyAll (jika tidak dihapus)
|
||||
data: null as Prisma.SdgsDesaGetPayload<{
|
||||
include: { image: true };
|
||||
}>[] | null,
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Medium (perlu update semua reference)
|
||||
|
||||
---
|
||||
|
||||
#### **5. Console.log di Production**
|
||||
|
||||
**Lokasi:** Multiple places di state file
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~48
|
||||
console.log(error);
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// Line ~80
|
||||
console.error("Failed to load media sosial:", res.data?.message);
|
||||
|
||||
// Line ~85
|
||||
console.error("Error loading media sosial:", error);
|
||||
|
||||
// Line ~132
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
|
||||
// Line ~136
|
||||
console.error("Error fetching data:", error);
|
||||
|
||||
// ... dan banyak lagi
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
Atau gunakan logging library (winston, pino, dll) dengan levels yang jelas.
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **6. Error Message Tidak Konsisten**
|
||||
|
||||
**Lokasi:** Multiple places
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Create - Line ~44
|
||||
return toast.error("Gagal menambahkan data");
|
||||
|
||||
// Create - Line ~46
|
||||
toast.error("Gagal menambahkan data");
|
||||
|
||||
// Delete - Line ~165
|
||||
toast.error("Terjadi kesalahan saat menghapus sdgs desa");
|
||||
|
||||
// Edit - Line ~210
|
||||
toast.error("Gagal memuat data");
|
||||
|
||||
// Edit update - Line ~250
|
||||
toast.error("Gagal mengupdate sdgs desa");
|
||||
|
||||
// Toast success - Line ~240
|
||||
toast.success("Berhasil update sdgs desa");
|
||||
```
|
||||
|
||||
**Issue:**
|
||||
- Inconsistent capitalization ("sdgs desa" vs "Sdgs Desa")
|
||||
- Mixed patterns ("Gagal menambahkan" vs "Terjadi kesalahan")
|
||||
- Typo: "sdgs" seharusnya "SDGs" (acronym)
|
||||
|
||||
**Rekomendasi:** Standardisasi error messages:
|
||||
|
||||
```typescript
|
||||
// Pattern: "[Action] [resource] gagal" dengan proper casing
|
||||
toast.error("Menambahkan data SDGs Desa gagal");
|
||||
toast.error("Menghapus data SDGs Desa gagal");
|
||||
toast.error("Memuat data SDGs Desa gagal");
|
||||
toast.error("Memperbarui data SDGs Desa gagal");
|
||||
|
||||
// Atau lebih spesifik dengan context
|
||||
toast.error("Gagal menambahkan data SDGs Desa");
|
||||
toast.error("Gagal menghapus SDGs Desa");
|
||||
toast.success("Berhasil memperbarui SDGs Desa");
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Zod Schema - Error Message Tidak Akurat**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/sdgs-desa.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~8
|
||||
const templatesdgsDesaForm = z.object({
|
||||
name: z.string().min(1, "Judul minimal 1 karakter"), // ❌ "Judul" instead of "Nama"
|
||||
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"), // ❌ "Deskripsi" instead of "Jumlah"
|
||||
imageId: z.string().min(1, "File minimal 1"),
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:** User confusion saat validasi error muncul:
|
||||
```
|
||||
Error: "Judul minimal 1 karakter" // User: "Lho, ini field nama bukan judul?"
|
||||
Error: "Deskripsi minimal 1 karakter" // User: "Ini field jumlah bukan deskripsi?"
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix error messages:
|
||||
|
||||
```typescript
|
||||
const templatesdgsDesaForm = z.object({
|
||||
name: z.string().min(1, "Nama SDGs Desa minimal 1 karakter"),
|
||||
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
|
||||
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||
});
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **8. Component Name Mismatch**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~30
|
||||
export default function EditKolaborasiInovasi() { // ❌ Wrong name
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:** Confusing untuk developer lain, sulit untuk search/reference.
|
||||
|
||||
**Rekomendasi:** Rename ke yang sesuai:
|
||||
```typescript
|
||||
export default function EditSDGsDesa() { // ✅ Correct name
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low (hanya rename)
|
||||
|
||||
---
|
||||
|
||||
#### **9. Text Label Tidak Konsisten**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Create page - Line ~100
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Program Inovasi // ❌ Wrong label
|
||||
</Text>
|
||||
|
||||
// Edit page - Line ~170
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar Program Inovasi // ❌ Wrong label (copy-paste?)
|
||||
</Text>
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix label:
|
||||
```typescript
|
||||
<Text fw="bold" fz="sm" mb={6}>
|
||||
Gambar SDGs Desa // ✅ Correct label
|
||||
</Text>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Placeholder Search Tidak Spesifik**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~17
|
||||
<HeaderSearch
|
||||
title='Sdgs Desa'
|
||||
placeholder='Cari Sdgs Desa...' // ⚠️ Generic
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Rekomendasi:** Lebih spesifik:
|
||||
```typescript
|
||||
placeholder='Cari nama SDGs Desa...'
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Capitalization Inconsistency**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// page.tsx - Line ~17
|
||||
title='Sdgs Desa' // ❌ Mixed case
|
||||
|
||||
// create/page.tsx - Line ~90
|
||||
<Title>Tambah Sdgs Desa</Title> // ❌ Mixed case
|
||||
|
||||
// edit/page.tsx - Line ~160
|
||||
<Title>Edit Sdgs Desa</Title> // ❌ Mixed case
|
||||
|
||||
// Should be:
|
||||
// "SDGs Desa" (all caps for acronym)
|
||||
```
|
||||
|
||||
**Rekomendasi:** Standardisasi:
|
||||
```typescript
|
||||
title='SDGs Desa'
|
||||
<Title>Tambah SDGs Desa</Title>
|
||||
<Title>Edit SDGs Desa</Title>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Schema - deletedAt Default Value**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma`
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model SdgsDesa {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ Always has default value
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
**Issue:** `deletedAt @default(now())` berarti setiap record baru langsung punya `deletedAt` value, yang bisa membingungkan untuk soft delete logic.
|
||||
|
||||
**Rekomendasi:**
|
||||
```prisma
|
||||
model SdgsDesa {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null) // ✅ Nullable, null = not deleted
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Medium (potential logic issue)
|
||||
**Effort:** Medium (perlu migration)
|
||||
|
||||
---
|
||||
|
||||
#### **13. Duplicate Error Logging**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~80
|
||||
} catch (error) {
|
||||
console.error("Error loading sdgs desa:", error); // ❌ Duplicate
|
||||
toast.error("Gagal memuat data sdgs desa");
|
||||
}
|
||||
|
||||
// Line ~120
|
||||
} catch (error) {
|
||||
console.error("Error updating sdgs desa:", error); // ❌ Duplicate
|
||||
toast.error("Terjadi kesalahan saat memperbarui sdgs desa");
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Cukup satu logging yang informatif:
|
||||
```typescript
|
||||
} catch (error) {
|
||||
console.error('Failed to load SDGs Desa:', err);
|
||||
toast.error('Gagal memuat data SDGs Desa');
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **14. API Response Handling - Inconsistent Error Messages**
|
||||
|
||||
**Lokasi:** API endpoints
|
||||
|
||||
**Masalah:** (dari grep search results)
|
||||
```typescript
|
||||
// del.ts - Line ~18
|
||||
message: "Berhasil menghapus SDGS Desa", // ✅ Proper
|
||||
|
||||
// updt.ts - Line ~38
|
||||
message: "SDGS Desa berhasil diperbarui", // ✅ Proper
|
||||
|
||||
// create.ts - (assumed)
|
||||
// Might have inconsistent casing
|
||||
```
|
||||
|
||||
**Rekomendasi:** Ensure all API responses use consistent "SDGs Desa" casing.
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P0 | Missing loading state in findUnique | State | Medium | Low | Perlu fix |
|
||||
| 🔴 P1 | Unused findManyAll code | State | Low | Low | Should remove |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Medium | Optional |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Error message inconsistency | State/UI | Low | Low | Optional |
|
||||
| 🟡 M | Zod schema error messages | State | Low | Low | Should fix |
|
||||
| 🟢 L | Component name mismatch | Edit page | Low | Low | Optional |
|
||||
| 🟢 L | Wrong label text ("Program Inovasi") | Create/Edit | Low | Low | Should fix |
|
||||
| 🟢 L | Placeholder tidak spesifik | List page | Low | Low | Optional |
|
||||
| 🟢 L | Capitalization inconsistency | All UI | Low | Low | Should fix |
|
||||
| 🟢 M | deletedAt default value | Schema | Medium | Medium | Should fix |
|
||||
| 🟢 L | Duplicate error logging | Edit page | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (7.5/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX konsisten & responsive
|
||||
2. ✅ File upload handling solid
|
||||
3. ✅ Form validation dengan Zod schema
|
||||
4. ✅ State management terstruktur (Valtio)
|
||||
5. ✅ **Edit form reset sudah benar** (original data tracking)
|
||||
6. ✅ Modal konfirmasi hapus untuk user safety
|
||||
7. ✅ Type number input untuk field jumlah
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Consistency:** Fetch method pattern (ApiFetch vs fetch manual)
|
||||
2. ⚠️ **Loading States:** findUnique tidak ada loading state management
|
||||
3. ⚠️ **Dead Code:** findManyAll tidak digunakan
|
||||
4. ⚠️ **Type Safety:** Reduce `any` usage, gunakan Prisma types
|
||||
5. ⚠️ **Schema:** deletedAt default value bisa menyebabkan logic issue
|
||||
6. ⚠️ **Naming:** Component name & label text masih ada yang salah
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
2. **Add loading state** di findUnique operations
|
||||
3. **Remove findManyAll** jika tidak digunakan
|
||||
4. **Fix component name** (EditKolaborasiInovasi → EditSDGsDesa)
|
||||
5. **Fix label text** ("Gambar Program Inovasi" → "Gambar SDGs Desa")
|
||||
6. **Fix capitalization** (Sdgs → SDGs)
|
||||
7. **Optional:** Improve type safety dengan remove `any`
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | Notes |
|
||||
|--------|--------|-------------------|-----------|-------|
|
||||
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
|
||||
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | Same issue |
|
||||
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | Consistent |
|
||||
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | Same issue |
|
||||
| File Upload | ✅ Images | ✅ Documents | ✅ Images | Different use case |
|
||||
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | Consistent |
|
||||
| Dead Code | ❌ None | ❌ None | ⚠️ findManyAll | SDGs unique issue |
|
||||
| Naming Issues | ❌ None | ⚠️ Some | ⚠️ Some | Similar level |
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul SDGs Desa sudah **production-ready** dengan beberapa improvements yang bisa dilakukan secara incremental. Module ini memiliki struktur yang mirip dengan modul lain (Profil, Desa Anti Korupsi) sehingga pattern improvement yang sama bisa diterapkan.
|
||||
|
||||
**Unique Issues:**
|
||||
1. findManyAll unused code (tidak ada di modul lain)
|
||||
2. Component name mismatch (EditKolaborasiInovasi)
|
||||
3. Wrong label text ("Gambar Program Inovasi") - kemungkinan copy-paste dari modul Program Inovasi
|
||||
879
QC/PPID/QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md
Normal file
879
QC/PPID/QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md
Normal file
@@ -0,0 +1,879 @@
|
||||
# QC Summary - Daftar Informasi Publik PPID Module
|
||||
|
||||
**Scope:** List Daftar Informasi Publik, Create, Edit, Detail
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Aspect | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| Daftar 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
|
||||
- ✅ Sticky table header untuk better UX
|
||||
- ✅ Responsive button text ("Tambah" vs "Tambah Baru")
|
||||
|
||||
### **2. Table & Card Layout**
|
||||
- ✅ Fixed column widths (25%, 40%, 20%)
|
||||
- ✅ Sticky header table untuk long lists
|
||||
- ✅ Striped rows untuk readability
|
||||
- ✅ Highlight on hover
|
||||
- ✅ HTML tag stripping untuk preview deskripsi
|
||||
- ✅ Text truncation dengan lineClamp dan substring
|
||||
- ✅ Mobile card view dengan proper information hierarchy
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// page.tsx - Line ~95-120
|
||||
<Table
|
||||
highlightOnHover
|
||||
striped
|
||||
stickyHeader // ✅ GOOD - Header tetap visible saat scroll
|
||||
style={{ minWidth: '700px' }} // ✅ GOOD - Minimum width untuk readability
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="25%">
|
||||
<Text fw={600} lh={1.4}>Jenis Informasi</Text>
|
||||
</TableTh>
|
||||
<TableTh w="40%">
|
||||
<Text fw={600} lh={1.4}>Deskripsi</Text>
|
||||
</TableTh>
|
||||
<TableTh ta="center" w="20%">
|
||||
<Text fw={600} lh={1.4}>Aksi</Text>
|
||||
</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Table layout dengan sticky header 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
|
||||
- ✅ Proper date formatting untuk update operation
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~50-85
|
||||
findMany: {
|
||||
data: null as Prisma.DaftarInformasiPublikGetPayload<{ omit: { isActive: true } }>[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
daftarInformasiPublik.findMany.loading = true; // ✅ Start loading
|
||||
daftarInformasiPublik.findMany.page = page;
|
||||
daftarInformasiPublik.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
daftarInformasiPublik.findMany.data = res.data.data ?? [];
|
||||
daftarInformasiPublik.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch daftar informasi publik:", err);
|
||||
daftarInformasiPublik.findMany.data = [];
|
||||
daftarInformasiPublik.findMany.totalPages = 1;
|
||||
} finally {
|
||||
daftarInformasiPublik.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
|
||||
- ✅ Minimum character validation (3 characters)
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~8-12
|
||||
const templateDaftarInformasi = z.object({
|
||||
jenisInformasi: z.string().min(3, "Jenis Informasi minimal 3 karakter"),
|
||||
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||
tanggal: z.string().min(3, "Tanggal minimal 3 karakter"),
|
||||
});
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Validation yang proper!
|
||||
|
||||
---
|
||||
|
||||
### **5. Edit Form - Original Data Tracking**
|
||||
- ✅ Original data state untuk reset form (via useState)
|
||||
- ✅ Load data existing dengan benar
|
||||
- ✅ Reset form mengembalikan ke data original
|
||||
- ✅ Rich text content handling yang proper
|
||||
- ✅ Date formatting untuk input type="date"
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~30-60
|
||||
const [formData, setFormData] = useState<FormDaftarInformasi>({
|
||||
jenisInformasi: '',
|
||||
deskripsi: '',
|
||||
tanggal: '',
|
||||
});
|
||||
|
||||
const formatDateForInput = (dateString: string) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0]; // ✅ Format untuk input date
|
||||
};
|
||||
|
||||
// Load data
|
||||
useEffect(() => {
|
||||
const loadDaftarInformasi = async () => {
|
||||
const data = await daftarInformasi.edit.load(id);
|
||||
if (data) {
|
||||
setFormData({
|
||||
jenisInformasi: data.jenisInformasi || '',
|
||||
deskripsi: data.deskripsi || '',
|
||||
tanggal: data.tanggal || '',
|
||||
});
|
||||
}
|
||||
};
|
||||
loadDaftarInformasi();
|
||||
}, [params?.id]);
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Original data tracking sudah implementasi dengan baik!
|
||||
|
||||
---
|
||||
|
||||
### **6. Rich Text Editor**
|
||||
- ✅ CreateEditor untuk create page
|
||||
- ✅ EditEditor untuk edit page
|
||||
- ✅ Reusable component pattern
|
||||
- ✅ HTML content handling yang proper
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - deletedAt Default Value SALAH**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 414)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model DaftarInformasiPublik {
|
||||
id String @id @default(cuid())
|
||||
jenisInformasi String
|
||||
deskripsi String
|
||||
tanggal DateTime @db.Date
|
||||
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:**
|
||||
```prisma
|
||||
// Record baru dibuat
|
||||
CREATE DaftarInformasiPublik {
|
||||
jenisInformasi: "Informasi 1",
|
||||
deskripsi: "Deskripsi 1",
|
||||
tanggal: "2024-01-01",
|
||||
// deletedAt otomatis ter-set ke now() ❌
|
||||
// isActive: true ✅
|
||||
}
|
||||
|
||||
// Query untuk data aktif (seharusnya return data ini)
|
||||
prisma.daftarInformasiPublik.findMany({
|
||||
where: { deletedAt: null, isActive: true }
|
||||
})
|
||||
// ❌ Return kosong! Karena deletedAt sudah ter-set
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix schema:
|
||||
```prisma
|
||||
model DaftarInformasiPublik {
|
||||
id String @id @default(cuid())
|
||||
jenisInformasi String
|
||||
deskripsi String
|
||||
tanggal DateTime @db.Date
|
||||
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. State Management - Fetch Pattern Inconsistency**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany)
|
||||
const res = await ApiFetch.api.ppid.daftarinformasipublik["create"].post(form);
|
||||
const res = await ApiFetch.api.ppid.daftarinformasipublik["find-many"].get({ query });
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique, edit, delete)
|
||||
const res = await fetch(`/api/ppid/daftarinformasipublik/${id}`);
|
||||
const response = await fetch(`/api/ppid/daftarinformasipublik/del/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.daftarinformasipublik[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
const data = res.data.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
jenisInformasi: data.jenisInformasi,
|
||||
deskripsi: data.deskripsi,
|
||||
tanggal: data.tanggal,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.data?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast.error("Gagal memuat data");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async byId(id: string) {
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.daftarinformasipublik["del"][id].delete();
|
||||
|
||||
if (res.data?.success) {
|
||||
toast.success(res.data.message || "Berhasil hapus");
|
||||
await daftarInformasiPublik.findMany.load();
|
||||
} else {
|
||||
toast.error(res.data?.message || "Gagal hapus");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di findUnique, edit, delete methods)
|
||||
|
||||
---
|
||||
|
||||
#### **3. Missing Loading State di Edit Button**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~130-145
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!isFormValid()} // ⚠️ Missing loading check
|
||||
radius="md"
|
||||
size="md"
|
||||
// ...
|
||||
>
|
||||
Simpan Perubahan
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Issue:** Button tidak disabled saat submitting. User bisa click multiple times.
|
||||
|
||||
**Rekomendasi:** Add loading state:
|
||||
```typescript
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// In handleSubmit
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await daftarInformasi.edit.update();
|
||||
router.push('/admin/ppid/daftar-informasi-publik');
|
||||
} catch (error) {
|
||||
// ...
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// In button
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
// ...
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan Perubahan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Console.log di Production**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~45
|
||||
console.log((error as Error).message);
|
||||
|
||||
// Line ~80
|
||||
console.error("Gagal fetch daftar informasi publik paginated:", err);
|
||||
|
||||
// Line ~100
|
||||
console.error("Failed to fetch daftar informasi publik:", res.statusText);
|
||||
|
||||
// Line ~104
|
||||
console.error("Error fetching daftar informasi publik:", error);
|
||||
|
||||
// Line ~180
|
||||
console.error("Error loading daftar informasi publik:", error);
|
||||
|
||||
// Line ~230
|
||||
console.error("Error updating daftar informasi publik:", error);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **5. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~70
|
||||
const query: any = { page, limit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed query:
|
||||
|
||||
```typescript
|
||||
// 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. Alert() Instead of Toast**
|
||||
|
||||
**Lokasi:** `create/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~30-40
|
||||
const handleSubmit = async () => {
|
||||
if (!daftarInformasi.create.form.jenisInformasi) {
|
||||
return alert('Mohon isi jenis informasi'); // ❌ Using alert()
|
||||
}
|
||||
if (!daftarInformasi.create.form.deskripsi) {
|
||||
return alert('Mohon isi deskripsi'); // ❌ Using alert()
|
||||
}
|
||||
if (!daftarInformasi.create.form.tanggal) {
|
||||
return alert('Mohon pilih tanggal publikasi'); // ❌ Using alert()
|
||||
}
|
||||
|
||||
try {
|
||||
await daftarInformasi.create.create();
|
||||
// ...
|
||||
} catch (error) {
|
||||
console.error('Error creating informasi publik:', error);
|
||||
alert('Terjadi kesalahan saat menyimpan data'); // ❌ Using alert()
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan toast untuk consistency:
|
||||
|
||||
```typescript
|
||||
if (!daftarInformasi.create.form.jenisInformasi) {
|
||||
return toast.warn('Mohon isi jenis informasi'); // ✅ Using toast
|
||||
}
|
||||
// ...
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Missing Reset Form Function**
|
||||
|
||||
**Lokasi:** `create/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~20-25
|
||||
const resetForm = () => {
|
||||
daftarInformasi.create.form = {
|
||||
jenisInformasi: "",
|
||||
deskripsi: "",
|
||||
tanggal: "",
|
||||
};
|
||||
};
|
||||
|
||||
// resetForm dipanggil di handleSubmit tapi tidak ada di form inputs
|
||||
// Form inputs langsung update state tanpa reset setelah submit
|
||||
```
|
||||
|
||||
**Issue:** Form tidak reset setelah successful submit.
|
||||
|
||||
**Rekomendasi:** Ensure reset is called:
|
||||
```typescript
|
||||
const handleSubmit = async () => {
|
||||
// ... validation
|
||||
|
||||
try {
|
||||
await daftarInformasi.create.create();
|
||||
resetForm(); // ✅ Make sure this is called
|
||||
router.push("/admin/ppid/daftar-informasi-publik");
|
||||
} catch (error) {
|
||||
// ...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - resetForm() sudah dipanggil di handleSubmit!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **8. Pagination onChange Tidak Include Search**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~190-200
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10); // ⚠️ Missing search parameter
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Issue:** Saat ganti page, search query hilang.
|
||||
|
||||
**Rekomendasi:** Include search:
|
||||
```typescript
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, debouncedSearch); // ✅ Include search
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **9. Duplicate Error Logging**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~60
|
||||
} catch (error) {
|
||||
console.error('Error loading daftar informasi:', error); // ❌ Duplicate
|
||||
toast.error('Gagal memuat data daftar informasi');
|
||||
}
|
||||
|
||||
// edit/page.tsx - Line ~80
|
||||
} catch (error) {
|
||||
console.error('Error updating berita:', error); // ❌ Duplicate + wrong module name
|
||||
toast.error('Terjadi kesalahan saat memperbarui berita'); // ❌ Wrong module name
|
||||
}
|
||||
```
|
||||
|
||||
**Issue:** Copy-paste error dari module "berita"!
|
||||
|
||||
**Rekomendasi:** Fix error messages:
|
||||
```typescript
|
||||
} catch (error) {
|
||||
console.error('Failed to load Daftar Informasi Publik:', err);
|
||||
toast.error('Gagal memuat data Daftar Informasi Publik');
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Missing Loading State di Detail Page**
|
||||
|
||||
**Lokasi:** `[id]/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~20-25
|
||||
useShallowEffect(() => {
|
||||
stateDaftarInformasi.findUnique.load(params?.id as string)
|
||||
}, [params?.id])
|
||||
|
||||
if (!stateDaftarInformasi.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:
|
||||
```typescript
|
||||
if (stateDaftarInformasi.findUnique.loading) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={500} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stateDaftarInformasi.findUnique.data) {
|
||||
return (
|
||||
<Alert icon={<IconAlertCircle />} color="red">
|
||||
Data tidak ditemukan
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~30-35
|
||||
<HeaderSearch
|
||||
title='Daftar Informasi Publik'
|
||||
placeholder='Cari jenis informasi atau deskripsi...' // ✅ Actually pretty specific!
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Placeholder sudah spesifik!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **12. Empty State Icon Consistency**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~85-95
|
||||
<Stack align="center" py="xl">
|
||||
<IconDeviceImacCog size={40} stroke={1.5} color={colors['blue-button']} />
|
||||
<Text fz={{ base: 'sm', md: 'md' }} fw={500} c="dimmed" lh={1.5}>
|
||||
Belum ada informasi publik yang tersedia
|
||||
</Text>
|
||||
</Stack>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Empty state dengan icon yang proper!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **13. HTML Tag Stripping for Preview**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~125-130
|
||||
<Text fz="sm" lh={1.5} c="dimmed" lineClamp={1}>
|
||||
{item.deskripsi?.replace(/<[^>]*>?/gm, '').substring(0, 80)}...
|
||||
</Text>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - HTML tag stripping yang proper untuk preview!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P1 | Missing loading state di edit button | UI | Medium | Low | Should fix |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
|
||||
| 🟡 M | Alert() instead of toast | Create UI | Low | Low | Should fix |
|
||||
| 🟡 M | Copy-paste error messages (berita) | Edit UI | Low | Low | Should fix |
|
||||
| 🟢 L | Pagination missing search param | UI | Low | Low | Optional |
|
||||
| 🟢 L | Missing loading state di detail page | UI | Low | Low | Optional |
|
||||
| 🟢 L | Duplicate error logging | UI/State | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (8/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX clean & responsive
|
||||
2. ✅ **Sticky header table** - Better UX untuk long lists
|
||||
3. ✅ **HTML tag stripping** untuk preview deskripsi
|
||||
4. ✅ Search functionality dengan debounce
|
||||
5. ✅ Empty state handling yang informatif
|
||||
6. ✅ **Zod validation** comprehensive
|
||||
7. ✅ State management dengan ApiFetch untuk create & findMany
|
||||
8. ✅ Loading state management dengan finally block
|
||||
9. ✅ Mobile cards responsive
|
||||
10. ✅ **Responsive button text** ("Tambah" vs "Tambah Baru")
|
||||
11. ✅ Edit form dengan original data tracking
|
||||
12. ✅ Date formatting untuk input type="date"
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
|
||||
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
3. ⚠️ Missing loading state di edit button
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
|
||||
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. ⚠️ **Add loading state** di edit button
|
||||
4. ⚠️ **Fix alert()** ke toast
|
||||
5. ⚠️ **Fix copy-paste error messages** dari module "berita"
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
|
||||
2. **🔴 HIGH: Refactor findUnique, edit, delete** ke ApiFetch - 1 jam
|
||||
3. **🔴 HIGH: Add loading state** di edit button - 15 menit
|
||||
4. **🟡 MEDIUM: Fix alert()** ke toast - 15 menit
|
||||
5. **🟡 MEDIUM: Fix copy-paste error messages** - 10 menit
|
||||
6. **🟢 LOW: Add pagination search param** - 10 menit
|
||||
7. **🟢 LOW: Polish minor issues** - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Module | Fetch Pattern | State | Validation | Schema | Loading State | Overall |
|
||||
|--------|--------------|-------|------------|--------|---------------|---------|
|
||||
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ⚠️ Some missing | 🟢 |
|
||||
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ⚠️ Some missing | 🟢 |
|
||||
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ⚠️ Missing | 🟢 |
|
||||
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Good | ✅ Good | 🟢 |
|
||||
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ⚠️ Some missing | 🟢 |
|
||||
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ Good | ❌ WRONG | ✅ Good | 🟢⭐ |
|
||||
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ⚠️ Inconsistent | ✅ Good | 🟢 |
|
||||
| Visi Misi PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | ✅ Good | 🟢⭐⭐ |
|
||||
| Dasar Hukum PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | ✅ Good | 🟢⭐⭐ |
|
||||
| Permohonan Informasi | ⚠️ Mixed | ⚠️ Good | ✅ **Best** | ❌ **4 models WRONG** | ✅ Good | 🟡 |
|
||||
| **Daftar Informasi** | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ⚠️ Some missing | 🟢 |
|
||||
|
||||
**Daftar Informasi PPID Highlights:**
|
||||
- ✅ **Sticky header table** - Unique feature untuk better UX
|
||||
- ✅ **HTML tag stripping** untuk preview - Good practice
|
||||
- ✅ **Responsive button text** - Attention to detail
|
||||
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
|
||||
- ⚠️ **Copy-paste errors** dari module "berita"
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF DAFTAR INFORMASI MODULE
|
||||
|
||||
**Best Table Implementation:**
|
||||
1. ✅ **Sticky header table** - Unique feature!
|
||||
2. ✅ **HTML tag stripping** untuk preview deskripsi
|
||||
3. ✅ **Responsive button text** - "Tambah" vs "Tambah Baru"
|
||||
4. ✅ **Fixed column widths** - 25%, 40%, 20%
|
||||
5. ✅ **Minimum table width** - 700px untuk readability
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ **Sticky header** - Best practice untuk long lists
|
||||
2. ✅ **HTML stripping** - Good practice untuk rich text preview
|
||||
3. ✅ **Loading state management** - Proper dengan finally block
|
||||
4. ✅ **Original data tracking** - Edit form reset yang proper
|
||||
5. ✅ **Date formatting** - Proper untuk input type="date"
|
||||
|
||||
**Critical Issues:**
|
||||
1. ❌ **Schema deletedAt SALAH** - Same issue seperti modul PPID lain
|
||||
2. ❌ **Fetch pattern inconsistency** - findUnique, edit, delete pakai fetch manual
|
||||
3. ❌ **Copy-paste error messages** - Dari module "berita"
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** **Daftar Informasi PPID adalah MODULE DENGAN TABLE IMPLEMENTATION TERBAIK** dengan sticky header dan HTML tag stripping untuk preview. Module ini juga punya attention to detail dengan responsive button text.
|
||||
|
||||
**Unique Strengths:**
|
||||
1. ✅ **Sticky header table** - Best table UX
|
||||
2. ✅ **HTML tag stripping** - Best practice untuk preview
|
||||
3. ✅ **Responsive button text** - Attention to detail
|
||||
4. ✅ **Fixed column widths** - Consistent layout
|
||||
5. ✅ **Date formatting** - Proper handling
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 414
|
||||
|
||||
model DaftarInformasiPublik {
|
||||
id String @id @default(cuid())
|
||||
jenisInformasi String
|
||||
deskripsi String
|
||||
tanggal DateTime @db.Date
|
||||
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_daftar_informasi
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 FIX COPY-PASTE ERRORS (10 MENIT):
|
||||
File: edit/page.tsx
|
||||
|
||||
// Line ~80
|
||||
- console.error('Error updating berita:', error);
|
||||
+ console.error('Error updating daftar informasi:', error);
|
||||
|
||||
- toast.error('Terjadi kesalahan saat memperbarui berita');
|
||||
+ toast.error('Terjadi kesalahan saat memperbarui daftar informasi');
|
||||
```
|
||||
|
||||
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST TABLE IMPLEMENTATION**! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**Daftar Informasi PPID Module adalah BEST PRACTICE untuk:**
|
||||
1. ✅ **Sticky header table** - Best practice untuk long lists
|
||||
2. ✅ **HTML tag stripping** - Good practice untuk rich text preview
|
||||
3. ✅ **Responsive button text** - Attention to detail
|
||||
4. ✅ **Fixed column widths** - Consistent layout
|
||||
5. ✅ **Date formatting** - Proper handling untuk date inputs
|
||||
|
||||
**Modules lain bisa belajar dari Daftar Informasi:**
|
||||
- **ALL MODULES WITH TABLES:** Use sticky header untuk better UX
|
||||
- **ALL MODULES WITH RICH TEXT:** Strip HTML tags untuk preview
|
||||
- **ALL MODULES:** Responsive text untuk buttons
|
||||
- **ALL MODULES:** Fixed column widths untuk consistency
|
||||
- **ALL MODULES:** Proper date formatting untuk date inputs
|
||||
|
||||
---
|
||||
|
||||
**File Location:** `QC/PPID/QC-DAFTAR-INFORMASI-PUBLIK-MODULE.md` 📄
|
||||
821
QC/PPID/QC-DASAR-HUKUM-PPID-MODULE.md
Normal file
821
QC/PPID/QC-DASAR-HUKUM-PPID-MODULE.md
Normal file
@@ -0,0 +1,821 @@
|
||||
# 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):**
|
||||
```typescript
|
||||
// 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):**
|
||||
```typescript
|
||||
// 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):**
|
||||
```typescript
|
||||
// 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):**
|
||||
```typescript
|
||||
// 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:**
|
||||
```prisma
|
||||
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:**
|
||||
```prisma
|
||||
// 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:
|
||||
```prisma
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// Line ~40
|
||||
console.error((error as Error).message);
|
||||
|
||||
// Line ~65
|
||||
console.error((error as Error).message);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
// Page title: order={2}
|
||||
// Content title (judul): order={3}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Missing Toast Success After Save**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```diff
|
||||
🔴 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
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 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**! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**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` 📄
|
||||
913
QC/PPID/QC-IKM-MODULE.md
Normal file
913
QC/PPID/QC-IKM-MODULE.md
Normal file
@@ -0,0 +1,913 @@
|
||||
# QC Summary - Indeks Kepuasan Masyarakat (IKM) PPID Module
|
||||
|
||||
**Scope:** Responden (CRUD), Grafik Kepuasan Masyarakat, Master Data (Jenis Kelamin, Rating, Kelompok Umur)
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Sub-Module | Schema | API | UI Admin | State Management | Overall |
|
||||
|------------|--------|-----|----------|-----------------|---------|
|
||||
| Responden | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 |
|
||||
| Grafik IKM | ✅ Baik | ✅ Baik | ✅ **Excellent** | ✅ Baik | 🟢 |
|
||||
| Master Data (JK, Rating, Umur) | ⚠️ Ada issue | ✅ Baik | N/A | ⚠️ Ada issue | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK
|
||||
|
||||
### **1. UI/UX - Grafik & Charts (UNIQUE FEATURE!)**
|
||||
- ✅ **Mantine Charts** - PieChart & BarChart yang modern
|
||||
- ✅ **3 Distribusi Charts**: Jenis Kelamin, Penilaian, Kelompok Umur
|
||||
- ✅ **Bar Chart Tren** - Monthly respondent trends
|
||||
- ✅ **Responsive design** - SimpleGrid dengan proper breakpoints
|
||||
- ✅ **Empty state handling** - "Tidak ada data" message
|
||||
- ✅ **Loading states** dengan Skeleton
|
||||
- ✅ **Color coding** yang konsisten
|
||||
- ✅ **Legend & Labels** yang informatif
|
||||
- ✅ **Tooltip** untuk interactive charts
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// grafik-kepuasan-masyarakat/page.tsx - Line ~100-150
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" radius="xl" shadow="sm">
|
||||
<Title order={3} mb="md" ta="center">Tren Jumlah Responden</Title>
|
||||
<Box h={320}>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **EXCELLENT** - Best chart implementation di semua modul PPID!
|
||||
|
||||
---
|
||||
|
||||
### **2. Data Processing untuk Charts**
|
||||
- ✅ Automatic calculation dari data responden
|
||||
- ✅ Grouping by gender, rating, age group
|
||||
- ✅ Monthly aggregation untuk bar chart
|
||||
- ✅ Date parsing dari multiple fields (createdAt, tanggal)
|
||||
- ✅ Sorting by month/year
|
||||
- ✅ Empty data handling (all values = 0)
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// grafik-kepuasan-masyarakat/page.tsx - Line ~45-85
|
||||
// Hitung total berdasarkan jenis kelamin
|
||||
const totalLaki = data.filter((item: any) =>
|
||||
item.jenisKelamin?.name?.toLowerCase() === 'laki-laki'
|
||||
).length;
|
||||
|
||||
const totalPerempuan = data.filter((item: any) =>
|
||||
item.jenisKelamin?.name?.toLowerCase() === 'perempuan'
|
||||
).length;
|
||||
|
||||
// Update gender chart data
|
||||
setDonutDataJenisKelamin([
|
||||
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
|
||||
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
|
||||
]);
|
||||
|
||||
// Process data for bar chart (group by month)
|
||||
const monthYearMap = new Map<string, number>();
|
||||
data.forEach((item: any) => {
|
||||
const dateValue = item.tanggal || item.createdAt;
|
||||
const parsedDate = new Date(dateValue);
|
||||
const month = parsedDate.getMonth() + 1;
|
||||
const year = parsedDate.getFullYear();
|
||||
const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
|
||||
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
|
||||
});
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **EXCELLENT** - Data processing yang comprehensive!
|
||||
|
||||
---
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk semua forms
|
||||
- ✅ Required field validation
|
||||
- ✅ Multiple dropdown dependencies (Jenis Kelamin, Rating, Umur)
|
||||
- ✅ Loading state handling untuk dropdown data
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~10-16
|
||||
const templateResponden = z.object({
|
||||
name: z.string().min(1, "Nama harus diisi"),
|
||||
tanggal: z.string().min(1, "Tanggal harus diisi"),
|
||||
jenisKelaminId: z.string().min(1, "Jenis kelamin harus diisi"),
|
||||
ratingId: z.string().min(1, "Rating harus diisi"),
|
||||
kelompokUmurId: z.string().min(1, "Kelompok umur harus diisi"),
|
||||
});
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Validation yang proper!
|
||||
|
||||
---
|
||||
|
||||
### **4. State Management**
|
||||
- ✅ Proper typing dengan Prisma types (untuk findUnique)
|
||||
- ✅ Loading state management dengan finally block
|
||||
- ✅ Error handling yang comprehensive
|
||||
- ✅ **ApiFetch consistency** untuk create & findMany! ✅
|
||||
- ✅ Multiple related states (responden, jenisKelamin, rating, umur)
|
||||
- ✅ Reusable Select component di edit page
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~60-95
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
responden.findMany.loading = true; // ✅ Start loading
|
||||
responden.findMany.page = page;
|
||||
responden.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.landingpage.responden["findMany"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
responden.findMany.data = res.data.data || [];
|
||||
responden.findMany.total = res.data.total || 0;
|
||||
responden.findMany.totalPages = res.data.totalPages || 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading responden:", error);
|
||||
responden.findMany.data = [];
|
||||
responden.findMany.total = 0;
|
||||
responden.findMany.totalPages = 1;
|
||||
} finally {
|
||||
responden.findMany.loading = false; // ✅ Stop loading
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - State management sudah proper 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
|
||||
- ✅ Reusable ControlledSelect component
|
||||
- ✅ Error display untuk setiap field
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~40-60
|
||||
const [formData, setFormData] = useState<FormResponden>({
|
||||
name: '',
|
||||
tanggal: '',
|
||||
jenisKelaminId: '',
|
||||
ratingId: '',
|
||||
kelompokUmurId: '',
|
||||
});
|
||||
|
||||
const [originalData, setOriginalData] = useState<FormResponden>({
|
||||
name: '',
|
||||
tanggal: '',
|
||||
jenisKelaminId: '',
|
||||
ratingId: '',
|
||||
kelompokUmurId: '',
|
||||
});
|
||||
|
||||
// Load data
|
||||
const data = await state.update.load(id);
|
||||
setFormData(newForm);
|
||||
setOriginalData(newForm); // ✅ Save original
|
||||
|
||||
// Line ~130 - Handle reset
|
||||
const handleResetForm = () => {
|
||||
setFormData({ ...originalData });
|
||||
toast.info('Form dikembalikan ke data awal');
|
||||
};
|
||||
|
||||
// Line ~150 - Reusable Select component
|
||||
const ControlledSelect = ({
|
||||
label, value, onChange, options, error, loading,
|
||||
}) => (
|
||||
<Select
|
||||
label={<Text fw="bold" fz="sm" mb={4}>{label}</Text>}
|
||||
value={value}
|
||||
onChange={(val) => onChange(val || '')}
|
||||
data={options}
|
||||
disabled={loading}
|
||||
clearable
|
||||
searchable
|
||||
required
|
||||
radius="md"
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **EXCELLENT** - Best edit form implementation dengan reusable component!
|
||||
|
||||
---
|
||||
|
||||
### **6. Master Data Management**
|
||||
- ✅ 3 master data tables: Jenis Kelamin, Rating, Kelompok Umur
|
||||
- ✅ Separate proxy states untuk masing-masing
|
||||
- ✅ Auto-load saat create/edit form
|
||||
- ✅ Proper filtering dan mapping untuk dropdown options
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - deletedAt Default Value SALAH (5 MODELS AFFECTED!)**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 266-297)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model Responden {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ SALAH
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model JenisKelaminResponden {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ SALAH
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model PilihanRatingResponden {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ SALAH
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model UmurResponden {
|
||||
// ...
|
||||
deletedAt DateTime @default(now()) // ❌ SALAH
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- **LOGIC ERROR!** Setiap record baru langsung punya `deletedAt` value
|
||||
- Soft delete tidak berfungsi dengan benar
|
||||
- Query dengan `where: { deletedAt: null }` tidak akan pernah return data
|
||||
- **5 models affected!** (Responden + 3 master data + StrukturPPID)
|
||||
|
||||
**Rekomendasi:** Fix semua schema:
|
||||
```prisma
|
||||
model Responden {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null) // ✅ Nullable
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model JenisKelaminResponden {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model PilihanRatingResponden {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model UmurResponden {
|
||||
// ...
|
||||
deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 **CRITICAL**
|
||||
**Effort:** Medium (perlu migration untuk 5 models)
|
||||
**Impact:** **HIGH** (data integrity & soft delete logic)
|
||||
|
||||
---
|
||||
|
||||
#### **2. State Management - Fetch Pattern Inconsistency**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany)
|
||||
const res = await ApiFetch.api.landingpage.responden["create"].post(form);
|
||||
const res = await ApiFetch.api.landingpage.responden["findMany"].get({ query });
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique, update)
|
||||
const res = await fetch(`/api/landingpage/responden/${id}`);
|
||||
const response = await fetch(`/api/landingpage/responden/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await ApiFetch.api.landingpage.responden[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
responden.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, update methods)
|
||||
|
||||
---
|
||||
|
||||
#### **3. Type Safety - Any Usage di findMany**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~58
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~270
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~370
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
|
||||
// Line ~470
|
||||
data: null as any[] | null, // ❌ Using 'any'
|
||||
```
|
||||
|
||||
**Issue:** findMany data tidak typed dengan Prisma types, hanya findUnique yang typed.
|
||||
|
||||
**Rekomendasi:** Gunakan typed data:
|
||||
|
||||
```typescript
|
||||
// Define type
|
||||
type RespondenWithRelations = Prisma.RespondenGetPayload<{
|
||||
include: {
|
||||
jenisKelamin: true;
|
||||
rating: true;
|
||||
kelompokUmur: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
// Use typed data
|
||||
data: null as RespondenWithRelations[] | null,
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Console.log di Production**
|
||||
|
||||
**Lokasi:** Multiple places di state file
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~80
|
||||
console.error("Failed to load responden:", res.data?.message);
|
||||
|
||||
// Line ~85
|
||||
console.error("Error loading responden:", error);
|
||||
|
||||
// Line ~110
|
||||
console.error("Failed to fetch data", res.status, res.statusText);
|
||||
|
||||
// Line ~114
|
||||
console.error("Error loading responden:", error);
|
||||
|
||||
// ... dan banyak lagi di semua master data states
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **5. Missing Loading State di Submit Button**
|
||||
|
||||
**Lokasi:** `create/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~100-110
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
// ...
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Loading state sudah ada di create page!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **6. Missing Loading State di Edit Submit Button**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~220-230
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
// ⚠️ Missing state.update.loading check
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Issue:** Button tidak check `state.update.loading` dari global state.
|
||||
|
||||
**Rekomendasi:** Check both states:
|
||||
```typescript
|
||||
disabled={!isFormValid() || isSubmitting || state.update.loading}
|
||||
{isSubmitting || state.update.loading ? (
|
||||
<Loader size="sm" color="white" />
|
||||
) : (
|
||||
'Simpan'
|
||||
)}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Pagination onChange Tidak Include Search**
|
||||
|
||||
**Lokasi:** `responden/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~200-210
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10); // ⚠️ Missing search parameter
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Issue:** Saat ganti page, search query hilang.
|
||||
|
||||
**Rekomendasi:** Include search:
|
||||
```typescript
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, debouncedSearch); // ✅ Include search
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **8. Missing Delete Function di Master Data**
|
||||
|
||||
**Lokasi:** State file untuk master data
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~270-290 (jenisKelaminResponden)
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
// ✅ Method sudah ada
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Delete function sudah ada di semua master data!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **9. Duplicate Loading State Assignment**
|
||||
|
||||
**Lokasi:** State file untuk master data
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~290-295 (jenisKelaminResponden.create)
|
||||
async create() {
|
||||
// ...
|
||||
jenisKelaminResponden.create.loading = true; // ✅ First assignment
|
||||
try {
|
||||
jenisKelaminResponden.create.loading = true; // ❌ Duplicate!
|
||||
const res = await ApiFetch.api.landingpage.jeniskelaminresponden["create"].post(form);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Remove duplicate:
|
||||
```typescript
|
||||
async create() {
|
||||
// ...
|
||||
jenisKelaminResponden.create.loading = true; // ✅ Keep only this
|
||||
try {
|
||||
// Remove duplicate line
|
||||
const res = await ApiFetch.api.landingpage.jeniskelaminresponden["create"].post(form);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low (ada di 3 master data states)
|
||||
|
||||
---
|
||||
|
||||
#### **10. Inconsistent Toast Messages**
|
||||
|
||||
**Lokasi:** State file
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~45 (responden.create)
|
||||
toast.success("Responden berhasil ditambahkan");
|
||||
|
||||
// Line ~295 (jenisKelaminResponden.create)
|
||||
toast.success("Jenis kelamin responden berhasil ditambahkan");
|
||||
|
||||
// Line ~400 (pilihanRatingResponden.create)
|
||||
toast.success("Jenis kelamin responden berhasil ditambahkan"); // ❌ Wrong message!
|
||||
|
||||
// Line ~505 (kelompokUmurResponden.create)
|
||||
toast.success("Kelompok umur responden berhasil ditambahkan");
|
||||
```
|
||||
|
||||
**Issue:** Copy-paste error di pilihanRatingResponden (masih "Jenis kelamin responden").
|
||||
|
||||
**Rekomendasi:** Fix message:
|
||||
```typescript
|
||||
toast.success("Pilihan rating responden berhasil ditambahkan");
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Missing Edit Page untuk Master Data**
|
||||
|
||||
**Lokasi:** Module structure
|
||||
|
||||
**Masalah:**
|
||||
- ✅ Responden: Create, Edit, Detail, Delete
|
||||
- ❌ Jenis Kelamin: Create, Delete (NO EDIT)
|
||||
- ❌ Rating: Create, Delete (NO EDIT)
|
||||
- ❌ Kelompok Umur: Create, Delete (NO EDIT)
|
||||
|
||||
**Issue:** Master data tidak bisa diedit, hanya bisa delete & create ulang.
|
||||
|
||||
**Rekomendasi:** Consider adding edit pages untuk master data jika diperlukan:
|
||||
```typescript
|
||||
// Add edit method di state (sudah ada)
|
||||
// Add edit page di UI
|
||||
/admin/ppid/indeks-kepuasan-masyarakat/jenis-kelamin/[id]/edit
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low (business decision)
|
||||
**Effort:** Medium
|
||||
|
||||
---
|
||||
|
||||
#### **12. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:** `responden/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~30-35
|
||||
<HeaderSearch
|
||||
title="Data Responden"
|
||||
placeholder="Cari nama responden..." // ✅ Actually pretty specific!
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Placeholder sudah spesifik!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **13. Chart Color Hardcoding**
|
||||
|
||||
**Lokasi:** `grafik-kepuasan-masyarakat/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~55-60
|
||||
setDonutDataJenisKelamin([
|
||||
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
|
||||
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' }, // ❌ Hardcoded
|
||||
]);
|
||||
|
||||
setDonutDataRating([
|
||||
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
|
||||
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' }, // ❌ Hardcoded
|
||||
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' }, // ❌ Hardcoded
|
||||
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' }, // ❌ Hardcoded
|
||||
]);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Define color constants:
|
||||
```typescript
|
||||
// con/colors.ts atau file terpisah
|
||||
export const chartColors = {
|
||||
primary: colors['blue-button'],
|
||||
success: '#10A85AFF',
|
||||
warning: '#FFA500',
|
||||
danger: '#FF4500',
|
||||
};
|
||||
|
||||
// Use in chart data
|
||||
{ name: 'Perempuan', value: totalPerempuan, color: chartColors.success },
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **14. Date Parsing di Detail Page**
|
||||
|
||||
**Lokasi:** `responden/[id]/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~65-70
|
||||
<Text fz="md" c="dimmed">{
|
||||
stateDetail.findUnique.data?.tanggal
|
||||
? new Date(stateDetail.findUnique.data.tanggal).toLocaleDateString('id-ID')
|
||||
: '-'
|
||||
}</Text>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Date formatting yang proper!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | **Schema deletedAt default SALAH (5 models)** | Schema | **CRITICAL** | Medium | **MUST FIX** |
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Missing loading state di edit submit | UI | Low | Low | Should fix |
|
||||
| 🟡 M | Pagination missing search param | UI | Low | Low | Should fix |
|
||||
| 🟢 L | Duplicate loading state assignment | State | Low | Low | Optional |
|
||||
| 🟢 L | Inconsistent toast messages | State | Low | Low | Should fix |
|
||||
| 🟢 L | Missing edit page untuk master data | UI | Low | Medium | Optional |
|
||||
| 🟢 L | Chart color hardcoding | UI | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (8/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ **Grafik & Charts EXCELLENT** - Best chart implementation di semua modul PPID!
|
||||
2. ✅ **Data processing comprehensive** - Automatic calculation dari data responden
|
||||
3. ✅ **3 Distribusi Charts** - Jenis Kelamin, Penilaian, Kelompok Umur
|
||||
4. ✅ **Bar Chart Tren** - Monthly respondent trends
|
||||
5. ✅ UI/UX clean & responsive
|
||||
6. ✅ Form validation comprehensive
|
||||
7. ✅ State management dengan ApiFetch untuk create & findMany
|
||||
8. ✅ **Edit form EXCELLENT** - Reusable ControlledSelect component
|
||||
9. ✅ Original data tracking untuk reset form
|
||||
10. ✅ Master data management proper (3 tables)
|
||||
11. ✅ Loading state management dengan finally block
|
||||
12. ✅ Mobile cards responsive
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **Schema deletedAt default SALAH** - 5 models affected (CRITICAL)
|
||||
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
3. ⚠️ Type safety (any usage di findMany)
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Fix schema deletedAt** untuk 5 models dari `@default(now())` ke `@default(null)` dengan nullable
|
||||
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. ⚠️ **Improve type safety** dengan remove `any` usage
|
||||
4. ⚠️ **Add loading state** di edit submit button
|
||||
5. ⚠️ **Fix duplicate loading state** di master data create methods
|
||||
6. ⚠️ **Fix copy-paste toast message** di pilihanRatingResponden
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Fix schema deletedAt** untuk 5 models - 1 jam (perlu migration)
|
||||
2. **🔴 HIGH: Refactor findUnique, update** ke ApiFetch - 1 jam
|
||||
3. **🟡 MEDIUM: Improve type safety** - 30 menit
|
||||
4. **🟡 MEDIUM: Add loading state** di edit submit - 10 menit
|
||||
5. **🟡 MEDIUM: Fix pagination search param** - 10 menit
|
||||
6. **🟢 LOW: Fix duplicate loading state** - 15 menit
|
||||
7. **🟢 LOW: Fix toast message** - 5 menit
|
||||
8. **🟢 LOW: Define chart color constants** - 15 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Module | Charts | Data Processing | Edit Form | State | Schema | Overall |
|
||||
|--------|--------|----------------|-----------|-------|--------|---------|
|
||||
| Profil | ❌ None | N/A | ✅ Good | ⚠️ Good | ⚠️ deletedAt | 🟢 |
|
||||
| Desa Anti Korupsi | ❌ None | N/A | ✅ Good | ⚠️ Good | ⚠️ deletedAt | 🟢 |
|
||||
| SDGs Desa | ❌ None | N/A | ✅ Good | ⚠️ Good | ⚠️ deletedAt | 🟢 |
|
||||
| APBDes | ❌ None | ✅ Items hierarchy | ✅ Good | ⚠️ Good | ✅ Good | 🟢 |
|
||||
| Prestasi Desa | ❌ None | N/A | ✅ Good | ⚠️ Good | ❌ WRONG | 🟢 |
|
||||
| PPID Profil | ❌ None | N/A | ✅ **Excellent** | ✅ **Best** | ❌ WRONG | 🟢⭐ |
|
||||
| Struktur PPID | ❌ None | N/A | ✅ Good | ✅ Good | ⚠️ Inconsistent | 🟢 |
|
||||
| Visi Misi PPID | ❌ None | N/A | ✅ Good | ✅ **Best** | ❌ WRONG | 🟢⭐⭐ |
|
||||
| Dasar Hukum PPID | ❌ None | N/A | ✅ Good | ✅ **Best** | ❌ WRONG | 🟢⭐⭐ |
|
||||
| Permohonan Informasi | ❌ None | N/A | ❌ Missing | ⚠️ Good | ❌ **4 models WRONG** | 🟡 |
|
||||
| Permohonan Keberatan | ❌ None | N/A | ❌ Missing | ⚠️ Good | ❌ WRONG | 🟡 |
|
||||
| Daftar Informasi | ❌ None | N/A | ✅ Good | ⚠️ Good | ❌ WRONG | 🟢 |
|
||||
| **IKM (Indeks Kepuasan)** | ✅ **EXCELLENT** | ✅ **EXCELLENT** | ✅ **Excellent** | ⚠️ Good | ❌ **5 models WRONG** | 🟢 |
|
||||
|
||||
**IKM Highlights:**
|
||||
- ✅ **BEST CHARTS** - Mantine Charts (PieChart, BarChart)
|
||||
- ✅ **BEST DATA PROCESSING** - Automatic calculation & grouping
|
||||
- ✅ **BEST EDIT FORM** - Reusable ControlledSelect component
|
||||
- ⚠️ **5 models affected** - deletedAt issue (most affected module!)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF IKM MODULE
|
||||
|
||||
**Most Advanced Data Visualization:**
|
||||
1. ✅ **Mantine Charts** - PieChart & BarChart (UNIQUE!)
|
||||
2. ✅ **3 Distribusi Charts** - Jenis Kelamin, Penilaian, Kelompok Umur
|
||||
3. ✅ **Monthly Trend Chart** - Bar chart dengan grouping
|
||||
4. ✅ **Automatic Calculation** - Filter & count dari data
|
||||
5. ✅ **Reusable Select Component** - ControlledSelect di edit form
|
||||
6. ✅ **3 Master Data Tables** - Jenis Kelamin, Rating, Kelompok Umur
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ **Chart implementation** - Best practice untuk data visualization
|
||||
2. ✅ **Data processing** - Comprehensive calculation & grouping
|
||||
3. ✅ **Reusable components** - ControlledSelect untuk dropdowns
|
||||
4. ✅ **Loading state management** - Proper dengan finally block
|
||||
5. ✅ **Original data tracking** - Edit form reset yang proper
|
||||
6. ✅ **Master data management** - Separate states untuk masing-masing
|
||||
|
||||
**Critical Issues:**
|
||||
1. ❌ **5 models dengan deletedAt SALAH** - Most affected module!
|
||||
2. ❌ **Fetch pattern inconsistency** - findUnique, update pakai fetch manual
|
||||
3. ❌ **Type safety** - any usage di findMany
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** **IKM adalah MODULE DENGAN CHARTS & DATA VISUALIZATION TERBAIK** dengan Mantine Charts implementation yang excellent. Module ini juga punya **BEST EDIT FORM** dengan reusable ControlledSelect component. Tapi juga **MODULE DENGAN PALING BANYAK MODEL AFFECTED** oleh deletedAt issue (5 models!).
|
||||
|
||||
**Unique Strengths:**
|
||||
1. ✅ **Charts EXCELLENT** - Best data visualization
|
||||
2. ✅ **Data processing** - Automatic calculation & grouping
|
||||
3. ✅ **Edit form EXCELLENT** - Reusable ControlledSelect
|
||||
4. ✅ **Master data management** - 3 separate tables
|
||||
5. ✅ **Monthly trends** - Bar chart dengan grouping
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (1 JAM + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 266-297
|
||||
|
||||
# Fix 5 models:
|
||||
|
||||
model Responden {
|
||||
// ...
|
||||
- deletedAt DateTime @default(now())
|
||||
+ deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model JenisKelaminResponden {
|
||||
// ...
|
||||
- deletedAt DateTime @default(now())
|
||||
+ deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model PilihanRatingResponden {
|
||||
// ...
|
||||
- deletedAt DateTime @default(now())
|
||||
+ deletedAt DateTime? @default(null)
|
||||
isActive Boolean @default(true)
|
||||
}
|
||||
|
||||
model UmurResponden {
|
||||
// ...
|
||||
- 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_ikm
|
||||
```
|
||||
|
||||
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST CHARTS & DATA VISUALIZATION**! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**IKM Module adalah BEST PRACTICE untuk:**
|
||||
1. ✅ **Charts & Data Visualization** - Mantine Charts implementation
|
||||
2. ✅ **Data Processing** - Automatic calculation & grouping
|
||||
3. ✅ **Reusable Components** - ControlledSelect untuk dropdowns
|
||||
4. ✅ **Edit Form** - Original data tracking dengan reusable components
|
||||
5. ✅ **Master Data Management** - Separate states untuk multiple tables
|
||||
|
||||
**Modules lain bisa belajar dari IKM:**
|
||||
- **ALL MODULES WITH CHARTS:** Use Mantine Charts (PieChart, BarChart)
|
||||
- **ALL MODULES WITH DROPDOWNS:** Use reusable ControlledSelect component
|
||||
- **ALL MODULES:** Automatic data calculation untuk charts
|
||||
- **ALL MODULES:** Master data management dengan separate states
|
||||
- **ALL MODULES:** Edit form dengan original data tracking
|
||||
|
||||
---
|
||||
|
||||
**File Location:** `QC/PPID/QC-IKM-MODULE.md` 📄
|
||||
844
QC/PPID/QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md
Normal file
844
QC/PPID/QC-PERMOHONAN-INFORMASI-PUBLIK-MODULE.md
Normal file
@@ -0,0 +1,844 @@
|
||||
# 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):**
|
||||
```typescript
|
||||
// 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):**
|
||||
```typescript
|
||||
// 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):**
|
||||
```typescript
|
||||
// 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!
|
||||
|
||||
---
|
||||
|
||||
### **5. Related Data Management**
|
||||
- ✅ Separate proxy states untuk dropdown data
|
||||
- ✅ JenisInformasiDiminta, CaraMemperolehInformasi, CaraMemperolehSalinanInformasi
|
||||
- ✅ Proper typing dengan Prisma types
|
||||
- ✅ ApiFetch consistency untuk load dropdown data
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// 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:**
|
||||
```prisma
|
||||
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:
|
||||
```prisma
|
||||
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:
|
||||
|
||||
```typescript
|
||||
// ❌ 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:
|
||||
|
||||
```typescript
|
||||
// ✅ 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:**
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
```prisma
|
||||
// Add to schema
|
||||
model PermohonanInformasiPublik {
|
||||
// ...
|
||||
status String @default("pending") // pending, processed, completed
|
||||
processedAt DateTime?
|
||||
processedBy String?
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// Line ~145
|
||||
const query: any = { page, limit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed query:
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
#### **7. Pagination onChange Tidak Include Search**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
console.error('Failed to load Permohonan Informasi Publik:', err);
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~70, 110
|
||||
<TextInput
|
||||
placeholder={"Cari nama..."} // ⚠️ Generic
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Rekomendasi:** Lebih spesifik:
|
||||
```typescript
|
||||
placeholder={"Cari nama pemohon..."}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Missing Data Relationships di Detail Page**
|
||||
|
||||
**Lokasi:** `[id]/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
<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:**
|
||||
```typescript
|
||||
// Line ~70
|
||||
console.log(caraMemperolehSalinanInformasi); // ❌ Debug log yang tidak terpakai
|
||||
```
|
||||
|
||||
**Rekomendasi:** Remove:
|
||||
```typescript
|
||||
// Remove this line completely
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **13. Missing Empty State Icon di Mobile**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// 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:**
|
||||
```diff
|
||||
🔴 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
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 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**! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**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` 📄
|
||||
771
QC/PPID/QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md
Normal file
771
QC/PPID/QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md
Normal file
@@ -0,0 +1,771 @@
|
||||
# QC Summary - Permohonan Keberatan Informasi Publik PPID Module
|
||||
|
||||
**Scope:** List Permohonan Keberatan, Detail Permohonan Keberatan
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Aspect | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| Permohonan Keberatan | ⚠️ 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, Mail, Phone, Info) untuk visual clarity
|
||||
- ✅ Consistent empty state messages
|
||||
|
||||
### **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
|
||||
- ✅ Proper spacing dan gap untuk readability
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// page.tsx - Line ~130-180
|
||||
<Table highlightOnHover
|
||||
layout="fixed" // ✅ PENTING - consistent column widths
|
||||
withColumnBorders={false}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh fz="sm" fw={600} lh={1.4} ta="center">No</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
<Group gap={5}>
|
||||
<IconUser size={16} />
|
||||
Nama
|
||||
</Group>
|
||||
</TableTh>
|
||||
<TableTh fz="sm" fw={600} lh={1.4}>
|
||||
<Group gap={5}>
|
||||
<IconMail size={16} />
|
||||
Email
|
||||
</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
|
||||
- ✅ Return boolean untuk create operation (success/failure handling)
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// state file - Line ~30-55
|
||||
create: {
|
||||
form: {} as PermohonanKeberatanInformasiForm,
|
||||
loading: false,
|
||||
async create() {
|
||||
const cek = templateForm.safeParse(permohonanKeberatanInformasi.create.form);
|
||||
if (!cek.success) {
|
||||
toast.error(cek.error.issues.map((i) => i.message).join("\n"));
|
||||
return false; // ✅ GOOD - Return false untuk failure
|
||||
}
|
||||
try {
|
||||
permohonanKeberatanInformasi.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(form);
|
||||
|
||||
if (res.data?.success === false) {
|
||||
toast.error(res.data?.message);
|
||||
return false; // ✅ GOOD - Return false untuk API failure
|
||||
}
|
||||
|
||||
toast.success("Sukses menambahkan");
|
||||
return true; // ✅ GOOD - Return true untuk success
|
||||
} catch {
|
||||
toast.error("Terjadi kesalahan server");
|
||||
return false;
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.create.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **EXCELLENT** - Proper return value handling untuk create operation!
|
||||
|
||||
---
|
||||
|
||||
### **4. Zod Schema Validation**
|
||||
- ✅ Comprehensive validation untuk semua fields
|
||||
- ✅ Specific error messages untuk setiap field
|
||||
- ✅ Phone number length validation (3-15 chars)
|
||||
- ✅ Minimum character validation (3 characters)
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~8-15
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||
email: z.string().min(3, "Email minimal 3 karakter"),
|
||||
notelp: z
|
||||
.string()
|
||||
.min(3, "Nomor Telepon minimal 3 karakter")
|
||||
.max(15, "Nomor Telepon maksimal 15 angka"), // ✅ Specific validation
|
||||
alasan: z.string().min(3, "Alasan minimal 3 karakter"),
|
||||
});
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Validation yang proper dengan specific rules!
|
||||
|
||||
---
|
||||
|
||||
### **5. Empty State Handling**
|
||||
- ✅ Different messages untuk search vs empty data
|
||||
- ✅ Icon integration untuk visual clarity
|
||||
- ✅ Proper text formatting dan centering
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// page.tsx - Line ~70-85
|
||||
<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 keberatan yang tercatat'
|
||||
}
|
||||
</Text>
|
||||
</Stack>
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Empty state dengan conditional messages yang helpful!
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - deletedAt Default Value SALAH**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 478)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model FormulirPermohonanKeberatan {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String
|
||||
notelp String
|
||||
alasan String
|
||||
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
|
||||
|
||||
**Rekomendasi:** Fix schema:
|
||||
```prisma
|
||||
model FormulirPermohonanKeberatan {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String
|
||||
notelp String
|
||||
alasan String
|
||||
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. State Management - Fetch Pattern Inconsistency**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany)
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(form);
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["find-many"].get({ query });
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique)
|
||||
const res = await fetch(`/api/ppid/permohonankeberataninformasipublik/${id}`);
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
permohonanKeberatanInformasi.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. Missing Delete Function**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// state file - Line ~100-120
|
||||
// ❌ MISSING: delete method
|
||||
const permohonanKeberatanInformasi = proxy({
|
||||
create: { ... },
|
||||
findMany: { ... },
|
||||
findUnique: { ... },
|
||||
// ❌ NO delete method!
|
||||
});
|
||||
```
|
||||
|
||||
**Issue:** Tidak ada cara untuk menghapus data permohonan keberatan.
|
||||
|
||||
**Rekomendasi:** Add delete method:
|
||||
```typescript
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
try {
|
||||
permohonanKeberatanInformasi.delete.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["del"][id].delete();
|
||||
|
||||
if (res.data?.success) {
|
||||
toast.success(res.data.message || "Berhasil hapus permohonan keberatan");
|
||||
await permohonanKeberatanInformasi.findMany.load();
|
||||
} else {
|
||||
toast.error(res.data?.message || "Gagal hapus permohonan keberatan");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus");
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.delete.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Medium (perlu add method + API endpoint)
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Console.log di Production**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~85
|
||||
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||
|
||||
// Line ~90
|
||||
console.error("Error loading permohonan keberatan informasi:", error);
|
||||
|
||||
// Line ~110
|
||||
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
||||
|
||||
// Line ~114
|
||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **5. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/permohonan_keberatan_informasi_publik/permohonanKeberatanInformasi.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~75
|
||||
const query: any = { page, limit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed query:
|
||||
|
||||
```typescript
|
||||
// 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. Missing Edit Function**
|
||||
|
||||
**Lokasi:** Module structure
|
||||
|
||||
**Masalah:**
|
||||
- ❌ Tidak ada halaman edit untuk permohonan keberatan
|
||||
- ❌ Tidak ada edit method di state
|
||||
- ⚠️ **QUESTION:** Apakah permohonan keberatan harus bisa diedit?
|
||||
|
||||
**Issue:** Jika ada kesalahan input, user tidak bisa mengoreksi data.
|
||||
|
||||
**Rekomendasi:** Consider adding edit functionality jika diperlukan:
|
||||
```typescript
|
||||
// Add edit method di state
|
||||
edit: {
|
||||
id: "",
|
||||
form: { ... },
|
||||
loading: false,
|
||||
async load(id: string) { ... },
|
||||
async update() { ... },
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low (depends on business requirement)
|
||||
**Effort:** Medium
|
||||
|
||||
---
|
||||
|
||||
#### **7. Pagination onChange Tidak Include Search**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// 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:
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// state file - Line ~85-90
|
||||
console.error("Failed to load permohonan keberatan informasi:", res.data?.message);
|
||||
console.error("Error loading permohonan keberatan informasi:", error);
|
||||
|
||||
// state file - Line ~110-114
|
||||
console.error("Failed to fetch permohonan keberatan informasi:", res.statusText);
|
||||
console.error("Error fetching permohonan keberatan informasi:", error);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Cukup satu logging yang informatif:
|
||||
```typescript
|
||||
console.error('Failed to load Permohonan Keberatan:', err);
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~70, 110
|
||||
<TextInput
|
||||
placeholder={"Cari nama..."} // ⚠️ Generic
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
**Rekomendasi:** Lebih spesifik:
|
||||
```typescript
|
||||
placeholder={"Cari nama pemohon..."}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Missing Data di Detail Page**
|
||||
|
||||
**Lokasi:** `[id]/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~50-80
|
||||
// Menampilkan: name, notelp, email, alasan
|
||||
// ❌ MISSING: createdAt, updatedAt, atau status
|
||||
```
|
||||
|
||||
**Issue:** Tidak menampilkan timestamp atau status permohonan.
|
||||
|
||||
**Rekomendasi:** Add missing fields jika ada di schema:
|
||||
```typescript
|
||||
<Box>
|
||||
<Text fz="lg" fw="bold" mb={4}>Tanggal Pengajuan</Text>
|
||||
<Text fz="md" c="dimmed">
|
||||
{data.createdAt ? new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}) : '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Title Inconsistency di Detail Page**
|
||||
|
||||
**Lokasi:** `[id]/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~40
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Informasi Publik // ⚠️ Generic title
|
||||
</Text>
|
||||
```
|
||||
|
||||
**Issue:** Title seharusnya lebih spesifik "Detail Permohonan Keberatan".
|
||||
|
||||
**Rekomendasi:** Fix title:
|
||||
```typescript
|
||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
||||
Detail Permohonan Keberatan Informasi Publik
|
||||
</Text>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | **Schema deletedAt default SALAH** | Schema | **CRITICAL** | Medium | **MUST FIX** |
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P1 | Missing delete function | State | Medium | Medium | Should add |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
|
||||
| 🟡 M | Missing edit function | State/UI | Low | Medium | Optional (business decision) |
|
||||
| 🟡 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 data di detail page | UI | Low | Low | Optional |
|
||||
| 🟢 L | Title inconsistency di detail page | UI | Low | Low | Should fix |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 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 (conditional messages)
|
||||
5. ✅ **Zod validation** comprehensive dengan specific rules
|
||||
6. ✅ **Proper return value handling** untuk create operation (return true/false)
|
||||
7. ✅ State management dengan ApiFetch untuk create & findMany
|
||||
8. ✅ Loading state management dengan finally block
|
||||
9. ✅ Mobile cards responsive
|
||||
10. ✅ Icon integration (User, Mail, Phone, Info)
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
|
||||
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
3. ⚠️ Missing delete function untuk hapus data
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Fix schema deletedAt** dari `@default(now())` ke `@default(null)` dengan nullable
|
||||
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. ⚠️ **Add delete method** untuk hapus data
|
||||
4. ⚠️ **Consider adding edit functionality** (business decision)
|
||||
5. ⚠️ **Improve type safety** dengan remove `any` usage
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
|
||||
2. **🔴 HIGH: Refactor findUnique** ke ApiFetch - 30 menit
|
||||
3. **🔴 HIGH: Add delete method** - 45 menit
|
||||
4. **🟡 MEDIUM: Add pagination search param** - 10 menit
|
||||
5. **🟢 LOW: Fix title di detail page** - 5 menit
|
||||
6. **🟢 LOW: Polish minor issues** - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Module | Fetch Pattern | State | Validation | Schema | Delete | Edit | Overall |
|
||||
|--------|--------------|-------|------------|--------|--------|------|---------|
|
||||
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ✅ Yes | ✅ Yes | 🟢 |
|
||||
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ✅ Yes | ✅ Yes | 🟢 |
|
||||
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ⚠️ deletedAt | ✅ Yes | ✅ Yes | 🟢 |
|
||||
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Good | ✅ Yes | ✅ Yes | 🟢 |
|
||||
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ✅ Yes | ✅ Yes | 🟢 |
|
||||
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ Good | ❌ WRONG | N/A | ✅ Yes | 🟢⭐ |
|
||||
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ⚠️ Inconsistent | ✅ Yes | ✅ Yes | 🟢 |
|
||||
| Visi Misi PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | N/A | ✅ Yes | 🟢⭐⭐ |
|
||||
| Dasar Hukum PPID | ✅ **100% ApiFetch!** | ✅ Best | ✅ Good | ❌ WRONG | N/A | ✅ Yes | 🟢⭐⭐ |
|
||||
| Permohonan Informasi | ⚠️ Mixed | ⚠️ Good | ✅ **Best** | ❌ **4 models WRONG** | ❌ Missing | ❌ Missing | 🟡 |
|
||||
| **Permohonan Keberatan** | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ WRONG | ❌ **MISSING** | ❌ **MISSING** | 🟡 |
|
||||
|
||||
**Permohonan Keberatan PPID Highlights:**
|
||||
- ✅ **Proper return value handling** - Return true/false untuk create operation
|
||||
- ✅ **Icon integration** - User, Mail, Phone, Info icons di table headers
|
||||
- ✅ **Conditional empty state messages** - Different messages untuk search vs empty
|
||||
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
|
||||
- ⚠️ **Missing delete function** - Cannot delete data
|
||||
- ⚠️ **Missing edit function** - Cannot edit data (same as Permohonan Informasi)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF PERMOHONAN KEBERATAN MODULE
|
||||
|
||||
**Simplest Read-Only Module:**
|
||||
1. ✅ **Proper return value handling** - Return true/false untuk create operation (UNIQUE!)
|
||||
2. ✅ **Conditional empty state messages** - Different messages untuk search vs empty
|
||||
3. ✅ **Icon integration** - User, Mail, Phone, Info icons
|
||||
4. ❌ **Missing delete function** - Cannot delete data
|
||||
5. ❌ **Missing edit function** - Cannot edit data
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ **Return value handling** - Best practice untuk create operation
|
||||
2. ✅ **Conditional empty state** - Good UX untuk search feedback
|
||||
3. ✅ **Loading state management** - Proper dengan finally block
|
||||
4. ✅ **Icon integration** - Visual clarity di table headers
|
||||
|
||||
**Critical Issues:**
|
||||
1. ❌ **Schema deletedAt SALAH** - Same issue seperti modul PPID lain
|
||||
2. ❌ **Fetch pattern inconsistency** - findUnique pakai fetch manual
|
||||
3. ❌ **Missing delete function** - Cannot delete data
|
||||
4. ❌ **Missing edit function** - Cannot edit data (same as Permohonan Informasi)
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** **Permohonan Keberatan PPID adalah MODULE DENGAN RETURN VALUE HANDLING TERBAIK** tapi juga **MISSING DELETE & EDIT FUNCTIONS**. Module ini mirip dengan Permohonan Informasi (read-only, no delete/edit).
|
||||
|
||||
**Unique Strengths:**
|
||||
1. ✅ **Return value handling** - Best practice (return true/false)
|
||||
2. ✅ **Conditional empty state** - Good UX
|
||||
3. ✅ **Icon integration** - Visual clarity
|
||||
4. ✅ **Validation comprehensive** - Phone length validation
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 478
|
||||
|
||||
model FormulirPermohonanKeberatan {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String
|
||||
notelp String
|
||||
alasan String
|
||||
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_keberatan
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 ADD DELETE FUNCTION (45 MENIT):
|
||||
File: state file
|
||||
|
||||
delete: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
try {
|
||||
permohonanKeberatanInformasi.delete.loading = true;
|
||||
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["del"][id].delete();
|
||||
|
||||
if (res.data?.success) {
|
||||
toast.success(res.data.message || "Berhasil hapus permohonan keberatan");
|
||||
await permohonanKeberatanInformasi.findMany.load();
|
||||
} else {
|
||||
toast.error(res.data?.message || "Gagal hapus permohonan keberatan");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus");
|
||||
} finally {
|
||||
permohonanKeberatanInformasi.delete.loading = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Setelah fix critical issues, module ini **PRODUCTION-READY** dengan **BEST RETURN VALUE HANDLING**! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**Permohonan Keberatan PPID Module adalah BEST PRACTICE untuk:**
|
||||
1. ✅ **Return value handling** - Return true/false untuk create operation
|
||||
2. ✅ **Conditional empty state** - Different messages untuk search vs empty
|
||||
3. ✅ **Icon integration** - Visual clarity di table headers
|
||||
4. ✅ **Phone validation** - Min/max length validation
|
||||
|
||||
**Modules lain bisa belajar dari Permohonan Keberatan:**
|
||||
- **ALL MODULES:** Use return values untuk handle create success/failure
|
||||
- **ALL MODULES:** Conditional empty state messages untuk better UX
|
||||
- **ALL MODULES:** Icon integration untuk visual clarity
|
||||
- **ALL MODULES:** Specific validation rules (min/max length)
|
||||
|
||||
---
|
||||
|
||||
**File Location:** `QC/PPID/QC-PERMOHONAN-KEBERATAN-INFORMASI-MODULE.md` 📄
|
||||
802
QC/PPID/QC-PPID-PROFIL-MODULE.md
Normal file
802
QC/PPID/QC-PPID-PROFIL-MODULE.md
Normal file
@@ -0,0 +1,802 @@
|
||||
# QC Summary - PPID Profil Module
|
||||
|
||||
**Scope:** Profil PPID (Preview & Edit), Rich Text Editor Forms
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Aspect | Schema | API | UI Admin | State Management | Overall |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| Profil 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
|
||||
- ✅ Error handling dengan Alert component
|
||||
- ✅ Empty state handling yang informatif
|
||||
- ✅ Edit button yang prominent
|
||||
|
||||
### **2. File Upload Handling**
|
||||
- ✅ Dropzone dengan preview image
|
||||
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
|
||||
- ✅ Validasi ukuran file (max 5MB)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
- ✅ Error handling untuk image load (onError fallback)
|
||||
|
||||
### **3. Rich Text Editor (Tiptap)**
|
||||
- ✅ Full-featured editor dengan toolbar lengkap
|
||||
- ✅ 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
|
||||
|
||||
### **4. Form Component Structure**
|
||||
- ✅ Modular form components (Biodata, Riwayat, Pengalaman, Unggulan)
|
||||
- ✅ Reusable EditPPIDEditor component
|
||||
- ✅ Proper TypeScript typing
|
||||
- ✅ Error display untuk setiap field
|
||||
- ✅ Controlled components dengan onChange handler
|
||||
|
||||
### **5. State Management - BEST PRACTICES**
|
||||
- ✅ Proper typing dengan Prisma types
|
||||
- ✅ Loading state management dengan finally block
|
||||
- ✅ Error handling yang comprehensive
|
||||
- ✅ Reset function untuk cleanup
|
||||
- ✅ **originalForm tracking** untuk reset ke data awal
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// state file - Line ~85-105
|
||||
editForm: {
|
||||
id: "",
|
||||
form: { ...defaultForm },
|
||||
originalForm: { ...defaultForm }, // ✅ Track original data
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
|
||||
initialize(profileData: ProfilePPIDForm) {
|
||||
this.id = profileData.id;
|
||||
const data = {
|
||||
name: profileData.name || "",
|
||||
biodata: profileData.biodata || "",
|
||||
riwayat: profileData.riwayat || "",
|
||||
pengalaman: profileData.pengalaman || "",
|
||||
unggulan: profileData.unggulan || "",
|
||||
imageId: profileData.imageId || "",
|
||||
};
|
||||
this.form = { ...data };
|
||||
this.originalForm = { ...data }; // ✅ Save original
|
||||
},
|
||||
|
||||
updateField(field: keyof typeof defaultForm, value: string) {
|
||||
this.form[field] = value;
|
||||
},
|
||||
|
||||
// ✅ Reset to original
|
||||
resetToOriginal() {
|
||||
this.form = { ...this.originalForm };
|
||||
toast.info("Data dikembalikan ke kondisi awal");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SANGAT BAIK** - State management paling baik dibanding modul lain!
|
||||
|
||||
---
|
||||
|
||||
### **6. Edit Form - Original Data Tracking**
|
||||
- ✅ Original data state untuk reset form
|
||||
- ✅ Load data existing dengan benar
|
||||
- ✅ Preview image dari data lama
|
||||
- ✅ Reset form mengembalikan ke data original
|
||||
- ✅ File replacement logic (upload baru jika ada perubahan)
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~100-115
|
||||
const handleResetForm = () => {
|
||||
if (!allState.profile.data) return;
|
||||
|
||||
// Reset form ke data awal yang di-load
|
||||
const original = allState.profile.data;
|
||||
|
||||
stateProfilePPID.editForm.form = {
|
||||
name: original.name || '',
|
||||
imageId: original.imageId || '',
|
||||
biodata: original.biodata || '',
|
||||
riwayat: original.riwayat || '',
|
||||
pengalaman: original.pengalaman || '',
|
||||
unggulan: original.unggulan || '',
|
||||
};
|
||||
|
||||
// Reset preview gambar juga
|
||||
setPreviewImage(original.image?.link || null);
|
||||
setFile(null);
|
||||
|
||||
toast.info('Perubahan dibatalkan');
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SANGAT BAIK** - Original data tracking sudah implementasi dengan sempurna!
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - deletedAt Default Value SALAH**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 401)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model ProfilePPID {
|
||||
// ...
|
||||
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:**
|
||||
```prisma
|
||||
// Record baru dibuat
|
||||
CREATE ProfilePPID {
|
||||
name: "PPID 1",
|
||||
// deletedAt otomatis ter-set ke now() ❌
|
||||
// isActive: true ✅
|
||||
}
|
||||
|
||||
// Query untuk data aktif (seharusnya return data ini)
|
||||
prisma.profilePPID.findMany({
|
||||
where: { deletedAt: null, isActive: true }
|
||||
})
|
||||
// ❌ Return kosong! Karena deletedAt sudah ter-set
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix schema:
|
||||
```prisma
|
||||
model ProfilePPID {
|
||||
// ...
|
||||
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:**
|
||||
```typescript
|
||||
// Line ~105-110
|
||||
<Text
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
ta="justify"
|
||||
c={colors['blue-button']}
|
||||
lh={1.5}
|
||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||
dangerouslySetInnerHTML={{ __html: item.biodata }} // ❌ No sanitization
|
||||
/>
|
||||
|
||||
// Line ~115-120 (Riwayat)
|
||||
dangerouslySetInnerHTML={{ __html: item.riwayat }} // ❌ No sanitization
|
||||
|
||||
// Line ~125-130 (Pengalaman)
|
||||
dangerouslySetInnerHTML={{ __html: item.pengalaman }} // ❌ No sanitization
|
||||
|
||||
// Line ~135-140 (Unggulan)
|
||||
dangerouslySetInnerHTML={{ __html: item.unggulan }} // ❌ No sanitization
|
||||
```
|
||||
|
||||
**Risk:**
|
||||
- XSS attack jika admin input script malicious
|
||||
- Bisa inject iframe, script tag, dll
|
||||
- Security vulnerability
|
||||
|
||||
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Sanitize sebelum render
|
||||
const sanitizedBiodata = DOMPurify.sanitize(item.biodata);
|
||||
const sanitizedRiwayat = DOMPurify.sanitize(item.riwayat);
|
||||
const sanitizedPengalaman = DOMPurify.sanitize(item.pengalaman);
|
||||
const sanitizedUnggulan = DOMPurify.sanitize(item.unggulan);
|
||||
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedBiodata }}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan (hanya `<p>`, `<ul>`, `<li>`, `<strong>`, dll).
|
||||
|
||||
**Priority:** 🔴 **HIGH** (**Security concern**)
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **3. State Management - Fetch Pattern Inconsistency**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: fetch manual (profile.load)
|
||||
const res = await fetch(`/api/ppid/profileppid/${id}`);
|
||||
|
||||
// ❌ Pattern 2: fetch manual (editForm.submit)
|
||||
const res = await fetch(`/api/ppid/profileppid/${this.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(this.form),
|
||||
});
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
- Tidak konsisten dengan modul lain yang sudah migrate ke ApiFetch
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
import ApiFetch from "@/lib/api-fetch";
|
||||
|
||||
// profile.load
|
||||
async load(id: string) {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
const res = await ApiFetch.api.ppid.profileppid[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
this.data = res.data.data;
|
||||
return res.data.data;
|
||||
} else {
|
||||
if (res.data?.message === "Data tidak ditemukan" ||
|
||||
res.data?.message === "Belum ada data profil PPID yang aktif") {
|
||||
this.error = res.data.message;
|
||||
return null;
|
||||
} else {
|
||||
throw new Error(res.data?.message || "Gagal memuat data profile");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
this.error = msg;
|
||||
console.error("Load profile error:", msg);
|
||||
if (msg !== "Data tidak ditemukan" && msg !== "Belum ada data profil PPID yang aktif") {
|
||||
toast.error("Gagal memuat data profile");
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// editForm.submit
|
||||
async submit() {
|
||||
const check = templateForm.safeParse(this.form);
|
||||
if (!check.success) {
|
||||
toast.error(
|
||||
check.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.profileppid[this.id].put(this.form);
|
||||
|
||||
if (res.data?.success) {
|
||||
toast.success("Berhasil update profile");
|
||||
this.originalForm = { ...this.form };
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(res.data?.message || "Gagal update profile");
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = (err as Error).message;
|
||||
this.error = msg;
|
||||
toast.error(msg);
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di semua methods)
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Console.log di Production**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~65
|
||||
console.error("Load profile error:", msg);
|
||||
|
||||
// edit/page.tsx - Line ~65
|
||||
console.error("Error updating profile:", error);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Load profile error:", msg);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **5. Zod Schema - Error Message Tidak Konsisten**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/profile_ppid/profile_PPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~6
|
||||
const templateForm = z.object({
|
||||
name: z.string().min(3, "Nama minimal 3 karakter"), // ✅ OK
|
||||
biodata: z.string().min(3, "Biodata minimal 3 karakter"), // ✅ OK
|
||||
riwayat: z.string().min(3, "Riwayat minimal 3 karakter"), // ✅ OK
|
||||
pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"), // ✅ OK
|
||||
unggulan: z.string().min(3, "Unggulan minimal 3 karakter"), // ✅ OK
|
||||
imageId: z.string().min(1, "Gambar wajib dipilih"), // ✅ OK
|
||||
});
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Error messages sudah spesifik dan konsisten!
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **6. Missing Validation di Submit Button**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~270-280
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
style={{ ... }}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Issue:** Button tidak disabled saat submitting atau form invalid. User bisa click multiple times.
|
||||
|
||||
**Rekomendasi:** Add disabled state:
|
||||
|
||||
```typescript
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={isSubmitting || allState.editForm.loading}
|
||||
style={{
|
||||
background: isSubmitting || allState.editForm.loading
|
||||
? 'linear-gradient(135deg, #cccccc, #999999)'
|
||||
: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **7. Duplicate useEffect di Editor Component**
|
||||
|
||||
**Lokasi:** `editPPIDEditor.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~25-30
|
||||
useEffect(() => {
|
||||
if (editor && value && value !== editor.getHTML()) {
|
||||
editor.commands.setContent(value);
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
// Line ~32-40
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const updateHandler = () => onChange(editor.getHTML());
|
||||
editor.on('update', updateHandler);
|
||||
|
||||
return () => {
|
||||
editor.off('update', updateHandler);
|
||||
};
|
||||
}, [editor, onChange]);
|
||||
```
|
||||
|
||||
**Issue:** Ada 2 useEffect yang handle editor update. Yang pertama set content, yang kedua handle onChange. Bisa digabung untuk clarity.
|
||||
|
||||
**Rekomendasi:** Simplify:
|
||||
|
||||
```typescript
|
||||
const editor = useEditor({
|
||||
extensions: [...],
|
||||
content: value, // Set content directly
|
||||
onUpdate({ editor }) {
|
||||
onChange(editor.getHTML());
|
||||
},
|
||||
});
|
||||
|
||||
// Remove first useEffect, keep second for cleanup
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **8. Form Label Inconsistency**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~170
|
||||
<Text fw="bold">Nama Perbekel</Text>
|
||||
|
||||
// Should be:
|
||||
<Text fw="bold">Nama PPID</Text>
|
||||
```
|
||||
|
||||
**Issue:** Label "Nama Perbekel" tidak sesuai dengan context PPID. Ini profil PPID, bukan perbekel.
|
||||
|
||||
**Rekomendasi:** Fix label:
|
||||
```typescript
|
||||
<Text fw="bold">Nama PPID</Text>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **9. Image Label Text Size**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~180
|
||||
<Text fz={"md"} fw={"bold"}>Gambar</Text>
|
||||
|
||||
// Should be more specific:
|
||||
<Text fz={"md"} fw={"bold"}>Foto Profil PPID</Text>
|
||||
```
|
||||
|
||||
**Rekomendasi:** More descriptive label:
|
||||
```typescript
|
||||
<Text fz={"md"} fw={"bold"}>Foto Profil PPID</Text>
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Dropzone Accept Format**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~190
|
||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||
|
||||
// Missing mime type specifications
|
||||
```
|
||||
|
||||
**Rekomendasi:** Add full mime types:
|
||||
```typescript
|
||||
accept={{
|
||||
'image/jpeg': ['.jpeg', '.jpg'],
|
||||
'image/png': ['.png'],
|
||||
'image/webp': ['.webp'],
|
||||
}}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Preview Page - Title Order Inconsistency**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~55
|
||||
<Title order={4} ...>
|
||||
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
|
||||
</Title>
|
||||
|
||||
// Line ~90
|
||||
<Title order={3} ...>
|
||||
{item.name}
|
||||
</Title>
|
||||
|
||||
// Line ~100
|
||||
<Title order={3} ...>
|
||||
Biodata
|
||||
</Title>
|
||||
```
|
||||
|
||||
**Issue:** Title hierarchy tidak konsisten. Subtitle (order 4) lebih kecil dari content titles (order 3).
|
||||
|
||||
**Rekomendasi:** Samakan hierarchy:
|
||||
```typescript
|
||||
// Main title: order={2} atau order={3}
|
||||
// Section titles: order={4}
|
||||
// Name: order={3}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Missing Search Feature**
|
||||
|
||||
**Lokasi:** N/A (Single record module)
|
||||
|
||||
**Verdict:** ✅ **NOT APPLICABLE** - Module ini hanya handle single record, search tidak diperlukan.
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **13. Button Loading State Tidak Konsisten**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~270-280
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
// ...
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Issue:** Button hanya check `isSubmitting` local state, tidak check `allState.editForm.loading` dari global state.
|
||||
|
||||
**Rekomendasi:** Check both states:
|
||||
```typescript
|
||||
disabled={isSubmitting || allState.editForm.loading}
|
||||
{isSubmitting || allState.editForm.loading ? (
|
||||
<Loader size="sm" color="white" />
|
||||
) : (
|
||||
'Simpan'
|
||||
)}
|
||||
```
|
||||
|
||||
**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 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Missing validation di submit button | UI | Low | Low | Should fix |
|
||||
| 🟢 L | Duplicate useEffect di editor | Editor | Low | Low | Optional |
|
||||
| 🟢 L | Form label inconsistency | UI | Low | Low | Should fix |
|
||||
| 🟢 L | Image label text size | UI | Low | Low | Optional |
|
||||
| 🟢 L | Dropzone accept format | UI | Low | Low | Optional |
|
||||
| 🟢 L | Title order inconsistency | UI | Low | Low | Optional |
|
||||
| 🟢 L | Button loading state inconsistency | UI | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (8/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX clean & responsive
|
||||
2. ✅ File upload handling solid
|
||||
3. ✅ **Rich Text Editor** full-featured (Tiptap)
|
||||
4. ✅ **Modular form components** (Biodata, Riwayat, Pengalaman, Unggulan)
|
||||
5. ✅ **State management BEST PRACTICES** (originalForm tracking)
|
||||
6. ✅ **Edit form reset SANGAT BAIK** (original data tracking sempurna)
|
||||
7. ✅ Error handling comprehensive
|
||||
8. ✅ Loading state management dengan finally block
|
||||
9. ✅ Modal konfirmasi hapus untuk user safety
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **Schema deletedAt default SALAH** - Logic error untuk soft delete (CRITICAL)
|
||||
2. ⚠️ **HTML injection risk** - dangerouslySetInnerHTML tanpa sanitization (HIGH Security)
|
||||
3. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
|
||||
**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. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
4. ⚠️ **Add disabled state** di submit button
|
||||
5. ⚠️ **Fix form labels** (Nama Perbekel → Nama PPID)
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Fix schema deletedAt** - 30 menit (perlu migration)
|
||||
2. **🔴 HIGH: Fix HTML injection** dengan DOMPurify - 30 menit
|
||||
3. **🔴 HIGH: Refactor fetch methods** ke ApiFetch - 1 jam
|
||||
4. **🟡 MEDIUM: Add disabled state** di submit button - 15 menit
|
||||
5. **🟢 LOW: Fix form labels** - 10 menit
|
||||
6. **🟢 LOW: Polish minor issues** - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Aspect | Profil | Desa Anti Korupsi | SDGs Desa | APBDes | Prestasi Desa | **PPID Profil** | Notes |
|
||||
|--------|--------|-------------------|-----------|--------|---------------|-----------------|-------|
|
||||
| Fetch Pattern | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | ⚠️ Mixed | All perlu refactor |
|
||||
| Loading State | ⚠️ Some missing | ⚠️ Some missing | ⚠️ Missing | ✅ Good | ⚠️ findUnique missing | ✅ **Good** | PPID salah satu yang terbaik |
|
||||
| Edit Form Reset | ✅ Good | ✅ Good | ✅ Good | ✅ Good | ✅ Good | ✅ **EXCELLENT** | **PPID paling baik** (originalForm tracking) |
|
||||
| Type Safety | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ⚠️ Some `any` | ✅ **Good** | PPID typing lebih baik |
|
||||
| File Upload | ✅ Images | ✅ Documents | ✅ Images | ✅ Dual | ✅ Images | ✅ Images | Similar |
|
||||
| Error Handling | ✅ Good | ✅ Good (better) | ✅ Good | ✅ Good | ✅ Good | ✅ **Good** | Consistent |
|
||||
| **Schema deletedAt** | ⚠️ Issue | ⚠️ Issue | ⚠️ Issue | ✅ Good | ❌ **WRONG** | ❌ **WRONG** | **PPID CRITICAL** |
|
||||
| HTML Injection | ⚠️ Present | ⚠️ Present | N/A | N/A | ⚠️ Present | ⚠️ **Present** | Security concern |
|
||||
| Rich Text Editor | ✅ Present | ✅ Present | N/A | N/A | ✅ Present | ✅ **Best** | **PPID editor paling lengkap** |
|
||||
| Modular Forms | ❌ None | ❌ None | N/A | ❌ None | ❌ None | ✅ **YES** | **PPID unique feature** |
|
||||
| State Management | ⚠️ Good | ⚠️ Good | ⚠️ Good | ⚠️ Good | ⚠️ Good | ✅ **BEST** | **PPID state management terbaik** |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF PPID PROFIL MODULE
|
||||
|
||||
**Most Advanced Module:**
|
||||
1. ✅ **Rich Text Editor (Tiptap)** - Full-featured dengan toolbar lengkap
|
||||
2. ✅ **Modular Form Components** - Biodata, Riwayat, Pengalaman, Unggulan forms
|
||||
3. ✅ **originalForm Tracking** - State management best practice (unique to PPID)
|
||||
4. ✅ **Single Record Pattern** - Handle "edit" special ID untuk single profile
|
||||
5. ✅ **Comprehensive Error Handling** - Special handling untuk "data not found" cases
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ **State management PALING BAIK** dibanding semua modul lain
|
||||
2. ✅ **Edit form reset PALING BAIK** (originalForm tracking sempurna)
|
||||
3. ✅ **Type safety LEBIH BAIK** (minimal any usage)
|
||||
4. ✅ **Loading state management PROPER** (dengan finally block)
|
||||
5. ✅ **Modular component design** (reusable forms)
|
||||
|
||||
**Critical Issues:**
|
||||
1. ❌ **Schema deletedAt SALAH** - sama seperti SDGs, Desa Anti Korupsi, Prestasi Desa
|
||||
2. ❌ **HTML injection risk** - sama seperti modul lain yang pakai rich text
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul **PPID Profil adalah YANG PALING BAIK** dibanding semua modul yang sudah di-QC. State management-nya adalah best practice dengan originalForm tracking yang sempurna. Rich Text Editor implementation juga paling advanced.
|
||||
|
||||
**Unique Strengths:**
|
||||
1. ✅ **State management terbaik** - originalForm tracking untuk reset yang sempurna
|
||||
2. ✅ **Rich Text Editor terlengkap** - Tiptap dengan semua extensions
|
||||
3. ✅ **Modular form design** - Reusable components untuk setiap section
|
||||
4. ✅ **Type safety lebih baik** - Minimal any usage
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 401
|
||||
|
||||
model ProfilePPID {
|
||||
// ...
|
||||
- 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_default_ppid
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 FIX HTML INJECTION (30 MENIT):
|
||||
File: src/app/admin/(dashboard)/ppid/profil-ppid/page.tsx
|
||||
|
||||
+ import DOMPurify from 'dompurify';
|
||||
|
||||
// Line ~105
|
||||
- dangerouslySetInnerHTML={{ __html: item.biodata }}
|
||||
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.biodata) }}
|
||||
|
||||
// Repeat for riwayat, pengalaman, unggulan
|
||||
```
|
||||
|
||||
Setelah fix critical issues, module ini **PRODUCTION-READY** dan bisa jadi **REFERENCE** untuk modul lain! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**PPID Profil Module adalah BEST PRACTICE untuk:**
|
||||
1. ✅ **State management** - originalForm tracking pattern
|
||||
2. ✅ **Edit form reset** - Comprehensive reset logic
|
||||
3. ✅ **Modular form components** - Reusable design pattern
|
||||
4. ✅ **Rich Text Editor** - Tiptap implementation
|
||||
5. ✅ **Type safety** - Proper TypeScript typing
|
||||
|
||||
**Modules lain bisa belajar dari PPID Profil:**
|
||||
- APBDes: Implement originalForm tracking
|
||||
- Prestasi Desa: Implement originalForm tracking
|
||||
- SDGs Desa: Implement originalForm tracking
|
||||
- Desa Anti Korupsi: Implement originalForm tracking
|
||||
- Profil (Media Sosial, Program Inovasi): Implement originalForm tracking
|
||||
|
||||
---
|
||||
|
||||
**File Location:** `QC/PPID/QC-PPID-PROFIL-MODULE.md` 📄
|
||||
936
QC/PPID/QC-STRUKTUR-PPID-MODULE.md
Normal file
936
QC/PPID/QC-STRUKTUR-PPID-MODULE.md
Normal file
@@ -0,0 +1,936 @@
|
||||
# QC Summary - Struktur PPID Module
|
||||
|
||||
**Scope:** Struktur Organisasi (Organization Chart), Pegawai PPID, Posisi Organisasi
|
||||
**Date:** 2026-02-23
|
||||
**Status:** ✅ Secara umum sudah baik, ada beberapa improvement yang diperlukan
|
||||
|
||||
---
|
||||
|
||||
## 📊 OVERVIEW
|
||||
|
||||
| Sub-Module | Schema | API | UI Admin | State Management | Overall |
|
||||
|------------|--------|-----|----------|-----------------|---------|
|
||||
| Struktur Organisasi | ✅ Baik | ✅ Baik | ✅ **Excellent** | ✅ Baik | 🟢 |
|
||||
| Posisi Organisasi | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 |
|
||||
| Pegawai PPID | ⚠️ Ada issue | ✅ Baik | ✅ Baik | ⚠️ Ada issue | 🟡 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ YANG SUDAH BAIK
|
||||
|
||||
### **1. UI/UX - Organization Chart (UNIQUE FEATURE!)**
|
||||
- ✅ **PrimeReact OrganizationChart** - Visual hierarchy yang excellent
|
||||
- ✅ Interactive tree structure dengan expand/collapse
|
||||
- ✅ Custom node template dengan foto, nama, dan posisi
|
||||
- ✅ Responsive design dengan overflow handling
|
||||
- ✅ Empty state yang informatif
|
||||
- ✅ Loading state dengan spinner
|
||||
|
||||
**Code Example (✅ EXCELLENT):**
|
||||
```typescript
|
||||
// struktur-organisasi/page.tsx - Line ~45-75
|
||||
const posisiMap = new Map<string, any>();
|
||||
|
||||
const aktifPegawai = stateOrganisasi.findManyAll.data?.filter(p => p.isActive);
|
||||
|
||||
for (const pegawai of aktifPegawai) {
|
||||
const posisiId = pegawai.posisi.id;
|
||||
if (!posisiMap.has(posisiId)) {
|
||||
posisiMap.set(posisiId, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
}
|
||||
|
||||
// Build tree structure
|
||||
let root: any[] = [];
|
||||
posisiMap.forEach((posisi) => {
|
||||
if (posisi.parentId) {
|
||||
const parent = posisiMap.get(posisi.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(posisi);
|
||||
}
|
||||
} else {
|
||||
root.push(posisi);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to OrganizationChart format
|
||||
function toOrgChartFormat(node: any): any {
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Belum ada pegawai',
|
||||
status: node.nama,
|
||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png',
|
||||
},
|
||||
children: node.children.map(toOrgChartFormat),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **UNIQUE & EXCELLENT** - Satu-satunya modul dengan organization chart visual!
|
||||
|
||||
---
|
||||
|
||||
### **2. File Upload Handling**
|
||||
- ✅ Dropzone dengan preview image
|
||||
- ✅ Validasi format gambar (JPEG, JPG, PNG, WEBP)
|
||||
- ✅ Validasi ukuran file (max 5MB)
|
||||
- ✅ Tombol hapus preview (IconX di pojok kanan atas)
|
||||
- ✅ URL.createObjectURL untuk preview lokal
|
||||
|
||||
### **3. Form Validation**
|
||||
- ✅ Zod schema untuk validasi typed
|
||||
- ✅ Email validation dengan regex
|
||||
- ✅ Required field validation
|
||||
- ✅ isFormValid() check sebelum submit
|
||||
- ✅ Error toast dengan pesan spesifik
|
||||
- ✅ Button disabled saat invalid/loading
|
||||
|
||||
### **4. CRUD Operations**
|
||||
- ✅ Create dengan upload file
|
||||
- ✅ FindMany dengan pagination & search
|
||||
- ✅ FindUnique untuk detail
|
||||
- ✅ Delete dengan hard delete
|
||||
- ✅ Update dengan file replacement
|
||||
- ✅ **Non-active feature** untuk soft disable pegawai
|
||||
|
||||
### **5. State Management**
|
||||
- ✅ Proper typing dengan Prisma types
|
||||
- ✅ Loading state management dengan finally block
|
||||
- ✅ Error handling yang comprehensive
|
||||
- ✅ Reset function untuk cleanup
|
||||
- ✅ findManyAll untuk organization chart data
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// state file - Line ~270-290
|
||||
findManyAll: {
|
||||
data: null as Prisma.PegawaiPPIDGetPayload<{...}>[] | null,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (search = "") => {
|
||||
posisiOrganisasi.findManyAll.loading = true; // ✅ Start loading
|
||||
posisiOrganisasi.findManyAll.search = search;
|
||||
try {
|
||||
const query: any = { search };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai["find-many-all"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
posisiOrganisasi.findManyAll.data = res.data.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading pegawai:", error);
|
||||
posisiOrganisasi.findManyAll.data = [];
|
||||
} finally {
|
||||
posisiOrganisasi.findManyAll.loading = false; // ✅ Stop loading
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Loading state management sudah proper!
|
||||
|
||||
---
|
||||
|
||||
### **6. Edit Form - Original Data Tracking**
|
||||
- ✅ Original data state untuk reset form
|
||||
- ✅ Load data existing dengan benar
|
||||
- ✅ Preview image dari data lama
|
||||
- ✅ Reset form mengembalikan ke data original
|
||||
- ✅ File replacement logic (upload baru jika ada perubahan)
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~80-115
|
||||
const [originalData, setOriginalData] = useState({
|
||||
namaLengkap: "",
|
||||
gelarAkademik: "",
|
||||
imageId: "",
|
||||
tanggalMasuk: "",
|
||||
email: "",
|
||||
telepon: "",
|
||||
alamat: "",
|
||||
posisiId: "",
|
||||
imageUrl: "",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// Load data
|
||||
const data = await stateOrganisasi.edit.load(id);
|
||||
|
||||
setOriginalData({
|
||||
...data,
|
||||
imageUrl: data.image?.link || '',
|
||||
});
|
||||
|
||||
setPreviewImage(data.image?.link || null);
|
||||
|
||||
// Line ~135 - Handle reset
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
namaLengkap: originalData.namaLengkap,
|
||||
gelarAkademik: originalData.gelarAkademik,
|
||||
imageId: originalData.imageId,
|
||||
tanggalMasuk: originalData.tanggalMasuk,
|
||||
email: originalData.email,
|
||||
telepon: originalData.telepon,
|
||||
alamat: originalData.alamat,
|
||||
posisiId: originalData.posisiId,
|
||||
isActive: originalData.isActive,
|
||||
});
|
||||
setPreviewImage(originalData.imageUrl || null);
|
||||
setFile(null);
|
||||
toast.info("Form dikembalikan ke data awal");
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **BAIK** - Original data tracking sudah implementasi dengan baik!
|
||||
|
||||
---
|
||||
|
||||
### **7. Unique Features**
|
||||
- ✅ **Organization Chart** - Visual hierarchy tree (UNIQUE!)
|
||||
- ✅ **Hierarchical Positions** - Parent-child relationships
|
||||
- ✅ **Active/Non-active Toggle** - Soft disable untuk pegawai
|
||||
- ✅ **Email Validation** - Regex validation untuk email format
|
||||
- ✅ **Date Input Handling** - Proper date formatting untuk tanggal masuk
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - Missing deletedAt for Soft Delete**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 327-332, 343-351)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model PosisiOrganisasiPPID {
|
||||
// ...
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// ❌ MISSING: deletedAt field untuk soft delete
|
||||
}
|
||||
|
||||
model PegawaiPPID {
|
||||
// ...
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// ❌ MISSING: deletedAt field untuk soft delete
|
||||
}
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- **INCONSISTENT!** Model `StrukturOrganisasiPPID` punya `deletedAt`, tapi Posisi dan Pegawai tidak
|
||||
- Hard delete vs soft delete inconsistency
|
||||
- Data integrity issue saat delete (data hilang permanen)
|
||||
- Tidak bisa restore data yang ter-delete
|
||||
|
||||
**Rekomendasi:** Add deletedAt field:
|
||||
```prisma
|
||||
model PosisiOrganisasiPPID {
|
||||
// ...
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? @default(null) // ✅ Add for soft delete
|
||||
}
|
||||
|
||||
model PegawaiPPID {
|
||||
// ...
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? @default(null) // ✅ Add for soft delete
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 **HIGH**
|
||||
**Effort:** Medium (perlu migration)
|
||||
**Impact:** **HIGH** (data integrity & consistency)
|
||||
|
||||
---
|
||||
|
||||
#### **2. State Management - Fetch Pattern Inconsistency**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
|
||||
|
||||
**Masalah:** Ada 2 pattern berbeda untuk fetch API:
|
||||
|
||||
```typescript
|
||||
// ❌ Pattern 1: ApiFetch (create, findMany, findManyAll)
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(pegawai.create.form);
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai["find-many"].get({ query });
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai["find-many-all"].get({ query });
|
||||
|
||||
// ❌ Pattern 2: fetch manual (findUnique, edit, delete, nonActive)
|
||||
const res = await fetch(`/api/ppid/strukturppid/pegawai/${id}`);
|
||||
const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, { method: "DELETE" });
|
||||
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, { method: "DELETE" });
|
||||
```
|
||||
|
||||
**Dampak:**
|
||||
- Code consistency buruk
|
||||
- Sulit maintenance
|
||||
- Type safety tidak konsisten
|
||||
- Duplikasi logic error handling
|
||||
|
||||
**Rekomendasi:** Gunakan **ApiFetch** untuk semua operasi:
|
||||
|
||||
```typescript
|
||||
// ✅ Unified pattern
|
||||
async load(id: string) {
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[id].get();
|
||||
|
||||
if (res.data?.success) {
|
||||
const data = res.data.data;
|
||||
this.id = data.id;
|
||||
this.form = {
|
||||
namaLengkap: data.namaLengkap,
|
||||
gelarAkademik: data.gelarAkademik,
|
||||
imageId: data.imageId,
|
||||
tanggalMasuk: data.tanggalMasuk,
|
||||
email: data.email,
|
||||
telepon: data.telepon,
|
||||
alamat: data.alamat,
|
||||
posisiId: data.posisiId,
|
||||
isActive: data.isActive,
|
||||
};
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(res.data?.message || "Gagal memuat data");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading pegawai:", error);
|
||||
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async byId(id: string) {
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai["del"][id].delete();
|
||||
|
||||
if (res.data?.success) {
|
||||
toast.success(res.data.message || "Berhasil hapus pegawai");
|
||||
await pegawai.findMany.load();
|
||||
} else {
|
||||
toast.error(res.data?.message || "Gagal hapus pegawai");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal delete:", error);
|
||||
toast.error("Terjadi kesalahan saat menghapus");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🔴 High
|
||||
**Effort:** Medium (refactor di semua methods)
|
||||
|
||||
---
|
||||
|
||||
#### **3. HTML Injection Risk - dangerouslySetInnerHTML**
|
||||
|
||||
**Lokasi:**
|
||||
- `posisi-organisasi/page.tsx` (line ~95, 155)
|
||||
- `posisi-organisasi/create/page.tsx` (CreateEditor component)
|
||||
- `posisi-organisasi/[id]/edit/page.tsx` (EditEditor component)
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// ❌ Direct HTML render tanpa sanitization
|
||||
<Text
|
||||
fz="sm"
|
||||
lh={1.5}
|
||||
c="dimmed"
|
||||
lineClamp={1}
|
||||
dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
/>
|
||||
```
|
||||
|
||||
**Risk:**
|
||||
- XSS attack jika admin input script malicious
|
||||
- Bisa inject iframe, script tag, dll
|
||||
- Security vulnerability
|
||||
|
||||
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Sanitize sebelum render
|
||||
const sanitizedDeskripsi = DOMPurify.sanitize(item.deskripsi);
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedDeskripsi }}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
Atau validasi di backend untuk whitelist tag HTML yang diperbolehkan.
|
||||
|
||||
**Priority:** 🔴 **HIGH** (**Security concern**)
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Console.log di Production**
|
||||
|
||||
**Lokasi:** Multiple places di state file
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~65
|
||||
console.error("Load struktur error:", errorMessage);
|
||||
|
||||
// Line ~130
|
||||
console.error("Update struktur error:", errorMessage);
|
||||
|
||||
// Line ~220
|
||||
console.error("Failed to fetch posisiOrganisasi:", res.statusText);
|
||||
|
||||
// Line ~224
|
||||
console.error("Error fetching posisiOrganisasi:", error);
|
||||
|
||||
// Line ~370
|
||||
console.error("Gagal fetch posisi organisasi paginated:", err);
|
||||
|
||||
// Line ~400
|
||||
console.error("Failed to load posisiOrganisasi:", res.data?.message);
|
||||
|
||||
// Line ~404
|
||||
console.error("Error loading posisiOrganisasi:", error);
|
||||
|
||||
// ... dan banyak lagi
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **5. Type Safety - Any Usage**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~190
|
||||
const query: any = { page, limit: appliedLimit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
|
||||
// Line ~215
|
||||
const query: any = { search }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
|
||||
// Line ~365
|
||||
const query: any = { page, limit }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
|
||||
// Line ~395
|
||||
const query: any = { search }; // ❌ Using 'any'
|
||||
if (search) query.search = search;
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan typed query:
|
||||
|
||||
```typescript
|
||||
// Define type
|
||||
interface FindManyQuery {
|
||||
page: number | string;
|
||||
limit?: number | string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
// Use typed query
|
||||
const query: FindManyQuery = { page, limit: appliedLimit };
|
||||
if (search) query.search = search;
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **6. Error Message Tidak Konsisten**
|
||||
|
||||
**Lokasi:** Multiple places
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Create posisi - Line ~180
|
||||
toast.error("Terjadi kesalahan saat menambahkan posisi");
|
||||
|
||||
// Create pegawai - Line ~280
|
||||
toast.error("Terjadi kesalahan saat menambahkan pegawai");
|
||||
|
||||
// Delete - Line ~430
|
||||
toast.error("Terjadi kesalahan saat menghapus posisi organisasi");
|
||||
|
||||
// Edit - Line ~520
|
||||
toast.error("Gagal memuat data");
|
||||
|
||||
// Update - Line ~560
|
||||
toast.error("Gagal mengupdate posisi organisasi");
|
||||
```
|
||||
|
||||
**Issue:**
|
||||
- Generic error messages
|
||||
- Inconsistent patterns ("Terjadi kesalahan" vs "Gagal")
|
||||
- Tidak spesifik ke resource type
|
||||
|
||||
**Rekomendasi:** Standardisasi error messages:
|
||||
|
||||
```typescript
|
||||
// Pattern: "[Action] [resource] gagal"
|
||||
toast.error("Menambahkan Posisi Organisasi gagal");
|
||||
toast.error("Menghapus Posisi Organisasi gagal");
|
||||
toast.error("Memuat data Posisi Organisasi gagal");
|
||||
toast.error("Memperbarui data Posisi Organisasi gagal");
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **7. Zod Schema - Error Message Tidak Konsisten**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~170
|
||||
const templatePosisiOrganisasi = z.object({
|
||||
nama: z.string().min(1, "Nama harus diisi"), // ✅ OK
|
||||
deskripsi: z.string().optional(), // ⚠️ No min message
|
||||
hierarki: z.number().int().positive("Hierarki harus angka positif"), // ✅ OK
|
||||
});
|
||||
|
||||
// Line ~450
|
||||
const templatePegawai = z.object({
|
||||
namaLengkap: z.string().min(1, "Nama wajib diisi"), // ✅ OK
|
||||
gelarAkademik: z.string().min(1, "Gelar Akademik wajib diisi"), // ✅ OK
|
||||
imageId: z.string().min(1, "Gambar wajib dipilih"), // ✅ OK
|
||||
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"), // ✅ OK
|
||||
email: z.string().email("Email tidak valid").optional(), // ⚠️ Optional tapi ada validation
|
||||
telepon: z.string().min(1, "Telepom wajib diisi"), // ❌ Typo: "Telepom"
|
||||
alamat: z.string().min(1, "Alamat wajib diisi"), // ✅ OK
|
||||
posisiId: z.string().min(1, "Posisi wajib diisi"), // ✅ OK
|
||||
isActive: z.boolean().default(true), // ✅ OK
|
||||
});
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix typo dan standardisasi:
|
||||
|
||||
```typescript
|
||||
const templatePegawai = z.object({
|
||||
namaLengkap: z.string().min(1, "Nama lengkap wajib diisi"),
|
||||
gelarAkademik: z.string().min(1, "Gelar akademik wajib diisi"),
|
||||
imageId: z.string().min(1, "Foto profil wajib diunggah"),
|
||||
tanggalMasuk: z.string().min(1, "Tanggal masuk wajib diisi"),
|
||||
email: z.string().email("Format email tidak valid").optional().or(z.literal('')),
|
||||
telepon: z.string().min(1, "Nomor telepon wajib diisi"), // ✅ Fix typo
|
||||
alamat: z.string().min(1, "Alamat wajib diisi"),
|
||||
posisiId: z.string().min(1, "Posisi wajib dipilih"),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **8. Pagination onChange Tidak Include Search**
|
||||
|
||||
**Lokasi:** `pegawai/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~170
|
||||
<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:
|
||||
```typescript
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, debouncedSearch); // ✅ Include search
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **9. Missing Loading State di Submit Button**
|
||||
|
||||
**Lokasi:** `pegawai/create/page.tsx`, `pegawai/[id]/edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// create/page.tsx - Line ~240
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
// ...
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Issue:** Button tidak check `stateOrganisasi.create.loading` dari global state.
|
||||
|
||||
**Rekomendasi:** Check both states:
|
||||
```typescript
|
||||
disabled={!isFormValid() || isSubmitting || stateOrganisasi.create.loading}
|
||||
{isSubmitting || stateOrganisasi.create.loading ? (
|
||||
<Loader size="sm" color="white" />
|
||||
) : (
|
||||
'Simpan'
|
||||
)}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Duplicate Error Logging**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~120
|
||||
} catch (error) {
|
||||
console.error('Error loading pegawai:', error); // ❌ Duplicate
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
|
||||
}
|
||||
|
||||
// edit/page.tsx - Line ~160
|
||||
} catch (error) {
|
||||
console.error('Error updating pegawai:', error); // ❌ Duplicate
|
||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
|
||||
}
|
||||
```
|
||||
|
||||
**Rekomendasi:** Cukup satu logging yang informatif:
|
||||
```typescript
|
||||
} catch (error) {
|
||||
console.error('Failed to load Pegawai:', err);
|
||||
toast.error('Gagal memuat data Pegawai');
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **11. Button Label Inconsistency**
|
||||
|
||||
**Lokasi:** Multiple files
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// create/page.tsx - Line ~230
|
||||
<Button ...>Reset</Button>
|
||||
|
||||
// edit/page.tsx - Line ~140
|
||||
<Button ...>Batal</Button>
|
||||
|
||||
// Should be consistent: "Reset" atau "Batal"
|
||||
```
|
||||
|
||||
**Rekomendasi:** Standardisasi:
|
||||
```typescript
|
||||
// Create: "Reset"
|
||||
// Edit: "Batal" (lebih descriptive untuk cancel changes)
|
||||
// OR both: "Reset" / "Batal"
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Search Placeholder Tidak Spesifik**
|
||||
|
||||
**Lokasi:**
|
||||
- `pegawai/page.tsx`: `placeholder='Cari nama pegawai atau posisi...'` ✅ Spesifik
|
||||
- `posisi-organisasi/page.tsx`: `placeholder='Cari posisi organisasi...'` ✅ OK
|
||||
|
||||
**Verdict:** ✅ **SUDAH BENAR** - Placeholder sudah spesifik.
|
||||
|
||||
**Priority:** 🟢 None
|
||||
**Effort:** None
|
||||
|
||||
---
|
||||
|
||||
#### **13. Non-Active Endpoint Method**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~490
|
||||
nonActive: {
|
||||
loading: false,
|
||||
async byId(id: string) {
|
||||
// ...
|
||||
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
|
||||
method: "DELETE", // ⚠️ Biasanya nonActive pakai PATCH atau PUT
|
||||
});
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Issue:** Method "DELETE" untuk non-active agak confusing. Biasanya pakai "PATCH" atau "PUT".
|
||||
|
||||
**Rekomendasi:** Consider using PATCH:
|
||||
```typescript
|
||||
const res = await fetch(`/api/ppid/strukturppid/pegawai/non-active/${id}`, {
|
||||
method: "PATCH", // ✅ More semantic for toggle active/inactive
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isActive: false }),
|
||||
});
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low (perlu update API juga)
|
||||
|
||||
---
|
||||
|
||||
#### **14. OrganizationChart - Missing Expand/Collapse Controls**
|
||||
|
||||
**Lokasi:** `struktur-organisasi/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~80
|
||||
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
|
||||
```
|
||||
|
||||
**Issue:** Tidak ada controls untuk expand/collapse all nodes.
|
||||
|
||||
**Rekomendasi:** Add toggle button:
|
||||
```typescript
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
const toggleAll = () => {
|
||||
const newExpanded = !expanded;
|
||||
setExpanded(newExpanded);
|
||||
// Update chartData dengan expanded: newExpanded untuk semua nodes
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Group justify="flex-end" mb="md">
|
||||
<Button size="xs" onClick={toggleAll}>
|
||||
{expanded ? 'Collapse All' : 'Expand All'}
|
||||
</Button>
|
||||
</Group>
|
||||
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
|
||||
</Box>
|
||||
);
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
## 📋 RINGKASAN ACTION ITEMS
|
||||
|
||||
| Priority | Issue | Module | Impact | Effort | Status |
|
||||
|----------|-------|--------|--------|--------|--------|
|
||||
| 🔴 P0 | **Schema missing deletedAt** | Schema | **HIGH** | Medium | **MUST FIX** |
|
||||
| 🔴 P0 | Fetch method inconsistency | State | Medium | Medium | Perlu refactor |
|
||||
| 🔴 P1 | **HTML injection risk** | UI | **HIGH (Security)** | Low | **Should fix** |
|
||||
| 🟡 M | Console.log in production | State | Low | Low | Optional |
|
||||
| 🟡 M | Type safety (any usage) | State | Low | Low | Optional |
|
||||
| 🟡 M | Error message inconsistency | State/UI | Low | Low | Optional |
|
||||
| 🟡 M | Zod schema typo ("Telepom") | State | Low | Low | Should fix |
|
||||
| 🟢 L | Pagination missing search param | Pegawai UI | Low | Low | Should fix |
|
||||
| 🟢 L | Missing loading state di submit button | UI | Low | Low | Optional |
|
||||
| 🟢 L | Duplicate error logging | UI | Low | Low | Optional |
|
||||
| 🟢 L | Button label inconsistency | UI | Low | Low | Optional |
|
||||
| 🟢 L | Non-active endpoint method | API | Low | Low | Optional |
|
||||
| 🟢 L | OrganizationChart expand/collapse controls | UI | Low | Low | Nice to have |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (8/10)**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ **Organization Chart** - Unique visual hierarchy feature (EXCELLENT!)
|
||||
2. ✅ UI/UX clean & responsive
|
||||
3. ✅ File upload handling solid
|
||||
4. ✅ Form validation comprehensive (email validation, required fields)
|
||||
5. ✅ State management terstruktur (Valtio)
|
||||
6. ✅ **Edit form reset sudah benar** (original data tracking)
|
||||
7. ✅ **Active/Non-active toggle** untuk pegawai
|
||||
8. ✅ Loading state management dengan finally block
|
||||
9. ✅ findManyAll untuk organization chart data
|
||||
|
||||
**Critical Issues:**
|
||||
1. ⚠️ **Schema missing deletedAt** - Inconsistency dengan StrukturOrganisasiPPID (HIGH)
|
||||
2. ⚠️ Fetch method pattern inconsistency (ApiFetch vs fetch manual)
|
||||
3. ⚠️ **HTML injection risk** di deskripsi posisi (HIGH Security)
|
||||
|
||||
**Areas for Improvement:**
|
||||
1. ⚠️ **Add deletedAt field** ke PosisiOrganisasiPPID dan PegawaiPPID
|
||||
2. ⚠️ **Refactor fetch methods** untuk gunakan ApiFetch consistently
|
||||
3. ⚠️ **Fix HTML injection** dengan DOMPurify atau backend validation
|
||||
4. ⚠️ **Fix typo** "Telepom" → "Telepon" di Zod schema
|
||||
5. ⚠️ **Improve type safety** dengan remove `any` usage
|
||||
|
||||
**Recommended Next Steps:**
|
||||
1. **🔴 CRITICAL: Add schema deletedAt** - 30 menit (perlu migration)
|
||||
2. **🔴 HIGH: Fix HTML injection** dengan DOMPurify - 30 menit
|
||||
3. **🔴 HIGH: Refactor fetch methods** ke ApiFetch - 1 jam
|
||||
4. **🟡 MEDIUM: Fix typo** di Zod schema - 5 menit
|
||||
5. **🟢 LOW: Add pagination search param** - 10 menit
|
||||
6. **🟢 LOW: Polish minor issues** - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Module | Unique Features | Schema | State | Edit Reset | Overall |
|
||||
|--------|----------------|--------|-------|------------|---------|
|
||||
| Profil | ❌ None | ✅ Good | ⚠️ Good | ✅ Good | 🟢 |
|
||||
| Desa Anti Korupsi | ❌ None | ⚠️ deletedAt | ⚠️ Good | ✅ Good | 🟢 |
|
||||
| SDGs Desa | ❌ None | ⚠️ deletedAt | ⚠️ Good | ✅ Good | 🟢 |
|
||||
| APBDes | ✅ Dual upload, Items hierarchy | ✅ **Best** | ⚠️ Good | ✅ Good | 🟢 |
|
||||
| Prestasi Desa | ❌ None | ⚠️ deletedAt | ⚠️ Good | ✅ Good | 🟢 |
|
||||
| PPID Profil | ✅ Rich Text, Modular forms | ⚠️ deletedAt | ✅ **Best** | ✅ **Excellent** | 🟢⭐ |
|
||||
| **Struktur PPID** | ✅ **Org Chart**, Hierarchy, Non-active | ⚠️ Inconsistent | ✅ Good | ✅ Good | 🟢 |
|
||||
|
||||
**Struktur PPID Highlights:**
|
||||
- ✅ **UNIQUE:** Organization Chart visualization (no other module has this!)
|
||||
- ✅ **UNIQUE:** Hierarchical position structure (parent-child)
|
||||
- ✅ **UNIQUE:** Active/Non-active toggle feature
|
||||
- ✅ **GOOD:** Email validation dengan regex
|
||||
- ⚠️ **ISSUE:** Schema inconsistency (deletedAt missing di 2 models)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF STRUKTUR PPID MODULE
|
||||
|
||||
**Most Unique Module:**
|
||||
1. ✅ **PrimeReact OrganizationChart** - Visual tree hierarchy (UNIQUE!)
|
||||
2. ✅ **Parent-child position relationships** - Hierarchical structure
|
||||
3. ✅ **Active/Non-active toggle** - Soft disable tanpa delete
|
||||
4. ✅ **Email validation** - Regex validation untuk email format
|
||||
5. ✅ **findManyAll pattern** - Load all data untuk organization chart
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ Organization chart implementation excellent
|
||||
2. ✅ Loading state management proper (dengan finally block)
|
||||
3. ✅ Edit form reset comprehensive (original data tracking)
|
||||
4. ✅ Email validation di form (create & edit)
|
||||
5. ✅ Date input handling untuk tanggal masuk
|
||||
|
||||
**Critical Issues:**
|
||||
1. ❌ **Schema deletedAt missing** - Inconsistency issue
|
||||
2. ❌ **HTML injection risk** - Same issue as modul lain dengan rich text
|
||||
|
||||
---
|
||||
|
||||
**Catatan:** Secara keseluruhan, modul **Struktur PPID adalah YANG PALING UNIQUE** dengan Organization Chart visualization yang excellent. Module ini punya fitur-fitur yang tidak ada di modul lain (hierarchical positions, org chart, active/non-active toggle).
|
||||
|
||||
**Unique Strengths:**
|
||||
1. ✅ **Organization Chart** - Best visual representation
|
||||
2. ✅ **Hierarchical data structure** - Parent-child relationships
|
||||
3. ✅ **Active/Non-active feature** - Soft disable tanpa delete
|
||||
4. ✅ **Email validation** - Comprehensive form validation
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 327-332, 343-351
|
||||
|
||||
model PosisiOrganisasiPPID {
|
||||
// ...
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
+ deletedAt DateTime? @default(null) // ✅ Add for soft delete
|
||||
}
|
||||
|
||||
model PegawaiPPID {
|
||||
// ...
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
+ deletedAt DateTime? @default(null) // ✅ Add for soft delete
|
||||
}
|
||||
|
||||
# Lalu jalankan:
|
||||
bunx prisma db push
|
||||
# atau
|
||||
bunx prisma migrate dev --name add_deletedat_struktur_ppid
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 FIX HTML INJECTION (30 MENIT):
|
||||
File: posisi-organisasi/page.tsx
|
||||
+ import DOMPurify from 'dompurify';
|
||||
|
||||
// Line ~95
|
||||
- dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }}
|
||||
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(item.deskripsi) }}
|
||||
|
||||
// Repeat for mobile view line ~155
|
||||
```
|
||||
|
||||
Setelah fix critical issues, module ini **PRODUCTION-READY** dan **ORGANIZATION CHART** adalah fitur yang bisa jadi **SHOWCASE**! 🎉
|
||||
|
||||
---
|
||||
|
||||
**File Location:** `QC/PPID/QC-STRUKTUR-PPID-MODULE.md` 📄
|
||||
797
QC/PPID/QC-VISI-MISI-PPID-MODULE.md
Normal file
797
QC/PPID/QC-VISI-MISI-PPID-MODULE.md
Normal file
@@ -0,0 +1,797 @@
|
||||
# QC Summary - Visi Misi PPID Module
|
||||
|
||||
**Scope:** Preview Visi Misi, Edit Visi Misi 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 |
|
||||
|--------|--------|-----|----------|-----------------|---------|
|
||||
| Visi Misi 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 Visi dan Misi
|
||||
|
||||
### **2. Rich Text Editor (Tiptap)**
|
||||
- ✅ Full-featured editor dengan toolbar lengkap
|
||||
- ✅ 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
|
||||
- ✅ `immediatelyRender: false` untuk menghindari hydration mismatch
|
||||
|
||||
### **3. Form Component Structure**
|
||||
- ✅ Modular form components (VisiPPID, MisiPPID)
|
||||
- ✅ Reusable PPIDTextEditor component
|
||||
- ✅ Proper TypeScript typing
|
||||
- ✅ Controlled components dengan onChange handler
|
||||
|
||||
### **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):**
|
||||
```typescript
|
||||
// state file - Line ~30-50
|
||||
findById: {
|
||||
data: null as VisiMisiPPIDForm | null,
|
||||
loading: false,
|
||||
initialize() {
|
||||
stateVisiMisiPPID.findById.data = {
|
||||
id: "",
|
||||
misi: "",
|
||||
visi: "",
|
||||
} as VisiMisiPPIDForm;
|
||||
},
|
||||
async load(id: string) {
|
||||
try {
|
||||
stateVisiMisiPPID.findById.loading = true; // ✅ Start loading
|
||||
const res = await ApiFetch.api.ppid.visimisippid["find-by-id"].get({
|
||||
query: { id },
|
||||
});
|
||||
if (res.status === 200) {
|
||||
stateVisiMisiPPID.findById.data = res.data?.data ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error((error as Error).message);
|
||||
toast.error("Terjadi kesalahan saat mengambil data visi misi");
|
||||
} finally {
|
||||
stateVisiMisiPPID.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):**
|
||||
```typescript
|
||||
// edit/page.tsx - Line ~20-45
|
||||
const [formData, setFormData] = useState({ visi: '', misi: '' });
|
||||
const [originalData, setOriginalData] = useState({ visi: '', misi: '' });
|
||||
|
||||
// Initialize from global state
|
||||
useEffect(() => {
|
||||
if (visiMisi.findById.data) {
|
||||
setFormData({
|
||||
visi: visiMisi.findById.data.visi ?? '',
|
||||
misi: visiMisi.findById.data.misi ?? '',
|
||||
});
|
||||
setOriginalData({
|
||||
visi: visiMisi.findById.data.visi ?? '',
|
||||
misi: visiMisi.findById.data.misi ?? '',
|
||||
});
|
||||
}
|
||||
}, [visiMisi.findById.data]);
|
||||
|
||||
// Line ~60 - Handle reset
|
||||
const handleResetForm = () => {
|
||||
setFormData({
|
||||
visi: originalData.visi,
|
||||
misi: originalData.misi,
|
||||
});
|
||||
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
|
||||
|
||||
**Code Example (✅ GOOD):**
|
||||
```typescript
|
||||
// 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.visi) &&
|
||||
!isRichTextEmpty(formData.misi)
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Verdict:** ✅ **EXCELLENT** - Rich text validation yang comprehensive!
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ISSUES & SARAN PERBAIKAN
|
||||
|
||||
### **🔴 CRITICAL**
|
||||
|
||||
#### **1. Schema - deletedAt Default Value SALAH**
|
||||
|
||||
**Lokasi:** `prisma/schema.prisma` (line 374)
|
||||
|
||||
**Masalah:**
|
||||
```prisma
|
||||
model VisiMisiPPID {
|
||||
id String @id @default(cuid())
|
||||
visi String @db.Text
|
||||
misi 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:**
|
||||
```prisma
|
||||
// Record baru dibuat
|
||||
CREATE VisiMisiPPID {
|
||||
visi: "Visi 1",
|
||||
misi: "Misi 1",
|
||||
// deletedAt otomatis ter-set ke now() ❌
|
||||
// isActive: true ✅
|
||||
}
|
||||
|
||||
// Query untuk data aktif (seharusnya return data ini)
|
||||
prisma.visiMisiPPID.findMany({
|
||||
where: { deletedAt: null, isActive: true }
|
||||
})
|
||||
// ❌ Return kosong! Karena deletedAt sudah ter-set
|
||||
```
|
||||
|
||||
**Rekomendasi:** Fix schema:
|
||||
```prisma
|
||||
model VisiMisiPPID {
|
||||
id String @id @default(cuid())
|
||||
visi String @db.Text
|
||||
misi 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:**
|
||||
```typescript
|
||||
// Line ~85-95
|
||||
<Text
|
||||
ta={{ base: "center", md: "justify" }}
|
||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }} // ❌ No sanitization
|
||||
style={{ ... }}
|
||||
/>
|
||||
|
||||
// Line ~105-115 (Misi)
|
||||
<Text
|
||||
ta={"justify"}
|
||||
dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }} // ❌ No sanitization
|
||||
style={{ ... }}
|
||||
/>
|
||||
```
|
||||
|
||||
**Risk:**
|
||||
- XSS attack jika admin input script malicious
|
||||
- Bisa inject iframe, script tag, dll
|
||||
- Security vulnerability
|
||||
|
||||
**Rekomendasi:** Gunakan DOMPurify atau library sanitization:
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// Sanitize sebelum render
|
||||
const sanitizedVisi = DOMPurify.sanitize(listVisiMisi.findById.data.visi);
|
||||
const sanitizedMisi = DOMPurify.sanitize(listVisiMisi.findById.data.misi);
|
||||
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedVisi }}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
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 Visi Misi (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:
|
||||
```typescript
|
||||
const submit = () => {
|
||||
// Check if data has changed
|
||||
if (formData.visi === originalData.visi && formData.misi === originalData.misi) {
|
||||
toast.info('Tidak ada perubahan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation
|
||||
const confirmed = window.confirm('Apakah Anda yakin ingin mengubah Visi Misi PPID?');
|
||||
if (!confirmed) return;
|
||||
|
||||
// Then save...
|
||||
};
|
||||
```
|
||||
|
||||
**Priority:** 🔴 Medium
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟡 MEDIUM**
|
||||
|
||||
#### **4. Console.log di Production**
|
||||
|
||||
**Lokasi:** `src/app/admin/(dashboard)/_state/ppid/visi_misi_ppid/visimisiPPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~40
|
||||
console.error((error as Error).message);
|
||||
|
||||
// Line ~65
|
||||
console.error((error as Error).message);
|
||||
```
|
||||
|
||||
**Rekomendasi:** Gunakan conditional logging:
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
// Line ~120-130
|
||||
<Button
|
||||
onClick={submit}
|
||||
radius="md"
|
||||
size="md"
|
||||
disabled={!isFormValid() || isSubmitting}
|
||||
// ...
|
||||
>
|
||||
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Issue:** Button tidak check `visiMisi.update.loading` dari global state.
|
||||
|
||||
**Rekomendasi:** Check both states:
|
||||
```typescript
|
||||
disabled={!isFormValid() || isSubmitting || visiMisi.update.loading}
|
||||
{isSubmitting || visiMisi.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/visi_misi_ppid/visimisiPPID.ts`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~7
|
||||
const templateForm = z.object({
|
||||
misi: z.string().min(3, "Misi minimal 3 karakter"), // ⚠️ Generic
|
||||
visi: z.string().min(3, "Visi minimal 3 karakter"), // ⚠️ Generic
|
||||
});
|
||||
```
|
||||
|
||||
**Rekomendasi:** More specific error messages:
|
||||
```typescript
|
||||
const templateForm = z.object({
|
||||
misi: z.string().min(3, "Misi PPID minimal 3 karakter"),
|
||||
visi: z.string().min(3, "Visi PPID minimal 3 karakter"),
|
||||
});
|
||||
```
|
||||
|
||||
**Priority:** 🟡 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
### **🟢 LOW (Minor Polish)**
|
||||
|
||||
#### **7. Missing Change Detection**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~70-80
|
||||
const submit = () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
if (visiMisi.findById.data) {
|
||||
// update nilai global hanya saat submit
|
||||
visiMisi.findById.data.visi = formData.visi;
|
||||
visiMisi.findById.data.misi = formData.misi;
|
||||
visiMisi.update.save(visiMisi.findById.data);
|
||||
}
|
||||
router.push('/admin/ppid/visi-misi-ppid');
|
||||
} catch (error) {
|
||||
console.error("Error updating visi misi:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui visi misi");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Issue:** Tidak ada check apakah data sudah berubah. User bisa save tanpa perubahan.
|
||||
|
||||
**Rekomendasi:** Add change detection:
|
||||
```typescript
|
||||
const submit = () => {
|
||||
// Check if data has changed
|
||||
if (formData.visi === originalData.visi && formData.misi === originalData.misi) {
|
||||
toast.info('Tidak ada perubahan');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
// ... rest of save logic
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **8. Editor - Duplicate useEffect**
|
||||
|
||||
**Lokasi:** `PPIDTextEditor.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~30-35
|
||||
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:
|
||||
```typescript
|
||||
const editor = useEditor({
|
||||
extensions: [...],
|
||||
immediatelyRender: false,
|
||||
content: initialContent || '<p></p>', // ✅ Set content directly
|
||||
onUpdate: ({editor}) => {
|
||||
onChange(editor.getHTML())
|
||||
},
|
||||
editorProps: {
|
||||
// Optional: handle content updates better
|
||||
}
|
||||
});
|
||||
|
||||
// Remove useEffect completely
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **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:
|
||||
```typescript
|
||||
if (visiMisi.findById.error) {
|
||||
return (
|
||||
<Alert icon={<IconAlertCircle />} color="red">
|
||||
<Text fw="bold">Error</Text>
|
||||
<Text>{visiMisi.findById.error}</Text>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **10. Preview Page - Hardcoded Moto PPID**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~60-70
|
||||
<Text
|
||||
ta="center"
|
||||
fz={{ base: 'sm', md: 'md' }}
|
||||
lh={{ base: 1.5, md: 1.5 }}
|
||||
mt="sm"
|
||||
c="black"
|
||||
>
|
||||
MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN
|
||||
</Text>
|
||||
```
|
||||
|
||||
**Issue:** Moto PPID hardcoded di UI. Seharusnya dari database/config.
|
||||
|
||||
**Rekomendasi:** Move to database or config file:
|
||||
```typescript
|
||||
// Add to schema
|
||||
model VisiMisiPPID {
|
||||
// ...
|
||||
moto String? @db.Text
|
||||
}
|
||||
|
||||
// Or use config
|
||||
const PPID_MOTO = "MEMBERIKAN INFORMASI YANG CEPAT, MUDAH, TEPAT DAN TRANSPARAN";
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Medium (perlu schema change)
|
||||
|
||||
---
|
||||
|
||||
#### **11. Title Order Inconsistency**
|
||||
|
||||
**Lokasi:** `page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~45
|
||||
<Title order={3} ...>Preview Visi Misi PPID</Title>
|
||||
|
||||
// Line ~65
|
||||
<Title order={2} ...>MOTO PPID DESA DARMASABA</Title>
|
||||
|
||||
// Line ~80
|
||||
<Title order={2} ...>VISI PPID</Title>
|
||||
|
||||
// Line ~100
|
||||
<Title order={2} ...>MISI PPID</Title>
|
||||
```
|
||||
|
||||
**Issue:** Title hierarchy agak confusing. Page title (order 3) lebih kecil dari section titles (order 2).
|
||||
|
||||
**Rekomendasi:** Samakan hierarchy:
|
||||
```typescript
|
||||
// Page title: order={2}
|
||||
// Section titles: order={3}
|
||||
```
|
||||
|
||||
**Priority:** 🟢 Low
|
||||
**Effort:** Low
|
||||
|
||||
---
|
||||
|
||||
#### **12. Missing Toast Success After Save**
|
||||
|
||||
**Lokasi:** `edit/page.tsx`
|
||||
|
||||
**Masalah:**
|
||||
```typescript
|
||||
// Line ~70-85
|
||||
const submit = () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
if (visiMisi.findById.data) {
|
||||
visiMisi.findById.data.visi = formData.visi;
|
||||
visiMisi.findById.data.misi = formData.misi;
|
||||
visiMisi.update.save(visiMisi.findById.data);
|
||||
}
|
||||
router.push('/admin/ppid/visi-misi-ppid'); // ✅ Redirect tanpa toast
|
||||
} catch (error) {
|
||||
console.error("Error updating visi misi:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui visi misi");
|
||||
} 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:
|
||||
```typescript
|
||||
const submit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
if (visiMisi.findById.data) {
|
||||
visiMisi.findById.data.visi = formData.visi;
|
||||
visiMisi.findById.data.misi = formData.misi;
|
||||
await visiMisi.update.save(visiMisi.findById.data);
|
||||
toast.success("Visi Misi berhasil diperbarui!");
|
||||
setTimeout(() => {
|
||||
router.push('/admin/ppid/visi-misi-ppid');
|
||||
}, 1000); // Wait 1 second for toast to show
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating visi misi:", error);
|
||||
toast.error("Terjadi kesalahan saat memperbarui visi misi");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**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 | Hardcoded Moto PPID | UI | Low | Medium | Optional |
|
||||
| 🟢 L | Title order inconsistency | UI | Low | Low | Optional |
|
||||
| 🟢 L | Missing toast success timing | UI | Low | Low | Optional |
|
||||
|
||||
---
|
||||
|
||||
## ✅ KESIMPULAN
|
||||
|
||||
### **Overall Quality: 🟢 BAIK (8.5/10) - CLEANEST MODULE!**
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ UI/UX clean & responsive
|
||||
2. ✅ **Rich Text Editor** full-featured (Tiptap)
|
||||
3. ✅ **Modular form components** (Visi, Misi)
|
||||
4. ✅ **State management BEST PRACTICES** - **ONLY MODULE YANG 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. ✅ `immediatelyRender: false` untuk menghindari hydration mismatch
|
||||
|
||||
**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: Polish minor issues** - 30 menit
|
||||
|
||||
---
|
||||
|
||||
## 📈 COMPARISON WITH OTHER MODULES
|
||||
|
||||
| Module | Fetch Pattern | State | Edit Reset | Rich Text | HTML Injection | deletedAt | Overall |
|
||||
|--------|--------------|-------|------------|-----------|----------------|-----------|---------|
|
||||
| Profil | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | ⚠️ Present | ⚠️ Issue | 🟢 |
|
||||
| Desa Anti Korupsi | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Present | ⚠️ Present | ⚠️ Issue | 🟢 |
|
||||
| SDGs Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | ⚠️ Issue | 🟢 |
|
||||
| APBDes | ⚠️ Mixed | ⚠️ Good | ✅ Good | ❌ None | N/A | ✅ Good | 🟢 |
|
||||
| Prestasi Desa | ⚠️ Mixed | ⚠️ Good | ✅ Good | ✅ Present | ⚠️ Present | ❌ WRONG | 🟢 |
|
||||
| PPID Profil | ⚠️ Mixed | ✅ **Best** | ✅ **Excellent** | ✅ **Best** | ⚠️ Present | ❌ WRONG | 🟢⭐ |
|
||||
| Struktur PPID | ⚠️ Mixed | ✅ Good | ✅ Good | ✅ Present | ⚠️ Present | ⚠️ Inconsistent | 🟢 |
|
||||
| **Visi Misi PPID** | ✅ **100% ApiFetch!** | ✅ **Best** | ✅ Good | ✅ Present | ⚠️ Present | ❌ WRONG | 🟢⭐⭐ |
|
||||
|
||||
**Visi Misi PPID Highlights:**
|
||||
- ✅ **ONLY MODULE** yang 100% konsisten pakai ApiFetch! (NO fetch manual!)
|
||||
- ✅ **CLEANEST CODE** - Simple, straightforward, no complexity
|
||||
- ✅ **Rich text validation** paling comprehensive (check empty content)
|
||||
- ✅ **Best state management** pattern (ApiFetch consistency)
|
||||
- ⚠️ **Same deletedAt issue** seperti modul PPID lain
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UNIQUE FEATURES OF VISI MISI PPID MODULE
|
||||
|
||||
**Simplest & Cleanest Module:**
|
||||
1. ✅ **100% ApiFetch consistency** - NO fetch manual sama sekali! (UNIQUE!)
|
||||
2. ✅ **Simple single record pattern** - Only 2 fields (visi, misi)
|
||||
3. ✅ **Rich text validation** - Check empty content after remove HTML tags
|
||||
4. ✅ **Modular editor components** - VisiPPID, MisiPPID separate
|
||||
5. ✅ **No file upload** - Simplest form (text only)
|
||||
|
||||
**Best Practices:**
|
||||
1. ✅ **ApiFetch 100%** - Best practice untuk API consistency
|
||||
2. ✅ **Loading state management** proper (dengan finally block)
|
||||
3. ✅ **Rich text validation** comprehensive
|
||||
4. ✅ **Original data tracking** untuk reset form
|
||||
5. ✅ **`immediatelyRender: false`** - Avoid hydration mismatch
|
||||
|
||||
**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:** **Visi Misi PPID adalah MODULE PALING CLEAN** dengan codebase paling simple dan **SATU-SATUNYA MODULE YANG 100% PAKAI ApiFetch** (no fetch manual sama sekali!). Module ini bisa jadi **REFERENCE** untuk API consistency!
|
||||
|
||||
**Unique Strengths:**
|
||||
1. ✅ **100% ApiFetch** - Best API consistency (NO fetch manual!)
|
||||
2. ✅ **Simple & clean** - No unnecessary complexity
|
||||
3. ✅ **Rich text validation** - Most comprehensive
|
||||
4. ✅ **Best state management** pattern
|
||||
|
||||
**Priority Action:**
|
||||
```diff
|
||||
🔴 FIX INI SEKARANG (30 MENIT + MIGRATION):
|
||||
File: prisma/schema.prisma
|
||||
Line: 374
|
||||
|
||||
model VisiMisiPPID {
|
||||
id String @id @default(cuid())
|
||||
visi String @db.Text
|
||||
misi 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_visimisi_ppid
|
||||
```
|
||||
|
||||
```diff
|
||||
🔴 FIX HTML INJECTION (30 MENIT):
|
||||
File: page.tsx
|
||||
+ import DOMPurify from 'dompurify';
|
||||
|
||||
// Line ~85
|
||||
- dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.visi }}
|
||||
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.visi) }}
|
||||
|
||||
// Line ~105
|
||||
- dangerouslySetInnerHTML={{ __html: listVisiMisi.findById.data.misi }}
|
||||
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(listVisiMisi.findById.data.misi) }}
|
||||
```
|
||||
|
||||
Setelah fix critical issues, module ini **PRODUCTION-READY** dan bisa jadi **REFERENCE untuk API CONSISTENCY**! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📚 RECOMMENDED AS REFERENCE FOR OTHER MODULES
|
||||
|
||||
**Visi Misi PPID Module adalah BEST PRACTICE untuk:**
|
||||
1. ✅ **API consistency** - 100% ApiFetch, NO fetch manual!
|
||||
2. ✅ **Simple state management** - Clean, straightforward
|
||||
3. ✅ **Rich text validation** - Check empty content pattern
|
||||
4. ✅ **Modular editor components** - Separate Visi & Misi
|
||||
|
||||
**Modules lain bisa belajar dari Visi Misi PPID:**
|
||||
- **ALL MODULES:** Use ApiFetch consistently (NO fetch manual!)
|
||||
- **ALL MODULES:** Keep it simple (avoid unnecessary complexity)
|
||||
- **Rich Text Modules:** Implement empty content validation
|
||||
- **ALL MODULES:** Proper loading state management
|
||||
|
||||
---
|
||||
|
||||
**File Location:** `QC/PPID/QC-VISI-MISI-PPID-MODULE.md` 📄
|
||||
232
QWEN.md
Normal file
232
QWEN.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# Desa Darmasaba - Village Management System
|
||||
|
||||
## Project Overview
|
||||
|
||||
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
|
||||
|
||||
### Key Technologies
|
||||
- **Framework**: Next.js 15 with App Router
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Styling**: Mantine UI components with custom CSS
|
||||
- **Backend**: Elysia.js API server integrated with Next.js
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **State Management**: Valtio for global state
|
||||
- **Authentication**: JWT with iron-session
|
||||
|
||||
### Architecture
|
||||
The application follows a modular architecture with:
|
||||
- A main frontend built with Next.js and Mantine UI
|
||||
- An integrated Elysia.js API server for backend operations
|
||||
- Prisma ORM for database interactions
|
||||
- File storage integration with Seafile
|
||||
- Multiple domain-specific modules (PPID, health, security, education, etc.)
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (with Bun runtime)
|
||||
- PostgreSQL database
|
||||
- Seafile server for file storage
|
||||
|
||||
### Setup Instructions
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Set up environment variables in `.env.local`:
|
||||
```
|
||||
DATABASE_URL=your_postgresql_connection_string
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_BASE_URL=your_seafile_base_url
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
SEAFILE_URL=your_seafile_api_url
|
||||
WIBU_UPLOAD_DIR=your_upload_directory
|
||||
```
|
||||
|
||||
3. Generate Prisma client:
|
||||
```bash
|
||||
bunx prisma generate
|
||||
```
|
||||
|
||||
4. Push database schema:
|
||||
```bash
|
||||
bunx prisma db push
|
||||
```
|
||||
|
||||
5. Seed the database:
|
||||
```bash
|
||||
bun run prisma/seed.ts
|
||||
```
|
||||
|
||||
6. Run the development server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
- `bun run dev` - Start development server
|
||||
- `bun run build` - Build for production
|
||||
- `bun run start` - Start production server
|
||||
- `bun run prisma/seed.ts` - Run database seeding
|
||||
- `bunx prisma generate` - Generate Prisma client
|
||||
- `bunx prisma db push` - Push schema changes to database
|
||||
- `bunx prisma studio` - Open Prisma Studio GUI
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Code Structure
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js app router pages
|
||||
│ ├── admin/ # Admin dashboard pages
|
||||
│ ├── api/ # API routes with Elysia.js
|
||||
│ ├── darmasaba/ # Public-facing village pages
|
||||
│ └── ...
|
||||
├── con/ # Constants and configuration
|
||||
├── hooks/ # React hooks
|
||||
├── lib/ # Utility functions and configurations
|
||||
├── middlewares/ # Next.js middleware
|
||||
├── state/ # Global state management
|
||||
├── store/ # Additional state management
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
### Import Conventions
|
||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||
- Group imports: external libraries first, then internal modules
|
||||
- Keep import statements organized and remove unused imports
|
||||
|
||||
```typescript
|
||||
// External libraries
|
||||
import { useState } from 'react'
|
||||
import { Button, Stack } from '@mantine/core'
|
||||
|
||||
// Internal modules
|
||||
import ApiFetch from '@/lib/api-fetch'
|
||||
import { MyComponent } from '@/components/my-component'
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- Strict mode enabled (`"strict": true`)
|
||||
- Target: ES2017
|
||||
- Module resolution: bundler
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
|
||||
### Naming Conventions
|
||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
||||
- **Variables/Functions**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE
|
||||
- **Database Models**: PascalCase (Prisma convention)
|
||||
|
||||
### Error Handling
|
||||
- Use try-catch blocks for async operations
|
||||
- Implement proper error boundaries in React components
|
||||
- Log errors appropriately without exposing sensitive data
|
||||
- Use Zod for runtime validation and type safety
|
||||
|
||||
### API Structure
|
||||
- Backend uses Elysia.js with TypeScript
|
||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
||||
- Use treaty client for type-safe API calls
|
||||
- Follow RESTful conventions for endpoints
|
||||
- Include proper HTTP status codes and error responses
|
||||
|
||||
### Database Operations
|
||||
- Use Prisma client from `@/lib/prisma.ts`
|
||||
- Database connection includes graceful shutdown handling
|
||||
- Use transactions for complex operations
|
||||
- Implement proper error handling for database queries
|
||||
|
||||
### Component Guidelines
|
||||
- Use functional components with hooks
|
||||
- Implement proper prop types with TypeScript interfaces
|
||||
- Use Mantine components for UI consistency
|
||||
- Follow atomic design principles when possible
|
||||
- Add loading states and error states for async operations
|
||||
|
||||
### State Management
|
||||
- Use Valtio proxies for global state
|
||||
- Keep local state in components when possible
|
||||
- Use SWR for server state caching
|
||||
- Implement optimistic updates for better UX
|
||||
|
||||
### Styling
|
||||
- Primary: Mantine UI components
|
||||
- Use Mantine theme system for customization
|
||||
- Custom CSS should be minimal and scoped
|
||||
- Follow responsive design principles
|
||||
- Use semantic HTML5 elements
|
||||
|
||||
### Security Practices
|
||||
- Validate all user inputs with Zod schemas
|
||||
- Use JWT tokens for authentication
|
||||
- Implement proper CORS configuration
|
||||
- Never expose database credentials or API keys
|
||||
- Use HTTPS in production
|
||||
- Implement rate limiting for sensitive endpoints
|
||||
|
||||
### Performance Considerations
|
||||
- Use Next.js Image optimization
|
||||
- Implement proper caching strategies
|
||||
- Use React.memo for expensive components
|
||||
- Optimize bundle size with dynamic imports
|
||||
- Use Prisma query optimization
|
||||
|
||||
## Domain Modules
|
||||
|
||||
The application is organized into several domain modules:
|
||||
|
||||
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
|
||||
2. **Health**: Health facilities, programs, emergency response, disease information
|
||||
3. **Security**: Community security, emergency contacts, crime prevention
|
||||
4. **Education**: Schools, scholarships, educational programs
|
||||
5. **Economy**: Local markets, BUMDes, employment data
|
||||
6. **Environment**: Environmental data, conservation, waste management
|
||||
7. **Innovation**: Digital services, innovation programs
|
||||
8. **Culture**: Village traditions, music, cultural preservation
|
||||
|
||||
Each module has its own section in both the admin panel and public-facing areas.
|
||||
|
||||
## File Storage Integration
|
||||
|
||||
The application integrates with Seafile for file storage, with specific handling for:
|
||||
- Images and documents
|
||||
- Public sharing capabilities
|
||||
- CDN URL generation
|
||||
- Batch processing of assets
|
||||
|
||||
## Testing
|
||||
|
||||
Currently no formal test framework is configured. When adding tests:
|
||||
- Consider Jest or Vitest for unit testing
|
||||
- Use Playwright for E2E testing
|
||||
- Update this section with specific test commands
|
||||
|
||||
## Deployment
|
||||
|
||||
The application includes deployment scripts in the `NOTE.md` file that outline:
|
||||
- Automated deployment with GitHub API integration
|
||||
- Environment-specific configurations
|
||||
- PM2 process management
|
||||
- Release management with versioning
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
- **API endpoints returning 404**: Check that environment variables are properly configured
|
||||
- **Database connection errors**: Verify DATABASE_URL in environment variables
|
||||
- **File upload issues**: Ensure Seafile integration is properly configured
|
||||
- **Build failures**: Run `bunx prisma generate` before building
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Always run type checking before committing: `bunx tsc --noEmit`
|
||||
2. Run linting to catch style issues: `bun run eslint .`
|
||||
3. Test database changes with `bunx prisma db push`
|
||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||
5. Check environment variables are properly configured
|
||||
6. Verify responsive design on different screen sizes
|
||||
30
__tests__/api/fileStorage.test.ts
Normal file
30
__tests__/api/fileStorage.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
describe('FileStorage API', () => {
|
||||
it('should fetch a list of files from /api/fileStorage/findMany', async () => {
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const responseBody = response.data as any;
|
||||
|
||||
expect(responseBody.data).toBeInstanceOf(Array);
|
||||
expect(responseBody.data.length).toBe(2);
|
||||
expect(responseBody.data[0].name).toBe('file1.jpg');
|
||||
});
|
||||
|
||||
it('should create a file using /api/fileStorage/create', async () => {
|
||||
const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
|
||||
const response = await ApiFetch.api.fileStorage.create.post({
|
||||
file: mockFile,
|
||||
name: 'hello.png',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const responseBody = response.data as any;
|
||||
|
||||
expect(responseBody.data.realName).toBe('hello.png');
|
||||
expect(responseBody.data.id).toBe('3');
|
||||
});
|
||||
});
|
||||
11
__tests__/e2e/homepage.spec.ts
Normal file
11
__tests__/e2e/homepage.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('homepage has correct title and content', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for the redirect to /darmasaba
|
||||
await page.waitForURL('/darmasaba');
|
||||
|
||||
// Check for the main heading
|
||||
await expect(page.getByText('DARMASABA', { exact: true })).toBeVisible();
|
||||
});
|
||||
43
__tests__/mocks/handlers.ts
Normal file
43
__tests__/mocks/handlers.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const handlers = [
|
||||
http.get('http://localhost:3000/api/fileStorage/findMany', () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{ id: '1', name: 'file1.jpg', url: '/uploads/file1.jpg' },
|
||||
{ id: '2', name: 'file2.png', url: '/uploads/file2.png' },
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
}),
|
||||
http.post('http://localhost:3000/api/fileStorage/create', async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const file = data.get('file') as File;
|
||||
const name = data.get('name') as string;
|
||||
|
||||
if (!file) {
|
||||
return new HttpResponse(null, { status: 400 });
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
id: '3',
|
||||
name: 'generated-nanoid',
|
||||
path: `/uploads/generated-nanoid`,
|
||||
link: `/uploads/generated-nanoid`,
|
||||
realName: name,
|
||||
mimeType: file.type,
|
||||
category: "uncategorized",
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
}
|
||||
});
|
||||
}),
|
||||
];
|
||||
4
__tests__/mocks/server.ts
Normal file
4
__tests__/mocks/server.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { handlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
7
__tests__/setup.ts
Normal file
7
__tests__/setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { server } from './mocks/server';
|
||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
BIN
foldergambar/desa/name.png
Normal file
BIN
foldergambar/desa/name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
foldergambar/desa/ppid/profile-ppid/name.png
Normal file
BIN
foldergambar/desa/ppid/profile-ppid/name.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 922 KiB |
99
gambar.ttx
Normal file
99
gambar.ttx
Normal file
@@ -0,0 +1,99 @@
|
||||
type DirItem = {
|
||||
type: "file" | "dir";
|
||||
name: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
// type FileDownloadResponse = {
|
||||
// url: string;
|
||||
// };
|
||||
|
||||
// const TOKEN = "20a19f4a04032215d50ce53292e6abdd38b9f806";
|
||||
// const REPO_ID = "8814bfe1-30d5-4e77-ab36-3122fa59a022";
|
||||
// const DIR_TARGET = "image";
|
||||
|
||||
// const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
||||
|
||||
const TOKEN = process.env.SEAFILE_TOKEN!;
|
||||
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||
|
||||
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
|
||||
const DIR_TARGET = "asset-web";
|
||||
|
||||
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
||||
|
||||
const headers = {
|
||||
Authorization: `Token ${TOKEN}`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Ambil list file di directory
|
||||
*/
|
||||
async function getDirItems(): Promise<DirItem[]> {
|
||||
const res = await fetch(
|
||||
`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed get dir items: ${res.statusText}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil download URL file
|
||||
*/
|
||||
async function getDownloadUrl(filePath: string): Promise<string> {
|
||||
|
||||
|
||||
const res = await fetch(
|
||||
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed get file url: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil semua download URL dari target dir
|
||||
*/
|
||||
async function getAllDownloadUrls() {
|
||||
const items = await getDirItems();
|
||||
|
||||
const files = items.filter((item) => item.type === "file");
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = `${DIR_TARGET}/${file.name}`;
|
||||
const url = await getDownloadUrl(filePath);
|
||||
return {
|
||||
name: file.name,
|
||||
path: filePath,
|
||||
downloadUrl: url,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// contoh eksekusi
|
||||
(async () => {
|
||||
try {
|
||||
console.log("ambil gambar")
|
||||
const urls = await getAllDownloadUrls();
|
||||
await Bun.write("list_image2.json", JSON.stringify(urls))
|
||||
console.log("selesai !")
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {},
|
||||
allowedDevOrigins: [
|
||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||
"http://localhost:3000", // akses lokal
|
||||
],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
|
||||
102
package.json
102
package.json
@@ -1,65 +1,131 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"prisma:seed": "bun run prisma/seed.ts"
|
||||
"test:api": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test": "bun run test:api && bun run test:e2e"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "bun run prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cubejs-client/core": "^0.31.0",
|
||||
"@elysiajs/cookie": "^0.8.0",
|
||||
"@elysiajs/cors": "^1.2.0",
|
||||
"@elysiajs/eden": "^1.2.0",
|
||||
"@elysiajs/eden": "^1.3.2",
|
||||
"@elysiajs/jwt": "^1.3.2",
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"@elysiajs/stream": "^1.1.0",
|
||||
"@elysiajs/swagger": "^1.2.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/carousel": "^7.16.2",
|
||||
"@mantine/charts": "^7.17.1",
|
||||
"@mantine/core": "^7.16.2",
|
||||
"@mantine/dropzone": "^7.17.0",
|
||||
"@mantine/hooks": "^7.16.2",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/dates": "^8.1.0",
|
||||
"@mantine/dropzone": "^8.1.1",
|
||||
"@mantine/form": "^8.1.0",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/modals": "^8.3.6",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@paljs/types": "^8.1.0",
|
||||
"@prisma/client": "^6.3.1",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"@tiptap/extension-highlight": "^2.11.7",
|
||||
"@tiptap/extension-link": "^2.11.7",
|
||||
"@tiptap/extension-subscript": "^2.11.7",
|
||||
"@tiptap/extension-superscript": "^2.11.7",
|
||||
"@tiptap/extension-text-align": "^2.11.7",
|
||||
"@tiptap/extension-underline": "^2.11.7",
|
||||
"@tiptap/pm": "^2.11.7",
|
||||
"@tiptap/react": "^2.11.7",
|
||||
"@tiptap/starter-kit": "^2.11.7",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"add": "^2.0.6",
|
||||
"adm-zip": "^0.5.16",
|
||||
"animate.css": "^4.1.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"bun": "^1.2.2",
|
||||
"elysia": "^1.2.12",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
"framer-motion": "^12.4.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"classnames": "^2.5.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"colors": "^1.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.2.3",
|
||||
"elysia": "^1.3.5",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"form-data": "^4.0.2",
|
||||
"framer-motion": "^12.23.5",
|
||||
"get-port": "^7.1.0",
|
||||
"iron-session": "^8.0.4",
|
||||
"jose": "^6.1.0",
|
||||
"jotai": "^2.12.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"list": "^2.0.19",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^3.0.2",
|
||||
"motion": "^12.4.1",
|
||||
"nanoid": "^5.1.0",
|
||||
"next": "15.1.6",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "^15.5.2",
|
||||
"next-view-transitions": "^0.3.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^7.0.10",
|
||||
"p-limit": "^6.2.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.6",
|
||||
"prisma": "^6.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-exif-orientation-img": "^0.1.5",
|
||||
"react-international-phone": "^4.6.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-simple-toasts": "^6.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"readdirp": "^4.1.1",
|
||||
"recharts": "2",
|
||||
"recharts": "^2.15.3",
|
||||
"sharp": "^0.34.3",
|
||||
"swr": "^2.3.2",
|
||||
"valtio": "^2.1.3"
|
||||
"uuid": "^11.1.0",
|
||||
"valtio": "^2.1.3",
|
||||
"zlib": "^1.0.5",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@types/cli-progress": "^3.11.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"jsdom": "^28.0.0",
|
||||
"msw": "^2.12.9",
|
||||
"parcel": "^2.6.2",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e7]:
|
||||
- button "Darmasaba Logo" [ref=e8] [cursor=pointer]:
|
||||
- img "Darmasaba Logo" [ref=e10]
|
||||
- button "PPID" [ref=e11] [cursor=pointer]:
|
||||
- generic [ref=e13]: PPID
|
||||
- button "Desa" [ref=e14] [cursor=pointer]:
|
||||
- generic [ref=e16]: Desa
|
||||
- button "Kesehatan" [ref=e17] [cursor=pointer]:
|
||||
- generic [ref=e19]: Kesehatan
|
||||
- button "Keamanan" [ref=e20] [cursor=pointer]:
|
||||
- generic [ref=e22]: Keamanan
|
||||
- button "Ekonomi" [ref=e23] [cursor=pointer]:
|
||||
- generic [ref=e25]: Ekonomi
|
||||
- button "Inovasi" [ref=e26] [cursor=pointer]:
|
||||
- generic [ref=e28]: Inovasi
|
||||
- button "Lingkungan" [ref=e29] [cursor=pointer]:
|
||||
- generic [ref=e31]: Lingkungan
|
||||
- button "Pendidikan" [ref=e32] [cursor=pointer]:
|
||||
- generic [ref=e34]: Pendidikan
|
||||
- button "Musik" [ref=e35] [cursor=pointer]:
|
||||
- generic [ref=e37]: Musik
|
||||
- button [ref=e38] [cursor=pointer]:
|
||||
- img [ref=e40]
|
||||
- generic [ref=e46]:
|
||||
- generic [ref=e51]:
|
||||
- generic [ref=e52]:
|
||||
- generic [ref=e53]:
|
||||
- img "Logo Darmasaba" [ref=e55]
|
||||
- img "Logo Pudak" [ref=e57]
|
||||
- generic [ref=e63]:
|
||||
- generic [ref=e65]:
|
||||
- generic [ref=e66]:
|
||||
- img [ref=e67]
|
||||
- paragraph [ref=e71]: Jam Operasional
|
||||
- generic [ref=e72]:
|
||||
- generic [ref=e74]: Buka
|
||||
- paragraph [ref=e75]: 07:30 - 15:30
|
||||
- generic [ref=e77]:
|
||||
- generic [ref=e78]:
|
||||
- img [ref=e79]
|
||||
- paragraph [ref=e82]: Hari Ini
|
||||
- generic [ref=e83]:
|
||||
- paragraph [ref=e84]: Status Kantor
|
||||
- paragraph [ref=e85]: Sedang Beroperasi
|
||||
- paragraph [ref=e95]: Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
|
||||
- generic [ref=e102]:
|
||||
- generic [ref=e103]: Browser Anda tidak mendukung video.
|
||||
- generic [ref=e106]:
|
||||
- heading "Penghargaan Desa" [level=2] [ref=e107]
|
||||
- paragraph [ref=e110]: Sedang memuat data penghargaan...
|
||||
- button "Lihat semua penghargaan" [ref=e111] [cursor=pointer]:
|
||||
- generic [ref=e112]:
|
||||
- paragraph [ref=e114]: Lihat Semua Penghargaan
|
||||
- img [ref=e116]
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e121]:
|
||||
- heading "Layanan" [level=1] [ref=e122]
|
||||
- paragraph [ref=e123]: Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!
|
||||
- link "Detail" [ref=e125] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/layanan
|
||||
- generic [ref=e127]: Detail
|
||||
- separator [ref=e129]
|
||||
- generic [ref=e130]:
|
||||
- generic [ref=e131]:
|
||||
- paragraph [ref=e132]: Potensi Desa
|
||||
- paragraph [ref=e133]: Jelajahi berbagai potensi dan peluang yang dimiliki desa. Fitur ini membantu warga maupun pemerintah desa dalam merencanakan dan mengembangkan program berbasis kekuatan lokal.
|
||||
- paragraph [ref=e136]: Sedang memuat potensi desa...
|
||||
- button "Lihat Semua Potensi" [ref=e139] [cursor=pointer]:
|
||||
- generic [ref=e140]:
|
||||
- generic [ref=e141]: Lihat Semua Potensi
|
||||
- img [ref=e143]
|
||||
- separator [ref=e146]
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- paragraph [ref=e150]: Desa Anti Korupsi
|
||||
- paragraph [ref=e151]: Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola secara terbuka dengan melibatkan warga dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan sesuai kebutuhan masyarakat.
|
||||
- link "Selengkapnya" [ref=e153] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa-anti-korupsi/detail
|
||||
- generic [ref=e155]: Selengkapnya
|
||||
- paragraph [ref=e158]: Memuat Data...
|
||||
- generic [ref=e166]:
|
||||
- heading "SDGs Desa" [level=1] [ref=e168]
|
||||
- paragraph [ref=e169]: SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
|
||||
- generic [ref=e170]:
|
||||
- generic [ref=e171]:
|
||||
- img [ref=e172]
|
||||
- paragraph [ref=e175]: Data SDGs Desa belum tersedia
|
||||
- link "Jelajahi Semua Tujuan SDGs Desa" [ref=e177] [cursor=pointer]:
|
||||
- /url: /darmasaba/sdgs-desa
|
||||
- paragraph [ref=e180]: Jelajahi Semua Tujuan SDGs Desa
|
||||
- generic [ref=e181]:
|
||||
- generic [ref=e183]:
|
||||
- heading "APBDes" [level=1] [ref=e184]
|
||||
- paragraph [ref=e185]: Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.
|
||||
- link "Lihat Semua Data" [ref=e187] [cursor=pointer]:
|
||||
- /url: /darmasaba/apbdes
|
||||
- generic [ref=e189]: Lihat Semua Data
|
||||
- generic [ref=e191]:
|
||||
- paragraph [ref=e193]: Pilih Tahun APBDes
|
||||
- generic [ref=e194]:
|
||||
- textbox "Pilih Tahun APBDes" [ref=e195]:
|
||||
- /placeholder: Pilih tahun
|
||||
- generic:
|
||||
- img
|
||||
- paragraph [ref=e197]: Tidak ada data APBDes untuk tahun yang dipilih.
|
||||
- generic [ref=e202]:
|
||||
- heading "Prestasi Desa" [level=1] [ref=e203]
|
||||
- paragraph [ref=e204]: Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
|
||||
- link "Lihat Semua Prestasi" [ref=e205] [cursor=pointer]:
|
||||
- /url: /darmasaba/prestasi-desa
|
||||
- generic [ref=e207]: Lihat Semua Prestasi
|
||||
- button [ref=e211] [cursor=pointer]:
|
||||
- img [ref=e214]
|
||||
- button [ref=e219] [cursor=pointer]:
|
||||
- img [ref=e221]
|
||||
- generic [ref=e225]:
|
||||
- contentinfo [ref=e228]:
|
||||
- generic [ref=e230]:
|
||||
- generic [ref=e231]:
|
||||
- heading "Komitmen Layanan Kami" [level=2] [ref=e232]
|
||||
- generic [ref=e233]:
|
||||
- generic [ref=e234]:
|
||||
- paragraph [ref=e235]: "1. Transparansi:"
|
||||
- paragraph [ref=e236]: Pengelolaan dana desa dilakukan secara terbuka agar masyarakat dapat memahami dan memantau penggunaan anggaran.
|
||||
- generic [ref=e237]:
|
||||
- paragraph [ref=e238]: "2. Profesionalisme:"
|
||||
- paragraph [ref=e239]: Layanan desa diberikan secara cepat, adil, dan profesional demi kepuasan masyarakat.
|
||||
- generic [ref=e240]:
|
||||
- paragraph [ref=e241]: "3. Partisipasi:"
|
||||
- paragraph [ref=e242]: Masyarakat dilibatkan aktif dalam pengambilan keputusan demi pembangunan desa yang berhasil.
|
||||
- generic [ref=e243]:
|
||||
- paragraph [ref=e244]: "4. Inovasi:"
|
||||
- paragraph [ref=e245]: Kami terus berinovasi, termasuk melalui teknologi, agar layanan semakin mudah diakses.
|
||||
- generic [ref=e246]:
|
||||
- paragraph [ref=e247]: "5. Keadilan:"
|
||||
- paragraph [ref=e248]: Kebijakan dan program disusun untuk memberi manfaat yang merata bagi seluruh warga.
|
||||
- generic [ref=e249]:
|
||||
- paragraph [ref=e250]: "6. Pemberdayaan:"
|
||||
- paragraph [ref=e251]: Masyarakat didukung melalui pelatihan, pendampingan, dan pengembangan usaha lokal.
|
||||
- generic [ref=e252]:
|
||||
- paragraph [ref=e253]: "7. Ramah Lingkungan:"
|
||||
- paragraph [ref=e254]: Seluruh kegiatan pembangunan memperhatikan keberlanjutan demi menjaga alam dan kesehatan warga.
|
||||
- separator [ref=e255]
|
||||
- generic [ref=e256]:
|
||||
- heading "Visi Kami" [level=2] [ref=e257]
|
||||
- paragraph [ref=e258]: Dengan visi ini, kami berkomitmen menjadikan desa sebagai tempat yang aman, sejahtera, dan nyaman bagi seluruh warga.
|
||||
- paragraph [ref=e259]: Kami percaya kemajuan dimulai dari kerja sama antara pemerintah desa dan masyarakat, didukung tata kelola yang baik demi kepentingan bersama. Saran maupun keluhan dapat disampaikan melalui kontak di bawah ini.
|
||||
- generic [ref=e260]:
|
||||
- paragraph [ref=e261]: "\"Desa Kuat, Warga Sejahtera!\""
|
||||
- button "Logo Desa" [ref=e262] [cursor=pointer]:
|
||||
- generic [ref=e263]:
|
||||
- img "Logo Desa"
|
||||
- generic [ref=e265]:
|
||||
- generic [ref=e267]:
|
||||
- paragraph [ref=e268]: Tentang Darmasaba
|
||||
- paragraph [ref=e269]: Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||
- generic [ref=e270]:
|
||||
- link [ref=e271] [cursor=pointer]:
|
||||
- /url: https://www.facebook.com/DarmasabaDesaku
|
||||
- img [ref=e273]
|
||||
- link [ref=e275] [cursor=pointer]:
|
||||
- /url: https://www.instagram.com/ddarmasaba/
|
||||
- img [ref=e277]
|
||||
- link [ref=e280] [cursor=pointer]:
|
||||
- /url: https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg
|
||||
- img [ref=e282]
|
||||
- link [ref=e285] [cursor=pointer]:
|
||||
- /url: https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc
|
||||
- img [ref=e287]
|
||||
- generic [ref=e290]:
|
||||
- paragraph [ref=e291]: Layanan Desa
|
||||
- link "Administrasi Kependudukan" [ref=e292] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/layanan/
|
||||
- link "Layanan Sosial" [ref=e293] [cursor=pointer]:
|
||||
- /url: /darmasaba/ekonomi/program-kemiskinan
|
||||
- link "Pengaduan Masyarakat" [ref=e294] [cursor=pointer]:
|
||||
- /url: /darmasaba/keamanan/laporan-publik
|
||||
- link "Informasi Publik" [ref=e295] [cursor=pointer]:
|
||||
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||
- generic [ref=e297]:
|
||||
- paragraph [ref=e298]: Tautan Penting
|
||||
- link "Portal Badung" [ref=e299] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/berita/semua
|
||||
- link "E-Government" [ref=e300] [cursor=pointer]:
|
||||
- /url: /darmasaba/inovasi/desa-digital-smart-village
|
||||
- link "Transparansi" [ref=e301] [cursor=pointer]:
|
||||
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||
- generic [ref=e303]:
|
||||
- paragraph [ref=e304]: Berlangganan Info
|
||||
- paragraph [ref=e305]: Dapatkan kabar terbaru tentang program dan kegiatan desa langsung ke email Anda.
|
||||
- generic [ref=e306]:
|
||||
- generic [ref=e308]:
|
||||
- textbox "Masukkan email Anda" [ref=e309]
|
||||
- img [ref=e311]
|
||||
- button "Daftar" [ref=e314] [cursor=pointer]:
|
||||
- generic [ref=e316]: Daftar
|
||||
- separator [ref=e317]
|
||||
- paragraph [ref=e318]: © 2025 Desa Darmasaba. Hak cipta dilindungi.
|
||||
- region "Notifications Alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e324] [cursor=pointer]:
|
||||
- img [ref=e325]
|
||||
- alert [ref=e328]
|
||||
```
|
||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
25
playwright.config.ts
Normal file
25
playwright.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './__tests__/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'bun run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
/* Mobile first */
|
||||
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
71
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
71
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kategoriBerita from "../../../data/desa/berita/kategori-berita.json";
|
||||
import beritaJson from "../../../data/desa/berita/berita.json";
|
||||
|
||||
export async function seedBerita() {
|
||||
// ================== SUBMENU BERITA ========================
|
||||
console.log("🔄 Seeding Kategori Berita...");
|
||||
for (const k of kategoriBerita) {
|
||||
await prisma.kategoriBerita.upsert({
|
||||
where: {
|
||||
name: k.name, // ✅ cocok dengan @unique
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: k.id, // ✅ id tetap bisa disimpan
|
||||
name: k.name,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori berita success ...");
|
||||
|
||||
console.log("🔄 Seeding Berita...");
|
||||
|
||||
for (const b of beritaJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (b.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: b.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${b.judul}": ${b.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.berita.upsert({
|
||||
where: { id: b.id },
|
||||
update: {
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
content: b.content,
|
||||
kategoriBeritaId: b.kategoriBeritaId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: b.id,
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
content: b.content,
|
||||
kategoriBeritaId: b.kategoriBeritaId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Berita seeded: ${b.judul}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Berita seed selesai");
|
||||
}
|
||||
|
||||
40
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
40
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import foto from "../../../../data/desa/gallery/foto/foto.json";
|
||||
|
||||
export async function seedFoto() {
|
||||
console.log("🔄 Seeding Foto...");
|
||||
for (const f of foto) {
|
||||
let imagesId: string | null = null;
|
||||
|
||||
if (f.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: f.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for foto "${f.name}": ${f.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imagesId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.galleryFoto.upsert({
|
||||
where: { id: f.id },
|
||||
update: {
|
||||
name: f.name,
|
||||
deskripsi: f.deskripsi,
|
||||
imagesId,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
deskripsi: f.deskripsi,
|
||||
imagesId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Foto seeding completed");
|
||||
}
|
||||
25
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
25
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import galleryVideo from "../../../../data/desa/gallery/video/video.json";
|
||||
|
||||
export async function seedVideo() {
|
||||
console.log("🔄 Seeding Gallery Video...");
|
||||
for (const v of galleryVideo) {
|
||||
await prisma.galleryVideo.upsert({
|
||||
where: {
|
||||
id: v.id,
|
||||
},
|
||||
update: {
|
||||
name: v.judul,
|
||||
deskripsi: v.deskripsi,
|
||||
linkVideo: v.linkVideo,
|
||||
},
|
||||
create: {
|
||||
name: v.judul,
|
||||
deskripsi: v.deskripsi,
|
||||
linkVideo: v.linkVideo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("gallery video success ...");
|
||||
}
|
||||
128
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
128
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import pelayananSuratKeterangan from "../../../data/desa/layanan/pelayananSuratKeterangan.json";
|
||||
import pelayananTelunjukSaktiDesa from "../../../data/desa/layanan/pelayananTelunjukSaktiDesa.json";
|
||||
import pelayananPerizinanBerusaha from "../../../data/desa/layanan/pelayananPerizinanBerusaha.json";
|
||||
import pelayananPendudukNonPermanen from "../../../data/desa/layanan/pelayananPendudukNonPermanen.json";
|
||||
|
||||
export async function seedLayanan() {
|
||||
console.log("🔄 Seeding Pelayanan Surat Keterangan...");
|
||||
|
||||
for (const p of pelayananSuratKeterangan) {
|
||||
const existing = await prisma.pelayananSuratKeterangan.findUnique({
|
||||
where: { id: p.id },
|
||||
select: { imageId: true, image2Id: true }, // 📌 tambahkan image2Id
|
||||
});
|
||||
|
||||
// 1️⃣ Handle imageId
|
||||
let imageId = existing?.imageId ?? null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pelayanan surat keterangan 1 "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Handle image2Id
|
||||
let image2Id = existing?.image2Id ?? null;
|
||||
|
||||
if (p.image2Name) {
|
||||
const image2 = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.image2Name },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image2) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pelayanan surat keterangan 2 "${p.name}": ${p.image2Name}`,
|
||||
);
|
||||
} else {
|
||||
image2Id = image2.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ Upsert dengan kedua image
|
||||
await prisma.pelayananSuratKeterangan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
image2Id, // 📌 tambahkan ini
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
image2Id, // 📌 tambahkan ini
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pelayanan Surat Keterangan success...");
|
||||
|
||||
for (const p of pelayananTelunjukSaktiDesa) {
|
||||
await prisma.pelayananTelunjukSaktiDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
link: p.link,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
link: p.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pelayanan telunjuk sakti desa success ...");
|
||||
|
||||
for (const l of pelayananPerizinanBerusaha) {
|
||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
link: l.link,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
link: l.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("pelayanan perizinan berusaha success ...");
|
||||
|
||||
for (const l of pelayananPendudukNonPermanen) {
|
||||
await prisma.pelayananPendudukNonPermanen.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pelayanan penduduk non permanen success ...");
|
||||
}
|
||||
44
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
44
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import penghargaan from "../../../data/desa/penghargaan/penghargaan.json"
|
||||
|
||||
export async function seedPenghargaan() {
|
||||
console.log("🔄 Seeding Penghargaan...");
|
||||
for (const m of penghargaan) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for penghargaan "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.penghargaan.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
juara: m.juara,
|
||||
deskripsi: m.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
juara: m.juara,
|
||||
deskripsi: m.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("penghargaan success ...");
|
||||
}
|
||||
|
||||
43
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
43
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { safeSeedUnique } from "../../../safeseedUnique";
|
||||
import kategoriPengumuman from "../../../data/desa/pengumuman/kategori-pengumuman.json";
|
||||
import pengumuman from "../../../data/desa/pengumuman/pengumuman.json";
|
||||
|
||||
export async function seedPengumuman() {
|
||||
console.log("🔄 Seeding Kategori Pengumuman...");
|
||||
for (const c of kategoriPengumuman) {
|
||||
await safeSeedUnique(
|
||||
"categoryPengumuman",
|
||||
{ name: c.name }, // ✅ where clause
|
||||
{
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
console.log("kategori pengumuman success ...");
|
||||
|
||||
console.log("🔄 Seeding Pengumuman...");
|
||||
for (const p of pengumuman) {
|
||||
await prisma.pengumuman.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
content: p.content,
|
||||
categoryPengumumanId: p.categoryPengumumanId,
|
||||
},
|
||||
create: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
content: p.content,
|
||||
categoryPengumumanId: p.categoryPengumumanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("pengumuman success ...");
|
||||
}
|
||||
64
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
64
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kategoriPotensi from "../../../data/desa/potensi/kategori-potensi.json";
|
||||
import potensiDesa from "../../../data/desa/potensi/potensi-desa.json";
|
||||
|
||||
export async function seedPotensi() {
|
||||
console.log("🔄Seeding Kategori Potensi Desa ...");
|
||||
for (const c of kategoriPotensi) {
|
||||
await prisma.kategoriPotensi.upsert({
|
||||
where: {
|
||||
id: c.id,
|
||||
},
|
||||
update: {
|
||||
nama: c.nama,
|
||||
},
|
||||
create: {
|
||||
id: c.id,
|
||||
nama: c.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori Potensi success ...");
|
||||
|
||||
console.log("🔄 Seeding Potensi Desa...");
|
||||
for (const m of potensiDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for potensi desa "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.potensiDesa.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
content: m.content,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
content: m.content,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("potensi desa success ...");
|
||||
}
|
||||
168
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
168
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import lambangDesa from "../../../data/desa/profile/lambang_desa.json";
|
||||
import maskotDesa from "../../../data/desa/profile/maskot_desa.json";
|
||||
import profilePerbekel from "../../../data/desa/profile/profil_perbekel.json";
|
||||
import profileDesaImage from "../../../data/desa/profile/profileDesaImage.json";
|
||||
import sejarahDesa from "../../../data/desa/profile/sejarah_desa.json";
|
||||
import visiMisiDesa from "../../../data/desa/profile/visi_misi_desa.json";
|
||||
|
||||
export async function seedProfileDesa() {
|
||||
// =========== SEJARAH DESA ===========
|
||||
for (const l of sejarahDesa) {
|
||||
await prisma.sejarahDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("sejarah desa success ...");
|
||||
|
||||
// =========== VISI MISI DESA ===========
|
||||
for (const l of visiMisiDesa) {
|
||||
await prisma.visiMisiDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
visi: l.visi,
|
||||
misi: l.misi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
visi: l.visi,
|
||||
misi: l.misi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("visi misi desa success ...");
|
||||
|
||||
// =========== MASKOT DESA ===========
|
||||
for (const l of maskotDesa) {
|
||||
await prisma.maskotDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("maskot desa success ...");
|
||||
|
||||
console.log("🔄 Seeding Profile Desa Image...");
|
||||
for (const m of profileDesaImage) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile desa image "${m.label}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profileDesaImage.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
label: m.label,
|
||||
maskotDesaId: m.maskotDesaId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
maskotDesaId: m.maskotDesaId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profile desa image success ...");
|
||||
|
||||
// =========== LAMBANG DESA ===========
|
||||
for (const l of lambangDesa) {
|
||||
await prisma.lambangDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("lambang desa success ...");
|
||||
|
||||
// =========== PROFILE PERBEKEL PROFILE DESA ===========
|
||||
console.log("🔄 Seeding Profile Perbekel...");
|
||||
for (const m of profilePerbekel) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile perbekel "${m.biodata}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profilPerbekel.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
biodata: m.biodata,
|
||||
pengalaman: m.pengalaman,
|
||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
||||
programUnggulan: m.programUnggulan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
biodata: m.biodata,
|
||||
pengalaman: m.pengalaman,
|
||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
||||
programUnggulan: m.programUnggulan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profile perbekel desa success ...");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import perbekelDariMasaKeMasa from "../../../data/desa/profile/profile-perbekel-lalu.json";
|
||||
|
||||
export async function seedProfilePerbekel() {
|
||||
console.log("🔄 Seeding Perbekel Dari Masa Ke Masa...");
|
||||
for (const p of perbekelDariMasaKeMasa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for Perbekel Dari Masa Ke Masa "${p.nama}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.perbekelDariMasaKeMasa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
nama: p.nama,
|
||||
periode: p.periode,
|
||||
daerah: p.daerah,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
nama: p.nama,
|
||||
periode: p.periode,
|
||||
daerah: p.daerah,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pejabat Desa seeding completed");
|
||||
}
|
||||
25
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import demografiPekerjaan from "../../data/ekonomi/demografi-pekerjaan/demografi-pekerjaan.json";
|
||||
|
||||
export async function seedDemografiPekerjaan() {
|
||||
console.log("🔄 Seeding Demografi Pekerjaan...");
|
||||
for (const k of demografiPekerjaan) {
|
||||
await prisma.dataDemografiPekerjaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
pekerjaan: k.pekerjaan,
|
||||
lakiLaki: k.lakiLaki,
|
||||
perempuan: k.perempuan,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
pekerjaan: k.pekerjaan,
|
||||
lakiLaki: k.lakiLaki,
|
||||
perempuan: k.perempuan,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Demografi Pekerjaan seeded successfully");
|
||||
}
|
||||
23
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
23
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import jumlahPendudukMiskin from "../../data/ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json";
|
||||
|
||||
export async function seedJumlahPendudukMiskin() {
|
||||
console.log("🔄 Seeding Jumlah Penduduk Miskin...");
|
||||
for (const k of jumlahPendudukMiskin) {
|
||||
await prisma.grafikJumlahPendudukMiskin.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
year: k.year,
|
||||
totalPoorPopulation: k.totalPoorPopulation,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
year: k.year,
|
||||
totalPoorPopulation: k.totalPoorPopulation,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jumlah Penduduk Miskin seeded successfully");
|
||||
}
|
||||
27
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import jumlahPengangguran from "../../data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
|
||||
|
||||
export async function seedJumlahPengangguran() {
|
||||
for (const d of jumlahPengangguran) {
|
||||
await prisma.detailDataPengangguran.upsert({
|
||||
where: {
|
||||
month_year: { month: d.month, year: d.year },
|
||||
},
|
||||
update: {
|
||||
totalUnemployment: d.totalUnemployment,
|
||||
educatedUnemployment: d.educatedUnemployment,
|
||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||
percentageChange: d.percentageChange,
|
||||
},
|
||||
create: {
|
||||
month: d.month,
|
||||
year: d.year,
|
||||
totalUnemployment: d.totalUnemployment,
|
||||
educatedUnemployment: d.educatedUnemployment,
|
||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||
percentageChange: d.percentageChange,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 detailDataPengangguran success ...");
|
||||
}
|
||||
35
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
35
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import lowonganKerjaLokal from "../../data/ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json";
|
||||
|
||||
export async function seedLowonganKerjaLokal() {
|
||||
console.log("🔄 Seeding Lowongan Kerja Lokal...");
|
||||
for (const k of lowonganKerjaLokal) {
|
||||
await prisma.lowonganPekerjaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
posisi: k.posisi,
|
||||
namaPerusahaan: k.namaPerusahaan,
|
||||
lokasi: k.lokasi,
|
||||
tipePekerjaan: k.tipePekerjaan,
|
||||
gaji: k.gaji,
|
||||
deskripsi: k.deskripsi,
|
||||
kualifikasi: k.kualifikasi,
|
||||
notelp: k.notelp,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
posisi: k.posisi,
|
||||
namaPerusahaan: k.namaPerusahaan,
|
||||
lokasi: k.lokasi,
|
||||
tipePekerjaan: k.tipePekerjaan,
|
||||
gaji: k.gaji,
|
||||
deskripsi: k.deskripsi,
|
||||
kualifikasi: k.kualifikasi,
|
||||
notelp: k.notelp,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Lowongan Kerja Lokal seeded successfully");
|
||||
}
|
||||
91
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
91
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kategoriProduk from "../../data/ekonomi/pasar-desa/kategori-produk.json";
|
||||
import pasarDesa from "../../data/ekonomi/pasar-desa/pasar-desa.json";
|
||||
import kategoriToPasar from "../../data/ekonomi/pasar-desa/kategori-to-pasar.json";
|
||||
|
||||
export async function seedPasarDesa() {
|
||||
console.log("🔄 Seeding Kategori Produk...");
|
||||
for (const k of kategoriProduk) {
|
||||
await prisma.kategoriProduk.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kategori Produk seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pasar Desa...");
|
||||
|
||||
for (const p of pasarDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pasar desa "${p.nama}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pasarDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
nama: p.nama,
|
||||
deskripsi: p.deskripsi,
|
||||
harga: p.harga,
|
||||
rating: p.rating,
|
||||
alamatUsaha: p.alamatUsaha,
|
||||
kontak: p.kontak,
|
||||
imageId,
|
||||
kategoriProdukId: p.kategoriProdukId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
nama: p.nama,
|
||||
deskripsi: p.deskripsi,
|
||||
harga: p.harga,
|
||||
rating: p.rating,
|
||||
alamatUsaha: p.alamatUsaha,
|
||||
kontak: p.kontak,
|
||||
imageId,
|
||||
kategoriProdukId: p.kategoriProdukId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Pasar desa seeded: ${p.nama}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Pasar desa seed selesai");
|
||||
|
||||
console.log("🔄 Seeding Kategori To Pasar...");
|
||||
for (const p of kategoriToPasar) {
|
||||
await prisma.kategoriToPasar.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
kategoriId: p.kategoriId,
|
||||
pasarDesaId: p.pasarDesaId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
kategoriId: p.kategoriId,
|
||||
pasarDesaId: p.pasarDesaId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
81
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
81
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import apbdes from "../../data/ekonomi/pendapatan-asli-desa/apbDesa.json";
|
||||
import pendapatan from "../../data/ekonomi/pendapatan-asli-desa/pendapatanDesa.json";
|
||||
import belanja from "../../data/ekonomi/pendapatan-asli-desa/belanjaDesa.json";
|
||||
import pembiayaan from "../../data/ekonomi/pendapatan-asli-desa/pembiayaanDesa.json";
|
||||
|
||||
export async function seedPendapatanAsli() {
|
||||
console.log("🔄 Seeding Pendapatan Asli...");
|
||||
for (const d of apbdes) {
|
||||
await prisma.apbDesa.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
tahun: d.tahun,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
tahun: d.tahun,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pendapatan Asli seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pendapatan...");
|
||||
for (const d of pendapatan) {
|
||||
await prisma.pendapatan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pendapatan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Belanja...");
|
||||
for (const d of belanja) {
|
||||
await prisma.belanja.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Belanja seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pembiayaan...");
|
||||
for (const d of pembiayaan) {
|
||||
await prisma.pembiayaan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pembiayaan seeded successfully");
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import grafikMenganggurBerdasarkanUsia from "../../data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json";
|
||||
import grafikMenganggurBerdasarkanPendidikan from "../../data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json";
|
||||
|
||||
export async function seedPendudukUsiaKerjaYangMenganggur() {
|
||||
for (const p of grafikMenganggurBerdasarkanUsia) {
|
||||
await prisma.grafikMenganggurBerdasarkanUsia.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
usia18_25: p.usia18_25,
|
||||
usia26_35: p.usia26_35,
|
||||
usia36_45: p.usia36_45,
|
||||
usia46_keatas: p.usia46_keatas,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
usia18_25: p.usia18_25,
|
||||
usia26_35: p.usia26_35,
|
||||
usia36_45: p.usia36_45,
|
||||
usia46_keatas: p.usia46_keatas,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||
for (const p of grafikMenganggurBerdasarkanPendidikan) {
|
||||
await prisma.grafikMenganggurBerdasarkanPendidikan.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
SD: p.SD,
|
||||
SMP: p.SMP,
|
||||
SMA: p.SMA,
|
||||
D3: p.D3,
|
||||
S1: p.S1,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
SD: p.SD,
|
||||
SMP: p.SMP,
|
||||
SMA: p.SMA,
|
||||
D3: p.D3,
|
||||
S1: p.S1,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||
}
|
||||
50
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
50
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import programKemiskinan from "../../data/ekonomi/program-kemiskinan/program-kemiskinan.json";
|
||||
import statistikKemiskinan from "../../data/ekonomi/program-kemiskinan/statistik-kemiskinan.json";
|
||||
|
||||
export async function seedProgramKemiskinan() {
|
||||
for (const s of statistikKemiskinan) {
|
||||
await prisma.statistikKemiskinan.upsert({
|
||||
where: { tahun: s.tahun }, // ✅ FIX
|
||||
update: {
|
||||
jumlah: s.jumlah,
|
||||
},
|
||||
create: {
|
||||
id: s.id, // id boleh tetap
|
||||
tahun: s.tahun,
|
||||
jumlah: s.jumlah,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("📊 Statistik Kemiskinan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Program Kemiskinan...");
|
||||
for (const k of programKemiskinan) {
|
||||
await prisma.programKemiskinan.upsert({
|
||||
where: { id: k.id },
|
||||
update: {
|
||||
nama: k.nama,
|
||||
deskripsi: k.deskripsi,
|
||||
icon: k.icon,
|
||||
statistik: {
|
||||
connect: {
|
||||
tahun: k.tahun, // 👈 BUKAN ID
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
deskripsi: k.deskripsi,
|
||||
icon: k.icon,
|
||||
statistik: {
|
||||
connect: {
|
||||
tahun: k.tahun,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Kemiskinan seeded successfully");
|
||||
}
|
||||
25
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import sektorUnggulanDesa from "../../data/ekonomi/sektor-unggulan/sektor-unggulan.json";
|
||||
|
||||
export async function seedSektorUnggulanDesa() {
|
||||
console.log("🔄 Seeding Sektor Unggulan Desa...");
|
||||
for (const k of sektorUnggulanDesa) {
|
||||
await prisma.sektorUnggulanDesa.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
description: k.description,
|
||||
value: k.value,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
description: k.description,
|
||||
value: k.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Sektor Unggulan Desa seeded successfully");
|
||||
}
|
||||
58
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
58
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import posisiOrganisasiBumDes from "../../data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
|
||||
import pegawai from "../../data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
|
||||
|
||||
export async function seedStrukturBumdes() {
|
||||
const flattenedPosisi = posisiOrganisasiBumDes.flat();
|
||||
|
||||
// ✅ Urutkan berdasarkan hierarki
|
||||
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
||||
|
||||
for (const p of sortedPosisi) {
|
||||
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||
if (p.parentId) {
|
||||
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||
if (!parentExists) {
|
||||
console.warn(
|
||||
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await prisma.posisiOrganisasiBumDes.upsert({
|
||||
where: { id: p.id },
|
||||
update: p,
|
||||
create: p,
|
||||
});
|
||||
}
|
||||
console.log("posisi organisasi berhasil");
|
||||
for (const p of pegawai) {
|
||||
await prisma.pegawaiBumDes.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
namaLengkap: p.namaLengkap,
|
||||
gelarAkademik: p.gelarAkademik,
|
||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||
email: p.email,
|
||||
telepon: p.telepon,
|
||||
alamat: p.alamat,
|
||||
posisiId: p.posisiId,
|
||||
isActive: p.isActive,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
namaLengkap: p.namaLengkap,
|
||||
gelarAkademik: p.gelarAkademik,
|
||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||
email: p.email,
|
||||
telepon: p.telepon,
|
||||
alamat: p.alamat,
|
||||
posisiId: p.posisiId,
|
||||
isActive: p.isActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pegawai success ...");
|
||||
}
|
||||
31
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
31
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import ajukanIde from "../../data/inovasi/ajukan-ide/ajukan-ide.json";
|
||||
|
||||
export async function seedAjukan() {
|
||||
console.log("🔄 Seeding Ajukan Ide Inovatif...");
|
||||
for (const d of ajukanIde) {
|
||||
await prisma.ajukanIdeInovatif.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
namaIde: d.namaIde,
|
||||
deskripsi: d.deskripsi,
|
||||
masalah: d.masalah,
|
||||
benefit: d.benefit,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
namaIde: d.namaIde,
|
||||
deskripsi: d.deskripsi,
|
||||
masalah: d.masalah,
|
||||
benefit: d.benefit,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Ajukan Ide Inovatif seeded successfully");
|
||||
}
|
||||
42
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
42
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import desaDigital from "../../data/inovasi/desa-digital/desa-digital.json";
|
||||
|
||||
export async function seedDesaDigital() {
|
||||
console.log("🔄 Seeding Desa Digital...");
|
||||
for (const d of desaDigital) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (d.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: d.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for desa digital "${d.name}": ${d.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.desaDigital.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
deskripsi: d.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
deskripsi: d.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Desa Digital seeded successfully");
|
||||
}
|
||||
42
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
42
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import infoTeknologi from "../../data/inovasi/info-teknologi/info-teknologi.json";
|
||||
|
||||
export async function seedInfoTeknologi() {
|
||||
console.log("🔄 Seeding Info Teknologi...");
|
||||
for (const p of infoTeknologi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.infoTekno.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Info Teknologi seeded successfully");
|
||||
}
|
||||
66
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
66
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kolaborasiInovasi from "../../data/inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json";
|
||||
import mitraKolaborasi from "../../data/inovasi/kolaborasi-inovasi/mitra-kolaborasi.json";
|
||||
|
||||
export async function seedKolaborasiInovasi() {
|
||||
console.log("🔄 Seeding Kolaborasi Inovasi...");
|
||||
for (const p of kolaborasiInovasi) {
|
||||
await prisma.kolaborasiInovasi.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
tahun: p.tahun,
|
||||
slug: p.slug,
|
||||
deskripsi: p.deskripsi,
|
||||
kolaborator: p.kolaborator,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
tahun: p.tahun,
|
||||
slug: p.slug,
|
||||
deskripsi: p.deskripsi,
|
||||
kolaborator: p.kolaborator,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kolaborasi Inovasi seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Mitra Kolaborasi...");
|
||||
for (const p of mitraKolaborasi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for mitra kolaborasi "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.mitraKolaborasi.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Mitra Kolaborasi seeded successfully");
|
||||
}
|
||||
113
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
113
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import jenisLayanan from "../../data/inovasi/layanan-online-desa/jenis-layanan.json";
|
||||
import administrasiOnline from "../../data/inovasi/layanan-online-desa/administrasi-online.json";
|
||||
import jenisPengaduan from "../../data/inovasi/layanan-online-desa/jenis-pengaduan.json";
|
||||
import pengaduanMasyarakat from "../../data/inovasi/layanan-online-desa/pengaduan-masyarakat.json";
|
||||
|
||||
export async function seedLayananOnlineDesa() {
|
||||
console.log("🔄 Seeding Jenis Layanan...");
|
||||
for (const j of jenisLayanan) {
|
||||
await prisma.jenisLayanan.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
deskripsi: j.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
deskripsi: j.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenis Layanan seeded successfully");
|
||||
console.log("🔄 Seeding Administrasi Online...");
|
||||
for (const d of administrasiOnline) {
|
||||
await prisma.administrasiOnline.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
jenisLayananId: d.jenisLayananId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
jenisLayananId: d.jenisLayananId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Administrasi Online seeded successfully");
|
||||
console.log("🔄 Seeding Jenis Pengaduan Masyarakat...");
|
||||
for (const d of jenisPengaduan) {
|
||||
await prisma.jenisPengaduan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
nama: d.nama,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
nama: d.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenis Pengaduan Masyarakat seeded successfully");
|
||||
console.log("🔄 Seeding Pengaduan Masyarakat...");
|
||||
for (const d of pengaduanMasyarakat) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (d.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: d.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pengaduan masyarakat "${d.name}": ${d.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pengaduanMasyarakat.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
nik: d.nik,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
judulPengaduan: d.judulPengaduan,
|
||||
lokasiKejadian: d.lokasiKejadian,
|
||||
imageId: imageId,
|
||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||
jenisPengaduanId: d.jenisPengaduanId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
nik: d.nik,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
judulPengaduan: d.judulPengaduan,
|
||||
lokasiKejadian: d.lokasiKejadian,
|
||||
imageId: imageId,
|
||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||
jenisPengaduanId: d.jenisPengaduanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pengaduan Masyarakat seeded successfully");
|
||||
}
|
||||
27
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
27
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import programKreatif from "../../data/inovasi/program-kreatif-desa/program-kreatif-desa.json";
|
||||
|
||||
export async function seedProgramKreatifDesa() {
|
||||
console.log("🔄 Seeding Program Kreatif...");
|
||||
for (const p of programKreatif) {
|
||||
await prisma.programKreatif.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
slug: p.slug,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
slug: p.slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Kreatif seeded successfully");
|
||||
}
|
||||
44
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
44
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import keamananLingkunganJson from "../../data/keamanan/keamanan-lingkungan/keamanan-lingkungan.json";
|
||||
|
||||
export async function seedKeamananLingkungan() {
|
||||
console.log("🔄 Seeding Keamanan Lingkungan...");
|
||||
|
||||
for (const p of keamananLingkunganJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for keamanan lingkungan "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.keamananLingkungan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Keamanan lingkungan seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Keamanan lingkungan seed selesai");
|
||||
}
|
||||
87
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
87
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kontakDaruratKeamanan from "../../data/keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json";
|
||||
import kontakItem from "../../data/keamanan/kontak-darurat-keamanan/kontakItem.json";
|
||||
import kontakDaruratToItem from "../../data/keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json";
|
||||
|
||||
export async function seedKontakDaruratKeamanan() {
|
||||
console.log("🔄 Seeding Kontak Item...");
|
||||
for (const e of kontakItem) {
|
||||
await prisma.kontakItem.upsert({
|
||||
where: {
|
||||
id: e.id,
|
||||
},
|
||||
update: {
|
||||
nama: e.nama,
|
||||
icon: e.icon,
|
||||
nomorTelepon: e.nomorTelepon,
|
||||
},
|
||||
create: {
|
||||
id: e.id, // ✅ WAJIB
|
||||
nama: e.nama,
|
||||
icon: e.icon,
|
||||
nomorTelepon: e.nomorTelepon,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kontak Item seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Kontak Darurat Keamanan...");
|
||||
for (const d of kontakDaruratKeamanan) {
|
||||
await prisma.kontakDaruratKeamanan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
nama: d.nama,
|
||||
icon: d.icon,
|
||||
kategoriId: d.kategoriId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
nama: d.nama,
|
||||
icon: d.icon,
|
||||
kategoriId: d.kategoriId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Kontak Darurat Keamanan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Kontak Darurat To Item...");
|
||||
for (const f of kontakDaruratToItem) {
|
||||
// ✅ Validasi foreign keys
|
||||
const kontakDaruratExists = await prisma.kontakDaruratKeamanan.findUnique({
|
||||
where: { id: f.kontakDaruratId },
|
||||
});
|
||||
|
||||
const kontakItemExists = await prisma.kontakItem.findUnique({
|
||||
where: { id: f.kontakItemId },
|
||||
});
|
||||
|
||||
if (!kontakDaruratExists) {
|
||||
console.warn(
|
||||
`⚠️ KontakDarurat ${f.kontakDaruratId} not found, skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!kontakItemExists) {
|
||||
console.warn(`⚠️ KontakItem ${f.kontakItemId} not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.kontakDaruratToItem.upsert({
|
||||
where: { id: f.id },
|
||||
update: {
|
||||
kontakDaruratId: f.kontakDaruratId,
|
||||
kontakItemId: f.kontakItemId,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
kontakDaruratId: f.kontakDaruratId,
|
||||
kontakItemId: f.kontakItemId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kontak Darurat To Item seeded successfully");
|
||||
}
|
||||
49
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
49
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import laporanPublik from "../../data/keamanan/laporan-publik/laporan-publik.json";
|
||||
import penangananLaporan from "../../data/keamanan/laporan-publik/penanganan-laporan.json";
|
||||
|
||||
export async function seedLaporanPublik() {
|
||||
console.log("🔄 Seeding Laporan Publik...");
|
||||
for (const l of laporanPublik) {
|
||||
await prisma.laporanPublik.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
lokasi: l.lokasi,
|
||||
tanggalWaktu: l.tanggalWaktu,
|
||||
kronologi: l.kronologi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
lokasi: l.lokasi,
|
||||
tanggalWaktu: l.tanggalWaktu,
|
||||
kronologi: l.kronologi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("laporan publik success ...");
|
||||
|
||||
console.log("🔄 Seeding Penanganan Laporan...");
|
||||
for (const l of penangananLaporan) {
|
||||
await prisma.penangananLaporanPublik.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
deskripsi: l.deskripsi,
|
||||
laporanId: l.laporanId,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
deskripsi: l.deskripsi,
|
||||
laporanId: l.laporanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("penanganan laporan success ...");
|
||||
}
|
||||
28
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
28
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import pencegahanKriminalitas from "../../data/keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json";
|
||||
|
||||
export async function seedPencegahanKriminalitas() {
|
||||
console.log("🔄 Seeding Pencegahan Kriminalitas...");
|
||||
for (const d of pencegahanKriminalitas) {
|
||||
await prisma.pencegahanKriminalitas.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
judul: d.judul,
|
||||
deskripsi: d.deskripsi,
|
||||
deskripsiSingkat: d.deskripsiSingkat,
|
||||
linkVideo: d.linkVideo,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
judul: d.judul,
|
||||
deskripsi: d.deskripsi,
|
||||
deskripsiSingkat: d.deskripsiSingkat,
|
||||
linkVideo: d.linkVideo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Pencegahan Kriminalitas seeded successfully");
|
||||
}
|
||||
80
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
80
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import layananPolsek from "../../data/keamanan/polsek-terdekat/layanan-polsek.json";
|
||||
import polsekTerdekat from "../../data/keamanan/polsek-terdekat/polsek-terdekat.json";
|
||||
import layananToPolsek from "../../data/keamanan/polsek-terdekat/layanan-to-polsek.json";
|
||||
|
||||
export async function seedPolsekTerdekat() {
|
||||
console.log("🔄 Seeding Layanan Polsek...");
|
||||
for (const k of layananPolsek) {
|
||||
await prisma.layananPolsek.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("layanan polsek success ...");
|
||||
|
||||
console.log("🔄 Seeding Polsek Terdekat...");
|
||||
for (const k of polsekTerdekat) {
|
||||
await prisma.polsekTerdekat.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
jarakKeDesa: k.jarakKeDesa,
|
||||
alamat: k.alamat,
|
||||
nomorTelepon: k.nomorTelepon,
|
||||
jamOperasional: k.jamOperasional,
|
||||
embedMapUrl: k.embedMapUrl,
|
||||
namaTempatMaps: k.namaTempatMaps,
|
||||
alamatMaps: k.alamatMaps,
|
||||
linkPetunjukArah: k.linkPetunjukArah,
|
||||
layananPolsekId: k.layananPolsekId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
jarakKeDesa: k.jarakKeDesa,
|
||||
alamat: k.alamat,
|
||||
nomorTelepon: k.nomorTelepon,
|
||||
jamOperasional: k.jamOperasional,
|
||||
embedMapUrl: k.embedMapUrl,
|
||||
namaTempatMaps: k.namaTempatMaps,
|
||||
alamatMaps: k.alamatMaps,
|
||||
linkPetunjukArah: k.linkPetunjukArah,
|
||||
layananPolsekId: k.layananPolsekId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("polsek terdekat success ...");
|
||||
|
||||
console.log("🔄 Seeding Layanan To Polsek...");
|
||||
for (const k of layananToPolsek) {
|
||||
await prisma.layananToPolsek.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
layananId: k.layananId,
|
||||
polsekTerdekatId: k.polsekTerdekatId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
layananId: k.layananId,
|
||||
polsekTerdekatId: k.polsekTerdekatId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("layanan to polsek success ...");
|
||||
}
|
||||
44
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
44
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import tipsKeamananJson from "../../data/keamanan/tips-keamanan/tips-keamanan.json";
|
||||
|
||||
export async function seedTipsKeamanan() {
|
||||
console.log("🔄 Seeding Tips Keamanan...");
|
||||
|
||||
for (const p of tipsKeamananJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for tips keamanan "${p.judul}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.menuTipsKeamanan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Tips Keamanan seeded: ${p.judul}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Tips Keamanan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import infoWabahPenyakitJson from "../../../data/kesehatan/infowabahpenyakit/infowabahpenyakit.json";
|
||||
|
||||
export async function seedInfoWabahPenyakit() {
|
||||
console.log("🔄 Seeding Info Wabah Penyakit...");
|
||||
|
||||
for (const p of infoWabahPenyakitJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for info wabah penyakit "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.infoWabahPenyakit.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsiLengkap: p.deskripsiLengkap,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsiLengkap: p.deskripsiLengkap,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Info wabah penyakit seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Info wabah penyakit seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import kontakDaruratJson from "../../../data/kesehatan/kontak-darurat/kontak-darurat.json";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function seedKontakDarurat() {
|
||||
console.log("🔄 Seeding Kontak Darurat...");
|
||||
|
||||
for (const p of kontakDaruratJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for kontak darurat "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.kontakDarurat.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
whatsapp: p.whatsapp,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
whatsapp: p.whatsapp,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Kontak darurat seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Kontak darurat seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import penangananDaruratJson from "../../../data/kesehatan/penanganan-darurat/penganan-darurat.json";
|
||||
|
||||
export async function seedPenangananDarurat() {
|
||||
console.log("🔄 Seeding Penanganan Darurat...");
|
||||
|
||||
for (const p of penangananDaruratJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for penanganan darurat "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.penangananDarurat.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Penanganan darurat seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Penanganan darurat seed selesai");
|
||||
}
|
||||
48
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
48
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import posyanduJson from "../../../data/kesehatan/posyandu/posyandu.json";
|
||||
|
||||
export async function seedPosyandu() {
|
||||
console.log("🔄 Seeding Posyandu...");
|
||||
|
||||
for (const p of posyanduJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for posyandu "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.posyandu.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
nomor: p.nomor,
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
nomor: p.nomor,
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Posyandu seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Posyandu seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import programKesehatanJson from "../../../data/kesehatan/program-kesehatan/program-kesehatan.json";
|
||||
|
||||
export async function seedProgramKesehatan() {
|
||||
for (const p of programKesehatanJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for program kesehatan "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.programKesehatan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Program kesehatan seeded: ${p.name}`);
|
||||
}
|
||||
}
|
||||
95
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
95
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import puskesmasJson from "../../../data/kesehatan/puskesmas/puskesmas.json";
|
||||
import kontakPuskesmasJson from "../../../data/kesehatan/puskesmas/kontak-puskesmas/kontak.json";
|
||||
import jamPuskesmasJson from "../../../data/kesehatan/puskesmas/jam-puskesmas/jam.json";
|
||||
|
||||
export async function seedPuskesmas() {
|
||||
console.log("🔄 Seeding Kontak Puskesmas...");
|
||||
for (const k of kontakPuskesmasJson) {
|
||||
await prisma.kontakPuskesmas.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
kontakPuskesmas: k.kontakPuskesmas,
|
||||
email: k.email,
|
||||
facebook: k.facebook,
|
||||
kontakUGD: k.kontakUGD,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
kontakPuskesmas: k.kontakPuskesmas,
|
||||
email: k.email,
|
||||
facebook: k.facebook,
|
||||
kontakUGD: k.kontakUGD,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kontak puskesmas success ...");
|
||||
|
||||
console.log("🔄 Seeding Jam Puskesmas...");
|
||||
for (const k of jamPuskesmasJson) {
|
||||
await prisma.jamOperasional.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
workDays: k.workDays,
|
||||
weekDays: k.weekDays,
|
||||
holiday: k.holiday,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
workDays: k.workDays,
|
||||
weekDays: k.weekDays,
|
||||
holiday: k.holiday,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("jam puskesmas success ...");
|
||||
|
||||
console.log("🔄 Seeding Puskesmas...");
|
||||
|
||||
for (const p of puskesmasJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for puskesmas "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.puskesmas.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
jamId: p.jamId,
|
||||
kontakId: p.kontakId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
jamId: p.jamId,
|
||||
kontakId: p.kontakId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Puskesmas seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Puskesmas seed selesai");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kategoriDesaAntiKorupsi from "../../../data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json"
|
||||
import desaAntiKorupsi from "../../../data/landing-page/desa-anti-korupsi/desaantiKorpusi.json"
|
||||
|
||||
export async function seedDesaAntiKorupsi() {
|
||||
for (const k of kategoriDesaAntiKorupsi) {
|
||||
await prisma.kategoriDesaAntiKorupsi.upsert({
|
||||
where: { id: k.id },
|
||||
update: {
|
||||
name: k.name,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kategori desa anti korupsi success ...");
|
||||
|
||||
// =========== DESA ANTI KORUPSI ===========
|
||||
for (const p of desaAntiKorupsi) {
|
||||
await prisma.desaAntiKorupsi.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
kategoriId: p.kategoriId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
kategoriId: p.kategoriId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("desa anti korupsi success ...");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import prestasiDesa from "../../../data/landing-page/prestasi-desa/prestasi-desa.json"
|
||||
import kategoriPrestasiDesa from "../../../data/landing-page/prestasi-desa/kategori-prestasi.json"
|
||||
|
||||
export async function seedPrestasiDesa() {
|
||||
|
||||
console.log("🔄 Seeding Kategori Prestasi Desa...");
|
||||
for (const c of kategoriPrestasiDesa) {
|
||||
await prisma.kategoriPrestasiDesa.upsert({
|
||||
where: { id: c.id },
|
||||
update: {
|
||||
name: c.name,
|
||||
},
|
||||
create: {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kategori prestasi desa success ...");
|
||||
|
||||
console.log("🔄 Seeding Prestasi Desa...");
|
||||
for (const m of prestasiDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for prestasi desa "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.prestasiDesa.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("prestasi desa success ...");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import mediaSosial from "../../../data/landing-page/profile/mediaSosial.json"
|
||||
|
||||
export async function seedMediaSosial() {
|
||||
console.log("🔄 Seeding Media Sosial...");
|
||||
for (const m of mediaSosial) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.mediaSosial.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
iconUrl: m.iconUrl,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
iconUrl: m.iconUrl,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("media sosial success ...");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import profilePejabatDesa from "../../../data/landing-page/profile/profile.json";
|
||||
|
||||
export async function seedProfileLP() {
|
||||
console.log("🔄 Seeding Pejabat Desa...");
|
||||
for (const p of profilePejabatDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pejabatDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
position: p.position,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
position: p.position,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pejabat Desa seeding completed");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import programInovasi from "../../../data/landing-page/profile/programInovasi.json";
|
||||
|
||||
export async function seedProgramInovasi() {
|
||||
console.log("🔄 Seeding Program Inovasi...");
|
||||
|
||||
for (const b of programInovasi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (b.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: b.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for program inovasi "${b.name}": ${b.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.programInovasi.upsert({
|
||||
where: { id: b.id },
|
||||
update: {
|
||||
name: b.name,
|
||||
description: b.description,
|
||||
link: b.link,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
description: b.description,
|
||||
link: b.link,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Program Inovasi seeded: ${b.name}`);
|
||||
}
|
||||
}
|
||||
41
prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts
Normal file
41
prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import sdgsDesa from "../../../data/landing-page/sdgs-desa/sdgs-desa.json";
|
||||
|
||||
export async function seedSDGSDesa() {
|
||||
console.log("🔄 Seeding SDGS Desa...");
|
||||
for (const m of sdgsDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for sdgs desa "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.sdgsDesa.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
jumlah: m.jumlah,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
jumlah: m.jumlah,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("sdgs desa success ...");
|
||||
}
|
||||
71
prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts
Normal file
71
prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import kategoriGotongRoyong from "../../data/lingkungan/gotong-royong/kategori-gotong-royong.json";
|
||||
import gotongRoyong from "../../data/lingkungan/gotong-royong/gotong-royong.json";
|
||||
|
||||
export async function seedDataGotongRoyong() {
|
||||
console.log("🔄 Seeding Kategori Gotong Royong...");
|
||||
|
||||
for (const k of kategoriGotongRoyong) {
|
||||
await prisma.kategoriKegiatan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kategori Gotong Royong seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Gotong Royong...");
|
||||
for (const k of gotongRoyong) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (k.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: k.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for gotong royong "${k.judul}": ${k.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.kegiatanDesa.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
judul: k.judul,
|
||||
deskripsiSingkat: k.deskripsiSingkat,
|
||||
deskripsiLengkap: k.deskripsiLengkap,
|
||||
tanggal: k.tanggal,
|
||||
lokasi: k.lokasi,
|
||||
partisipan: k.partisipan,
|
||||
imageId: imageId,
|
||||
kategoriKegiatanId: k.kategoriKegiatanId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
judul: k.judul,
|
||||
deskripsiSingkat: k.deskripsiSingkat,
|
||||
deskripsiLengkap: k.deskripsiLengkap,
|
||||
tanggal: k.tanggal,
|
||||
lokasi: k.lokasi,
|
||||
partisipan: k.partisipan,
|
||||
imageId: imageId,
|
||||
kategoriKegiatanId: k.kategoriKegiatanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Gotong Royong seeded successfully");
|
||||
}
|
||||
27
prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts
Normal file
27
prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import dataLingkunganDesa from "../../data/lingkungan/data-lingkungan-desa/data-lingkungan-desa.json";
|
||||
|
||||
export async function seedDataLingkunganDesa() {
|
||||
console.log("🔄 Seeding Data Lingkungan Desa...");
|
||||
for (const p of dataLingkunganDesa) {
|
||||
await prisma.dataLingkunganDesa.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
jumlah: p.jumlah,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
jumlah: p.jumlah,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Data Lingkungan Desa seeded successfully");
|
||||
}
|
||||
63
prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts
Normal file
63
prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import tujuanEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||
import materiEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||
import contohEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
|
||||
|
||||
export async function seedEdukasiLingkungan() {
|
||||
for (const e of tujuanEdukasiLingkungan) {
|
||||
await prisma.tujuanEdukasiLingkungan.upsert({
|
||||
where: {
|
||||
id: e.id,
|
||||
},
|
||||
update: {
|
||||
judul: e.judul,
|
||||
deskripsi: e.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: e.id,
|
||||
judul: e.judul,
|
||||
deskripsi: e.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("tujuan edukasi lingkungan success ...");
|
||||
|
||||
for (const m of materiEdukasiLingkungan) {
|
||||
await prisma.materiEdukasiLingkungan.upsert({
|
||||
where: {
|
||||
id: m.id,
|
||||
},
|
||||
update: {
|
||||
judul: m.judul,
|
||||
deskripsi: m.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
judul: m.judul,
|
||||
deskripsi: m.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("materi edukasi lingkungan success ...");
|
||||
|
||||
for (const c of contohEdukasiLingkungan) {
|
||||
await prisma.contohEdukasiLingkungan.upsert({
|
||||
where: {
|
||||
id: c.id,
|
||||
},
|
||||
update: {
|
||||
judul: c.judul,
|
||||
deskripsi: c.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: c.id,
|
||||
judul: c.judul,
|
||||
deskripsi: c.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("contoh edukasi lingkungan success ...");
|
||||
}
|
||||
63
prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts
Normal file
63
prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import filosofiTriHita from "../../data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||
import bentukKonservasiBerdasarkanAdat from "../../data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
||||
import nilaiKonservasiAdat from "../../data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||
|
||||
export async function seedKonservasiAdatBali() {
|
||||
for (const f of filosofiTriHita) {
|
||||
await prisma.filosofiTriHita.upsert({
|
||||
where: {
|
||||
id: f.id,
|
||||
},
|
||||
update: {
|
||||
judul: f.judul,
|
||||
deskripsi: f.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
judul: f.judul,
|
||||
deskripsi: f.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("filosofi tri hita success ...");
|
||||
|
||||
for (const b of bentukKonservasiBerdasarkanAdat) {
|
||||
await prisma.bentukKonservasiBerdasarkanAdat.upsert({
|
||||
where: {
|
||||
id: b.id,
|
||||
},
|
||||
update: {
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: b.id,
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("bentuk konservasi berdasarkan adat success ...");
|
||||
|
||||
for (const n of nilaiKonservasiAdat) {
|
||||
await prisma.nilaiKonservasiAdat.upsert({
|
||||
where: {
|
||||
id: n.id,
|
||||
},
|
||||
update: {
|
||||
judul: n.judul,
|
||||
deskripsi: n.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: n.id,
|
||||
judul: n.judul,
|
||||
deskripsi: n.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("nilai konservasi adat success ...");
|
||||
}
|
||||
51
prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts
Normal file
51
prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import pengelolaanSampah from "../../data/lingkungan/pengelolaan-sampah/pengelolaan-sampah.json";
|
||||
import keteranganBankSampah from "../../data/lingkungan/pengelolaan-sampah/keterangan-bank-sampah.json";
|
||||
|
||||
export async function seedPengelolaanSampah() {
|
||||
console.log("🔄 Seeding Pengelolaan Sampah...");
|
||||
for (const p of pengelolaanSampah) {
|
||||
await prisma.pengelolaanSampah.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pengelolaan Sampah seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Keterangan Bank Sampah...");
|
||||
for (const p of keteranganBankSampah) {
|
||||
await prisma.keteranganBankSampahTerdekat.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
namaTempatMaps: p.namaTempatMaps,
|
||||
linkPetunjukArah: p.linkPetunjukArah,
|
||||
lat: p.lat,
|
||||
lng: p.lng,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
namaTempatMaps: p.namaTempatMaps,
|
||||
linkPetunjukArah: p.linkPetunjukArah,
|
||||
lat: p.lat,
|
||||
lng: p.lng,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Keterangan Bank Sampah seeded successfully");
|
||||
}
|
||||
27
prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts
Normal file
27
prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import programPenghijauan from "../../data/lingkungan/program-penghijauan/program-penghijauan.json";
|
||||
|
||||
export async function seedProgramPenghijauan() {
|
||||
console.log("🔄 Seeding Program Penghijauan...");
|
||||
for (const p of programPenghijauan) {
|
||||
await prisma.programPenghijauan.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Penghijauan seeded successfully");
|
||||
}
|
||||
60
prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts
Normal file
60
prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import tujuanBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
|
||||
import lokasiJadwalBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
|
||||
import fasilitasBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
|
||||
|
||||
export async function seedBimbinganBelajar() {
|
||||
for (const t of tujuanBimbinganBelajarDesa) {
|
||||
await prisma.tujuanBimbinganBelajarDesa.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
"✅ tujuan bimbingan belajar desa seeded (editable later via UI)",
|
||||
);
|
||||
|
||||
for (const t of lokasiJadwalBimbinganBelajarDesa) {
|
||||
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
"✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)",
|
||||
);
|
||||
|
||||
for (const t of fasilitasBimbinganBelajarDesa) {
|
||||
await prisma.fasilitasBimbinganBelajarDesa.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||
);
|
||||
}
|
||||
23
prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts
Normal file
23
prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import dataPendidikan from "../../data/pendidikan/data-pendidikan/data-pendidikan.json";
|
||||
|
||||
export async function seedDataPendidikan() {
|
||||
console.log("🔄 Seeding Data pendidikan...");
|
||||
for (const k of dataPendidikan) {
|
||||
await prisma.dataPendidikan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
jumlah: k.jumlah,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
jumlah: k.jumlah,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Data pendidikan seeded successfully");
|
||||
}
|
||||
71
prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts
Normal file
71
prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import dataPerpustakaan from "../../data/pendidikan/perpustakaan-digital/perpustakaan-digital.json";
|
||||
import kategoriBuku from "../../data/pendidikan/perpustakaan-digital/kategori-buku.json";
|
||||
|
||||
export async function seedDataPerpustakaan() {
|
||||
console.log("🔄 Seeding Kategori Buku...");
|
||||
for (const k of kategoriBuku) {
|
||||
await prisma.kategoriBuku.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kategori Buku seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Data perpustakaan...");
|
||||
for (const k of dataPerpustakaan) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (k.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: k.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for perpustakaan "${k.judul}": ${k.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.dataPerpustakaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
judul: k.judul,
|
||||
deskripsi: k.deskripsi,
|
||||
kategoriId: k.kategoriId,
|
||||
imageId: imageId
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
judul: k.judul,
|
||||
deskripsi: k.deskripsi,
|
||||
kategoriId: k.kategoriId,
|
||||
imageId: imageId
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Data perpustakaan seeded successfully");
|
||||
}
|
||||
if (import.meta.main) {
|
||||
seedDataPerpustakaan()
|
||||
.then(() => {
|
||||
console.log("seed data perpustakaan success");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("gagal seed data perpustakaan", JSON.stringify(err));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import tujuanProgram from "../../data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||
import programUnggulan from "../../data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||
|
||||
export async function seedInfoProgramPendidikan() {
|
||||
for (const t of tujuanProgram) {
|
||||
await prisma.tujuanProgram.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ tujuan program seeded (editable later via UI)");
|
||||
for (const t of programUnggulan) {
|
||||
await prisma.programUnggulan.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ program unggulan seeded (editable later via UI)");
|
||||
}
|
||||
74
prisma/_seeder_list/pendidikan/seed_info_sekolah.ts
Normal file
74
prisma/_seeder_list/pendidikan/seed_info_sekolah.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import jenjangPendidikan from "../../data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
||||
import lembagaPendidikan from "../../data/pendidikan/info-sekolah/lembaga.json";
|
||||
import siswa from "../../data/pendidikan/info-sekolah/siswa.json";
|
||||
import pengajar from "../../data/pendidikan/info-sekolah/pengajar.json";
|
||||
|
||||
export async function seedInfoSekolah() {
|
||||
for (const j of jenjangPendidikan) {
|
||||
await prisma.jenjangPendidikan.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenjang Pendidikan seeded successfully");
|
||||
for (const j of lembagaPendidikan) {
|
||||
await prisma.lembaga.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
jenjangId: j.jenjangId,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
jenjangId: j.jenjangId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Lembaga Pendidikan seeded successfully");
|
||||
for (const j of siswa) {
|
||||
await prisma.siswa.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
lembagaId: j.lembagaId,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
lembagaId: j.lembagaId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ siswa seeded successfully");
|
||||
for (const j of pengajar) {
|
||||
await prisma.pengajar.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
lembagaId: j.lembagaId,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
lembagaId: j.lembagaId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ pengajar seeded successfully");
|
||||
}
|
||||
60
prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts
Normal file
60
prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import tujuanProgram from "../../data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
||||
import tempatKegiatan from "../../data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
|
||||
import jenisProgramYangDiselenggarakan from "../../data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
|
||||
|
||||
export async function seedPendidikanNonFormal() {
|
||||
for (const t of tujuanProgram) {
|
||||
await prisma.tujuanPendidikanNonFormal.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||
);
|
||||
|
||||
for (const t of tempatKegiatan) {
|
||||
await prisma.tempatKegiatan.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||
);
|
||||
|
||||
for (const t of jenisProgramYangDiselenggarakan) {
|
||||
await prisma.jenisProgramYangDiselenggarakan.upsert({
|
||||
where: { id: t.id },
|
||||
update: {
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: t.id,
|
||||
judul: t.judul,
|
||||
deskripsi: t.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import daftarInformasiPublik from "../../../data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json"
|
||||
import jenisInformasiDiminta from "../../../data/list-jenisInfromasi.json"
|
||||
import caraMemperolehInformasi from "../../../data/list-caraMemperolehInformasi.json"
|
||||
import caraMemperolehSalinanInformasi from "../../../data/list-caraMemperolehSalinanInformasi.json"
|
||||
|
||||
export async function seedDaftarInformasiPublikPpid() {
|
||||
|
||||
for (const v of daftarInformasiPublik) {
|
||||
// Convert string date to Date object
|
||||
const tanggal = new Date(v.tanggal);
|
||||
|
||||
await prisma.daftarInformasiPublik.upsert({
|
||||
where: {
|
||||
id: v.id,
|
||||
},
|
||||
update: {
|
||||
jenisInformasi: v.jenisInformasi,
|
||||
deskripsi: v.deskripsi,
|
||||
tanggal: tanggal,
|
||||
},
|
||||
create: {
|
||||
id: v.id,
|
||||
jenisInformasi: v.jenisInformasi,
|
||||
deskripsi: v.deskripsi,
|
||||
tanggal: tanggal,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("daftar informasi publik PPID success ...");
|
||||
|
||||
for (const j of jenisInformasiDiminta) {
|
||||
await prisma.jenisInformasiDiminta.upsert({
|
||||
where: {
|
||||
name: j.name,
|
||||
},
|
||||
update: {
|
||||
name: j.name,
|
||||
},
|
||||
create: {
|
||||
name: j.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("jenis informasi diminta success ...");
|
||||
|
||||
for (const c of caraMemperolehInformasi) {
|
||||
await prisma.caraMemperolehInformasi.upsert({
|
||||
where: {
|
||||
name: c.name,
|
||||
},
|
||||
update: {
|
||||
name: c.name,
|
||||
},
|
||||
create: {
|
||||
name: c.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("cara memperoleh informasi success ...");
|
||||
|
||||
for (const c of caraMemperolehSalinanInformasi) {
|
||||
await prisma.caraMemperolehSalinanInformasi.upsert({
|
||||
where: {
|
||||
name: c.name,
|
||||
},
|
||||
update: {
|
||||
name: c.name,
|
||||
},
|
||||
create: {
|
||||
name: c.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("cara memperoleh salinan informasi success ...");
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import dasarHukumPPID from "../../../data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"
|
||||
|
||||
export async function seedDasarHukumPpid() {
|
||||
for (const v of dasarHukumPPID) {
|
||||
await prisma.dasarHukumPPID.upsert({
|
||||
where: {
|
||||
id: v.id,
|
||||
},
|
||||
update: {
|
||||
judul: v.judul,
|
||||
content: v.content,
|
||||
},
|
||||
create: {
|
||||
id: v.id,
|
||||
judul: v.judul,
|
||||
content: v.content,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("dasar hukum PPID success ...");
|
||||
}
|
||||
54
prisma/_seeder_list/ppid/ikm/seed_ikm.ts
Normal file
54
prisma/_seeder_list/ppid/ikm/seed_ikm.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import jenisKelamin from "../../../data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
|
||||
import pilihanRatingResponden from "../../../data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
|
||||
import umurResponden from "../../../data/ppid/ikm/umur-responden/umur-responden.json";
|
||||
|
||||
export async function seedIkmPpid() {
|
||||
for (const j of jenisKelamin) {
|
||||
await prisma.jenisKelaminResponden.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
name: j.name,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
name: j.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("jenis kelamin responden success ...");
|
||||
|
||||
for (const r of pilihanRatingResponden) {
|
||||
await prisma.pilihanRatingResponden.upsert({
|
||||
where: {
|
||||
id: r.id,
|
||||
},
|
||||
update: {
|
||||
name: r.name,
|
||||
},
|
||||
create: {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pilihan rating responden success ...");
|
||||
|
||||
for (const u of umurResponden) {
|
||||
await prisma.umurResponden.upsert({
|
||||
where: {
|
||||
id: u.id,
|
||||
},
|
||||
update: {
|
||||
name: u.name,
|
||||
},
|
||||
create: {
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("umur responden success ...");
|
||||
}
|
||||
48
prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts
Normal file
48
prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import profilPpd from "../../../data/ppid/profile-ppid/profilePPid.json"
|
||||
|
||||
export async function seedProfilPpd() {
|
||||
console.log("🔄 Seeding Profil PPD...");
|
||||
for (const m of profilPpd) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profilePPID.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
biodata: m.biodata,
|
||||
riwayat: m.riwayat,
|
||||
pengalaman: m.pengalaman,
|
||||
unggulan: m.unggulan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
biodata: m.biodata,
|
||||
riwayat: m.riwayat,
|
||||
pengalaman: m.pengalaman,
|
||||
unggulan: m.unggulan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profil ppd success ...");
|
||||
}
|
||||
|
||||
82
prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts
Normal file
82
prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import pegawaiPpid from "../../../data/ppid/struktur-ppid/pegawai-PPID.json"
|
||||
import posisiOrganisasiPPID from "../../../data/ppid/struktur-ppid/posisi-organisasi-PPID.json"
|
||||
|
||||
export async function seedPegawaiPpid() {
|
||||
|
||||
const flattenedPosisi = posisiOrganisasiPPID.flat();
|
||||
|
||||
// ✅ Urutkan berdasarkan hierarki
|
||||
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
||||
|
||||
for (const p of sortedPosisi) {
|
||||
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||
|
||||
if (p.parentId) {
|
||||
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||
if (!parentExists) {
|
||||
console.warn(
|
||||
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.posisiOrganisasiPPID.upsert({
|
||||
where: { id: p.id },
|
||||
update: p,
|
||||
create: p,
|
||||
});
|
||||
}
|
||||
console.log("posisi organisasi berhasil");
|
||||
|
||||
console.log("🔄 Seeding Struktur Ppid...");
|
||||
for (const m of pegawaiPpid) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pegawai ppid "${m.namaLengkap}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pegawaiPPID.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
namaLengkap: m.namaLengkap,
|
||||
gelarAkademik: m.gelarAkademik,
|
||||
tanggalMasuk: m.tanggalMasuk,
|
||||
email: m.email,
|
||||
telepon: m.telepon,
|
||||
alamat: m.alamat,
|
||||
imageId,
|
||||
posisiId: m.posisiId,
|
||||
isActive: m.isActive,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
namaLengkap: m.namaLengkap,
|
||||
gelarAkademik: m.gelarAkademik,
|
||||
tanggalMasuk: m.tanggalMasuk,
|
||||
email: m.email,
|
||||
telepon: m.telepon,
|
||||
alamat: m.alamat,
|
||||
imageId,
|
||||
posisiId: m.posisiId,
|
||||
isActive: m.isActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("struktur ppid success ...");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import visiMisiPPID from "../../../data/ppid/visi-misi-ppid/visimisiPPID.json"
|
||||
|
||||
export async function seedVisiMisiPpid() {
|
||||
for (const v of visiMisiPPID) {
|
||||
await prisma.visiMisiPPID.upsert({
|
||||
where: {
|
||||
id: v.id,
|
||||
},
|
||||
update: {
|
||||
misi: v.misi,
|
||||
visi: v.visi,
|
||||
},
|
||||
create: {
|
||||
id: v.id,
|
||||
misi: v.misi,
|
||||
visi: v.visi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("visi misi PPID success ...");
|
||||
|
||||
}
|
||||
146
prisma/data/desa/berita/berita.json
Normal file
146
prisma/data/desa/berita/berita.json
Normal file
@@ -0,0 +1,146 @@
|
||||
[
|
||||
{
|
||||
"id": "cmk6ae8rz00003b6r06x7hsqi",
|
||||
"judul": "TP. Posyandu Bali Gelar Aksi Sosial ‘Membina dan Berbagi’ di Desa Darmasaba",
|
||||
"deskripsi": "<p>Kegiatan pembinaan dan bantuan kepada kader Posyandu Desa Darmasaba oleh TP Posyandu Provinsi Bali.</p>",
|
||||
"content": "<p>Sebanyak 50 kader posyandu mendapatkan pembinaan dan sembako sebagai dukungan terhadap peran Posyandu dalam pemberdayaan masyarakat. Kegiatan ini menunjukkan peran strategis posyandu dalam layanan publik desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tghy000vvnv8xeouenv5",
|
||||
"imageName": "Wp41ccw3yma9W8i6zBr6E-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6af7vf00013b6rj2br4nv8",
|
||||
"judul": "Desa Darmasaba Gelar Temu Sadar Hukum, Bahas Isu KDRT",
|
||||
"deskripsi": "<p>Temu Sadar Hukum untuk meningkatkan kesadaran hukum warga Desa Darmasaba.</p>",
|
||||
"content": "<p>Kegiatan ini membahas isu kekerasan dalam rumah tangga dengan narasumber dari Kanwil Kemenkum Bali, menjadi forum penting dalam membangun masyarakat yang melek hukum.</p>",
|
||||
"kategoriBeritaId": "cmk69tghy000vvnv8xeouenv5",
|
||||
"imageName": "IwedNmhjD_wGpY6PvYX7W-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6afo0g00033b6rjc2pae67",
|
||||
"judul": "Bicara Darmasaba Bahas Berbagai Persoalan, Dorong Warga Sampaikan Aspirasi",
|
||||
"deskripsi": "<p>Ruang dialog terbuka di Desa Darmasaba untuk membahas persoalan sampah dan partisipasi publik.</p>",
|
||||
"content": "<p>Forum dialog ini membantu pemerintahan desa menyusun kebijakan tepat sasaran, terutama terkait permasalahan lingkungan dan aspirasi warga.</p>",
|
||||
"kategoriBeritaId": "cmk69tghy000vvnv8xeouenv5",
|
||||
"imageName": "EVkMxPdoWyL3y31L7d7x1-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6ag56y00053b6rj9481z6m",
|
||||
"judul": "Bicara Darmasaba Bahas ‘Sampah Kita, Tanggung Jawab Siapa?’",
|
||||
"deskripsi": "<p>Diskusi terbuka antara pemerintah desa dan warga mengenai isu sampah.</p>",
|
||||
"content": "<p>Acara ini mendukung perumusan kebijakan desa yang sesuai kebutuhan warga dan meningkatkan partisipasi dalam pembangunan lingkungan.</p>",
|
||||
"kategoriBeritaId": "cmk69tghx000tvnv8g2d206wv",
|
||||
"imageName": "c_5xOKUbMiD8dTAbkAv9a-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6agzxx00073b6rr3vhxcsj",
|
||||
"judul": "Penutupan KKN-PMM Periode II Universitas Warmadewa di Desa Darmasaba",
|
||||
"deskripsi": "<p>Penutupan program KKN-PMM yang berjalan dengan berbagai kegiatan pemberdayaan masyarakat.</p>",
|
||||
"content": "<p>Kegiatan KKN meliputi edukasi kesehatan, pengelolaan sampah, literasi keuangan, dan upaya ekonomi lokal sebagai bagian dari pembangunan desa berkelanjutan.</p>",
|
||||
"kategoriBeritaId": "cmk69tghx000tvnv8g2d206wv",
|
||||
"imageName": "I9CDBqdeDXRbbzbPWhy6h-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6agzxx00073b6rr3vhxasj",
|
||||
"judul": "Desa Darmasaba Siap Kelola Sampah Mandiri, Anggarkan Rp1,5 Miliar",
|
||||
"deskripsi": "<p>Desa Darmasaba mengalokasikan anggaran untuk pengelolaan sampah berbasis komunitas.</p>",
|
||||
"content": "<p>Pengelolaan sampah mandiri melalui TPS3R, kader penyuluh, dan inovasi CINtA menjadi strategi desa dalam penanganan sampah sesuai kebijakan provinsi dan desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tghx000tvnv8g2d206wv",
|
||||
"imageName": "Gq-gEDaGNb-FkKasVs7i4-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6agzxx00073b6rr3vhxbsj",
|
||||
"judul": "Sekda Adi Arnawa Buka Darmasaba Village Festival II",
|
||||
"deskripsi": "<p>Pembukaan festival desa untuk mendorong UMKM dan ekonomi lokal.</p>",
|
||||
"content": "<p>Kegiatan ini menampilkan berbagai UMKM yang membantu meningkatkan pendapatan masyarakat setempat.</p>",
|
||||
"kategoriBeritaId": "cmk69tghx000uvnv847ppcxqh",
|
||||
"imageName": "TymQ5xDH7vEUOA9BY63hr-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6agzxx00073b6rr3vhxdsj",
|
||||
"judul": "Membangun Desa Berkelanjutan Melalui Ekowisata dan Kuliner di Darmasaba",
|
||||
"deskripsi": "<p>Program inovatif untuk memperkuat ekonomi lokal melalui ekowisata dan kuliner.</p>",
|
||||
"content": "<p>Kegiatan mencakup pembangunan green house, edukasi pemasaran digital, literasi bahasa Inggris, dan pengembangan potensi kuliner desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tghx000uvnv847ppcxqh",
|
||||
"imageName": "2nUvEBsMuigIJQWZIdcEJ-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6agzxx00073b6rr3vhxesj",
|
||||
"judul": "Inovasi Desa Darmasaba Lanjutkan Perjuangan ke Tingkat Nasional",
|
||||
"deskripsi": "<p>Desa Darmasaba meraih penghargaan juara dalam evaluasi perkembangan desa untuk mendukung perekonomian dan pemerintahan lokal.</p>",
|
||||
"content": "<p>Prestasi desa ditandai dengan keberhasilan dalam lomba evaluasi perkembangan desa di tingkat provinsi dan kabupaten, yang berdampak positif pada ekonomi desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tghx000uvnv847ppcxqh",
|
||||
"imageName": "Hokgum3-nI_NWTWJRnWi3-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6agzxx00073b6rr3vhxfsj",
|
||||
"judul": "Desa Darmasaba Kembali Ukir Prestasi Internasional BAJRA",
|
||||
"deskripsi": "<p>Prestasi desa dalam forum internasional mengenai penanggulangan rabies.</p>",
|
||||
"content": "<p>Partisipasi dalam konferensi Rabies in Borneo menunjukkan kolaborasi lintas sektor dan penggunaan data dalam pelayanan publik desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tght000svnv8ok5rid2v",
|
||||
"imageName": "T1fcksUoZSUqNMbzvr9WI-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp1z",
|
||||
"judul": "Cegah Penyebaran Rabies, Darmasaba Keluarkan Larangan Membuang Hewan",
|
||||
"deskripsi": "<p>Pemasangan spanduk larangan buang hewan sebagai langkah proteksi kesehatan masyarakat.</p>",
|
||||
"content": "<p>Pemerintah desa bersama Tim Bajra aktif dalam kampanye dan regulasi untuk mencegah rabies di tingkat desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tght000svnv8ok5rid2v",
|
||||
"imageName": "-M_tICRVz6ZxOfvkuHQgU-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp2z",
|
||||
"judul": "TP. Posyandu Bali dan Pemerintah Desa Kolaborasi Tingkatkan Pelayanan",
|
||||
"deskripsi": "<p>Kolaborasi pemerintahan desa dengan TP Posyandu untuk meningkatkan layanan masyarakat.</p>",
|
||||
"content": "<p>Kegiatan ini menunjukkan peran pemerintahan desa dalam mendukung layanan kesehatan dan pemberdayaan masyarakat.</p>",
|
||||
"kategoriBeritaId": "cmk69tght000svnv8ok5rid2v",
|
||||
"imageName": "O11hmN9oNwFKs_ACpH9uV-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp3z",
|
||||
"judul": "Membangun Desa Berkelanjutan Melalui Inovasi Pengelolaan Sampah",
|
||||
"deskripsi": "<p>Inisiatif pengelolaan sampah desa sebagai bagian dari inovasi teknologi lokal.</p>",
|
||||
"content": "<p>Penerapan metode pengelolaan sampah dan biopori menunjukkan upaya Desa Darmasaba dalam menggunakan solusi teknologi sederhana untuk masalah lingkungan.</p>",
|
||||
"kategoriBeritaId": "cmk69tghz000xvnv8kxzzt24h",
|
||||
"imageName": "rrgHHUYHDuq3jW94HCRsq-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp4z",
|
||||
"judul": "Inovasi BAJRA Integrasikan Pelaporan Cepat Berbasis Data",
|
||||
"deskripsi": "<p>Program BAJRA menerapkan mekanisme pelaporan cepat berbasis data untuk penanggulangan rabies.</p>",
|
||||
"content": "<p>Penggunaan teknologi informasi dalam pelaporan kasus rabies membantu respons cepat pemerintahan desa dan komunitas.</p>",
|
||||
"kategoriBeritaId": "cmk69tghz000xvnv8kxzzt24h",
|
||||
"imageName": "igz0V0MCoLYqAgLIBRZdG-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp5z",
|
||||
"judul": "Digitalisasi Desa Darmasaba",
|
||||
"deskripsi": "<p>Digitalisasi Desa Darmasaba Bersama PT. Bali Interaktif Perkasa.</p>",
|
||||
"content": "<p>Digitalisasi Desa Darmasaba Bersama PT. Bali Interaktif Perkasa<br><br>Dalam rangka mendukung transformasi digital dan inovasi desa, Desa Darmasaba bekerja sama dengan PT. Bali Interaktif Perkasa melaksanakan kegiatan Digitalisasi Desa.<br><br>Program ini bertujuan untuk memperkuat kapasitas desa dalam pemanfaatan teknologi informasi dan komunikasi, sehingga pelayanan publik menjadi lebih efektif, transparan, dan cepat. Masyarakat juga diberikan pemahaman terkait pemanfaatan platform digital untuk kegiatan administrasi, komunikasi, dan pengembangan potensi desa.<br><br>Kegiatan digitalisasi ini menjadi bagian dari komitmen Desa Darmasaba untuk mewujudkan desa cerdas (smart village) yang mampu bersaing dan beradaptasi di era digital, sekaligus meningkatkan inovasi dan pemberdayaan masyarakat.<br><br>Dengan kolaborasi ini, Desa Darmasaba menegaskan tekadnya untuk terus berinovasi, menghadirkan kemudahan bagi masyarakat, dan memperkuat tata kelola desa berbasis teknologi modern.<br><br>? Digitalisasi hari ini, kemajuan desa esok!<br><br>#DesaDarmasaba #DigitalisasiDesa #DesaCerdas #InovasiDesa #TransformasiDigital<br>#PemdesDarmasaba<br>#PerbekelDarmasaba<br>#DesaDarmasaba<br> #KitaDarmasaba<br> #DarmasabaBisa<br> <br> @kostergubernurbali<br> @giri.prasta<br> @iwayanadiarnawa<br> @gus.bota<br> @puturasniathiadiarnawa<br> @yunita_oktarini<br> @surya.suamba<br> @budhi.argawakba<br> @pemkabbadung<br> @ppidbadung<br> @dinaspmddukcapilprovbali<br> @surya_prabhawa<br> @kecamatanabiansemal<br> @dpmdbadungkab<br> @pemprov_bali<br> @prokompimbadung<br> @seputar_darmasaba</p>",
|
||||
"kategoriBeritaId": "cmk69tghz000xvnv8kxzzt24h",
|
||||
"imageName": "DzVIfpiAP3OcCsZ_VJ02b-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp6z",
|
||||
"judul": "Festival Desa Tingkatkan Kreativitas Digital UMKM",
|
||||
"deskripsi": "<p>Festival Darmasaba Village Festival II melibatkan promosi digital produk UMKM.</p>",
|
||||
"content": "<p>Promosi dan dokumentasi digital menjadi bagian dari strategi pemasaran UMKM dalam festival desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tghy000wvnv8umg2vloa",
|
||||
"imageName": "xzM77A6bDW2silyp_8W7n-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp7z",
|
||||
"judul": "Sekda Adi Arnawa dan Pementasan Seni Tradisional di Festival Darmasaba",
|
||||
"deskripsi": "<p>Pementasan seni tradisional menjadi bagian dari Darmasaba Village Festival II.</p>",
|
||||
"content": "<p>Kegiatan ini mengangkat warisan budaya lokal melalui pertunjukan dan lomba di festival desa.</p>",
|
||||
"kategoriBeritaId": "cmk69tghy000wvnv8umg2vloa",
|
||||
"imageName": "2wivBEDcVNxHGG8HUBsNH-mobile.webp"
|
||||
},
|
||||
{
|
||||
"id": "cmk6aih8f00093b6rqw63yp8z",
|
||||
"judul": "Dialog Publik Tingkatkan Partisipasi Budaya Lokal",
|
||||
"deskripsi": "<p>Forum dialog desa mengangkat tema partisipasi masyarakat dalam kegiatan budaya lokal.</p>",
|
||||
"content": "<p>Diskusi ini memperkuat peran budaya dalam pembangunan desa melalui keterlibatan warga dalam kegiatan adat dan sosial.</p>",
|
||||
"kategoriBeritaId": "cmk69tghy000wvnv8umg2vloa",
|
||||
"imageName": "T1fcksUoZSUqNMbzvr9WI-mobile.webp"
|
||||
}
|
||||
]
|
||||
8
prisma/data/desa/berita/kategori-berita.json
Normal file
8
prisma/data/desa/berita/kategori-berita.json
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{ "id": "cmk69tght000svnv8ok5rid2v", "name": "Pemerintahan" },
|
||||
{ "id": "cmk69tghx000tvnv8g2d206wv", "name": "Pembangunan" },
|
||||
{ "id": "cmk69tghx000uvnv847ppcxqh", "name": "Ekonomi" },
|
||||
{ "id": "cmk69tghy000vvnv8xeouenv5", "name": "Sosial" },
|
||||
{ "id": "cmk69tghy000wvnv8umg2vloa", "name": "Budaya" },
|
||||
{ "id": "cmk69tghz000xvnv8kxzzt24h", "name": "Teknologi" }
|
||||
]
|
||||
20
prisma/data/desa/gallery/foto/foto.json
Normal file
20
prisma/data/desa/gallery/foto/foto.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"id": "cml0aiiv1000004l754ldaf2v",
|
||||
"name": "Kunjungan Ibu TP PKK Kabupaten Badung",
|
||||
"deskripsi": "<p>Dokumentasi kunjungan Ibu TP PKK Kabupaten Badung ke Desa Darmasaba pada awal tahun 2026.</p>",
|
||||
"imageName": "foto1Gallery.webp"
|
||||
},
|
||||
{
|
||||
"id": "cml0aiqd4000104l7f91ee3xu",
|
||||
"name": "Darmasaba Village Festival II 2024",
|
||||
"deskripsi": "<p>Foto kegiatan Darmasaba Village Festival II Tahun 2024 yang diselenggarakan di Lapangan Umum Desa Darmasaba, menampilkan lomba, pementasan seni, dan UMKM lokal.</p>",
|
||||
"imageName": "foto2Gallery.webp"
|
||||
},
|
||||
{
|
||||
"id": "cml0aiyi7000204l7f2sy657c",
|
||||
"name": "Lomba Mancing Air Deras Banjar Gulingan",
|
||||
"deskripsi": "<p>Galeri foto kegiatan lomba mancing air deras Komunitas Pemancing Gulingan di Desa Darmasaba, bagian dari pemberdayaan masyarakat.</p>",
|
||||
"imageName": "foto3Gallery.webp"
|
||||
}
|
||||
]
|
||||
20
prisma/data/desa/gallery/video/video.json
Normal file
20
prisma/data/desa/gallery/video/video.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"id": "cmk6kvn6b0000vn6qzg5z6qa6",
|
||||
"judul": "TAHAP PENILAIAN VERIFIKASI LAPANGAN AJANG MANGUPURA AWARD TAHUN 2025",
|
||||
"deskripsi": "<p>TAHAP PENILAIAN VERIFIKASI LAPANGAN AJANG MANGUPURA AWARD Senin, 29 September 2025 – Pemerintah Desa Darmasaba mengikuti tahap penilaian verifikasi lapangan dalam rangkaian Ajang Mangupura Award Tahun 2025 pada kategori Pemerintah Desa. Ajang bergengsi ini merupakan bentuk apresiasi Pemerintah Kabupaten Badung kepada desa-desa yang berprestasi dalam tata kelola aset, keuangan, arsip, tata kelola sumber daya manusia, pelayanan publik dan persampahan, inovasi, sinergitas, dan akuntabilitas. Proses verifikasi lapangan diawali di Kantor Perbekel Darmasaba dengan pemeriksaan langsung kepada masing-masing pengampu indikator, kemudian dilanjutkan dengan kunjungan ke BUMDes Pudak Mesari serta TPS 3R Pudak Mesari sebagai bentuk evaluasi nyata terhadap kinerja dan program desa. Kegiatan ini menjadi langkah penting dalam menilai implementasi tata kelola pemerintahan desa yang transparan, inovatif, dan berkelanjutan. Melalui tahapan ini, Pemerintah Desa Darmasaba berharap dapat terus menghadirkan pelayanan terbaik bagi masyarakat, mengembangkan inovasi desa, serta memperkuat sinergi antara pemerintah, desa adat, dan masyarakat dalam mewujudkan pembangunan daerah yang maju, berdaya saing, dan berkelanjutan. #MangupuraAward2025 #Darmasaba #DesaBerprestasi #PemerintahDesa #Badung #Abiansemal #PemdesDarmasaba #PerbekelDarmasaba #DesaDarmasaba #KitaDarmasaba #DarmasabaBisa</p>",
|
||||
"linkVideo": "https://www.youtube.com/watch?v=e2tSRnNkYDE"
|
||||
},
|
||||
{
|
||||
"id": "cmk6kvn6b0000vn6qzg5z6qb7",
|
||||
"judul": "Vaksinasi Rabies di Desa Darmasaba",
|
||||
"deskripsi": "<p>Vaksinasi Rabies di Desa Darmasaba Selasa, 7 Oktober 2025 Pemerintah Desa Darmasaba melalui Tim Bajra Desa Darmasaba, bekerja sama dengan Dinas Pertanian dan Pangan Kabupaten Badung Bidang Kesehatan Hewan, menyelenggarakan kegiatan Vaksinasi Rabies bagi hewan penular rabies (HPR) yang meliputi anjing, kucing, dan kera di wilayah Desa Darmasaba. Kegiatan ini dilaksanakan sebagai langkah preventif untuk menekan penyebaran virus rabies sekaligus memberikan perlindungan kesehatan bagi hewan peliharaan maupun masyarakat. Melalui program vaksinasi ini, diharapkan Desa Darmasaba dapat terbebas dari ancaman rabies dan semakin meningkatkan kesadaran masyarakat akan pentingnya menjaga kesehatan hewan peliharaan. Pemerintah Desa Darmasaba mengimbau seluruh warga untuk memastikan hewan kesayangan mendapatkan vaksinasi sesuai jadwal yang telah ditentukan. Informasi lengkap mengenai jadwal vaksinasi dapat dilihat pada pengumuman yang tertera. Selain itu, demi kelancaran proses vaksinasi, diharapkan pemilik hewan dapat mengikat atau mengandangkan hewan peliharaannya saat proses vaksinasi berlangsung. Dengan adanya kolaborasi antara pemerintah desa, dinas terkait, dan partisipasi aktif masyarakat, kegiatan ini diharapkan mampu memberikan manfaat nyata serta menciptakan lingkungan Desa Darmasaba yang lebih sehat, aman, dan terbebas dari rabies. #VaksinasiRabies #KesehatanHewan #BebasRabies #Badung #Abiansemal #TimBajraDarmasaba #PemdesDarmasaba #PerbekelDarmasaba #DesaDarmasaba #KitaDarmasaba #DarmasabaBisa</p>",
|
||||
"linkVideo": "https://www.youtube.com/watch?v=bc99y94FBx8"
|
||||
},
|
||||
{
|
||||
"id": "cmk6kvn6b0000vn6qzg5z6qc8",
|
||||
"judul": "Musyawarah Perencanaan Pembangunan Desa Darmasaba Tahun 2026",
|
||||
"deskripsi":"<p>Musyawarah Perencanaan Pembangunan Desa Darmasaba Tahun 2026 Pemerintah Desa Darmasaba menyelenggarakan Musyawarah Perencanaan Pembangunan Desa (Musrenbangdes)dalam rangka penyusunan Rencana Kerja Pemerintah Desa (RKP Desa) Tahun 2026. Musrenbangdes ini merupakan forum resmi yang mempertemukan perwakilan dari DPMD Kab. Badung, Kecamatan Abiansemal, Pemerintah Desa, Lembaga Desa, Tokoh Masyarakat, serta perwakilan unsur lainnya untuk bersama-sama merumuskan arah pembangunan desa di tahun mendatang. Melalui kegiatan ini, seluruh peserta diberikan ruang untuk menyampaikan usulan, gagasan, serta masukan yang berkaitan dengan prioritas pembangunan, baik di bidang infrastruktur, pemberdayaan masyarakat, ekonomi, sosial, maupun pelestarian adat dan budaya. Proses musyawarah ini menjadi bagian penting dalam mewujudkan pembangunan desa yang partisipatif, transparan, dan berorientasi pada kebutuhan masyarakat. Dengan terselenggaranya Musrenbangdes ini, Pemerintah Desa Darmasaba berharap RKP Desa Tahun 2026 dapat disusun secara komprehensif, berkelanjutan, dan selaras dengan visi pembangunan daerah. Selain itu, kegiatan ini juga menegaskan komitmen Pemerintah Desa Darmasaba untuk terus menghadirkan pembangunan yang inklusif, berkeadilan, dan bermanfaat bagi seluruh lapisan masyarakat. #Musrenbangdes2026 #RKPDarmasaba2026 #PemdesDarmasaba #PerbekelDarmasaba #DesaDarmasaba #KitaDarmasaba #DarmasabaBisa</p>",
|
||||
"linkVideo": "https://www.youtube.com/watch?v=7pirwEmyP-4"
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user