diff --git a/.gemini/hooks/telegram-notify.ts b/.gemini/hooks/telegram-notify.ts new file mode 100755 index 00000000..063d337e --- /dev/null +++ b/.gemini/hooks/telegram-notify.ts @@ -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(); diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 00000000..ed736356 --- /dev/null +++ b/.gemini/settings.json @@ -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 + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index ebd64b35..2f3afc79 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ yarn-error.log* # env .env* +# QC +QC + # vercel .vercel diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..7e257db9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": [] +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..dc9d86dc --- /dev/null +++ b/AGENTS.md @@ -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 \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..4c8370da --- /dev/null +++ b/GEMINI.md @@ -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. diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 00000000..21d07dbf --- /dev/null +++ b/QWEN.md @@ -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 \ No newline at end of file diff --git a/__tests__/api/fileStorage.test.ts b/__tests__/api/fileStorage.test.ts new file mode 100644 index 00000000..ca12c5a2 --- /dev/null +++ b/__tests__/api/fileStorage.test.ts @@ -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'); + }); +}); diff --git a/__tests__/e2e/homepage.spec.ts b/__tests__/e2e/homepage.spec.ts new file mode 100644 index 00000000..57b02234 --- /dev/null +++ b/__tests__/e2e/homepage.spec.ts @@ -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(); +}); diff --git a/__tests__/mocks/handlers.ts b/__tests__/mocks/handlers.ts new file mode 100644 index 00000000..2854bf86 --- /dev/null +++ b/__tests__/mocks/handlers.ts @@ -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, + } + }); + }), +]; diff --git a/__tests__/mocks/server.ts b/__tests__/mocks/server.ts new file mode 100644 index 00000000..e52fee0a --- /dev/null +++ b/__tests__/mocks/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/__tests__/setup.ts b/__tests__/setup.ts new file mode 100644 index 00000000..83d8b89c --- /dev/null +++ b/__tests__/setup.ts @@ -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()); diff --git a/bun.lockb b/bun.lockb index e571b0b1..7f208dd4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/darkMode.md b/darkMode.md new file mode 100644 index 00000000..713ac5db --- /dev/null +++ b/darkMode.md @@ -0,0 +1,169 @@ +# 🌙 Dark Mode Design Specification +## Admin Darmasaba – Dashboard & CMS + +Dokumen ini mendefinisikan standar **Dark Mode UI** agar: +- nyaman di mata +- konsisten +- tidak flat +- tetap profesional untuk aplikasi pemerintahan + +--- + +## 🎨 Color Palette (Dark Mode) + +### Background Layers +| Layer | Token | Warna | Fungsi | +|------|------|------|------| +| Base | `--bg-base` | `#0B1220` | Background utama aplikasi | +| App | `--bg-app` | `#0F172A` | Area kerja utama | +| Card | `--bg-card` | `#162235` | Card / container | +| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input | + +--- + +### Border & Divider +| Token | Warna | Catatan | +|-----|------|--------| +| `--border-default` | `#2A3A52` | Border utama | +| `--border-soft` | `#22314A` | Divider halus | + +> ❗ Hindari border terlalu tipis (`opacity < 20%`) + +--- + +### Text Colors +| Jenis | Token | Warna | +|-----|------|------| +| Primary | `--text-primary` | `#E5E7EB` | +| Secondary | `--text-secondary` | `#9CA3AF` | +| Muted | `--text-muted` | `#6B7280` | +| Inverse | `--text-inverse` | `#020617` | + +--- + +### Accent & Action +| Fungsi | Warna | +|------|------| +| Primary Action | `#3B82F6` | +| Hover | `#2563EB` | +| Active | `#1D4ED8` | +| Link | `#60A5FA` | + +--- + +### Status Colors +| Status | Warna | +|------|------| +| Success | `#22C55E` | +| Warning | `#FACC15` | +| Error | `#EF4444` | +| Info | `#38BDF8` | + +--- + +## 🧱 Layout Rules + +### Sidebar +- Background: `--bg-app` +- Active menu: + - Background: `rgba(59,130,246,0.15)` + - Text: Primary + - Indicator: kiri (2–3px accent bar) +- Hover: + - Background: `rgba(255,255,255,0.04)` + +--- + +### Header / Topbar +- Background: `linear-gradient(#0F172A → #0B1220)` +- Border bawah wajib (`--border-soft`) +- Icon: + - Default: muted + - Hover: primary + +--- + +## 📦 Card & Section + +### Card +- Background: `--bg-card` +- Border: `--border-default` +- Radius: 12–16px +- Jangan pakai shadow hitam + +### Section Header +- Font weight lebih besar +- Text: primary +- Spacing jelas dari konten + +--- + +## 📊 Table (Dark Mode Friendly) + +### Table Header +- Background: `--bg-surface` +- Text: secondary +- Font weight: medium + +### Table Row +- Default: transparent +- Hover: + - Background: `rgba(255,255,255,0.03)` +- Divider antar row wajib terlihat + +### Link di Table +- Warna link **lebih terang dari text** +- Hover underline + +--- + +## 🔘 Button Rules + +### Primary Button +- Background: Primary Action +- Text: Inverse +- Hover: darker shade + +### Secondary Button +- Background: transparent +- Border: `--border-default` +- Text: primary + +### Icon Button +- Default: muted +- Hover: primary + bg soft + +--- + +## 🧭 Tab Navigation + +- Inactive: + - Text: muted +- Active: + - Background: `rgba(59,130,246,0.15)` + - Text: primary + - Icon ikut berubah + +--- + +## 🌗 Dark vs Light Mode Rule +- Layout, spacing, typography **HARUS SAMA** +- Yang boleh beda: + - warna + - border intensity + - background layer + +> ❌ Jangan ganti struktur UI antara dark & light + +--- + +## ✅ Dark Mode Checklist +- [ ] Kontras teks terbaca +- [ ] Active state jelas +- [ ] Hover terasa hidup +- [ ] Tidak flat +- [ ] Tidak silau + +--- + +Dokumen ini adalah **single source of truth** untuk Dark Mode. \ No newline at end of file diff --git a/gambar.ttx b/gambar.ttx new file mode 100644 index 00000000..6fa9e10e --- /dev/null +++ b/gambar.ttx @@ -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 { + 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 { + + + 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); + } +})(); diff --git a/package.json b/package.json index 472efe96..61958b92 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,12 @@ "version": "0.1.5", "private": true, "scripts": { - "dev": "bun --bun next dev", - "build": "bun --bun next build", - "start": "bun --bun next start" + "dev": "next dev", + "build": "next build", + "start": "next start", + "test:api": "vitest run", + "test:e2e": "playwright test", + "test": "bun run test:api && bun run test:e2e" }, "prisma": { "seed": "bun run prisma/seed.ts" @@ -19,6 +22,7 @@ "@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.17.4", @@ -26,6 +30,7 @@ "@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", @@ -43,20 +48,26 @@ "@types/bun": "^1.2.2", "@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", "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", + "dompurify": "^3.3.1", "dotenv": "^17.2.3", "elysia": "^1.3.5", - "embla-carousel-autoplay": "^8.5.2", - "embla-carousel-react": "^7.1.0", + "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", @@ -68,6 +79,7 @@ "leaflet": "^1.9.4", "list": "^2.0.19", "lodash": "^4.17.21", + "mime-types": "^3.0.2", "motion": "^12.4.1", "nanoid": "^5.1.5", "next": "^15.5.2", @@ -80,6 +92,7 @@ "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", @@ -97,16 +110,24 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@playwright/test": "^1.58.2", + "@testing-library/jest-dom": "^6.9.1", + "@types/cli-progress": "^3.11.6", + "@types/dompurify": "^3.2.0", "@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" } } diff --git a/playwright-report/data/3262232ac1998014dfaa14b6734778979a7c99c4.md b/playwright-report/data/3262232ac1998014dfaa14b6734778979a7c99c4.md new file mode 100644 index 00000000..c0cf174c --- /dev/null +++ b/playwright-report/data/3262232ac1998014dfaa14b6734778979a7c99c4.md @@ -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] +``` \ No newline at end of file diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 00000000..412d60b5 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,85 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..c5c67f4c --- /dev/null +++ b/playwright.config.ts @@ -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, + }, +}); diff --git a/postcss.config.cjs b/postcss.config.cjs index 069b0528..9e9652a2 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -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 }, }, - }; \ No newline at end of file + }, +}; diff --git a/prisma/_seeder_list/desa/berita/seed_berita.ts b/prisma/_seeder_list/desa/berita/seed_berita.ts new file mode 100644 index 00000000..1d1fb9f0 --- /dev/null +++ b/prisma/_seeder_list/desa/berita/seed_berita.ts @@ -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"); +} + diff --git a/prisma/_seeder_list/desa/gallery/foto/seed_foto.ts b/prisma/_seeder_list/desa/gallery/foto/seed_foto.ts new file mode 100644 index 00000000..6abf33c2 --- /dev/null +++ b/prisma/_seeder_list/desa/gallery/foto/seed_foto.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/desa/gallery/video/seed_video.ts b/prisma/_seeder_list/desa/gallery/video/seed_video.ts new file mode 100644 index 00000000..42d97860 --- /dev/null +++ b/prisma/_seeder_list/desa/gallery/video/seed_video.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/desa/layanan/seed_layanan.ts b/prisma/_seeder_list/desa/layanan/seed_layanan.ts new file mode 100644 index 00000000..a242458c --- /dev/null +++ b/prisma/_seeder_list/desa/layanan/seed_layanan.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/desa/penghargaan/penghargaan.ts b/prisma/_seeder_list/desa/penghargaan/penghargaan.ts new file mode 100644 index 00000000..b696ae75 --- /dev/null +++ b/prisma/_seeder_list/desa/penghargaan/penghargaan.ts @@ -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 ..."); +} + \ No newline at end of file diff --git a/prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts b/prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts new file mode 100644 index 00000000..184fc804 --- /dev/null +++ b/prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/desa/potensi/seed_potensi.ts b/prisma/_seeder_list/desa/potensi/seed_potensi.ts new file mode 100644 index 00000000..7e1ad408 --- /dev/null +++ b/prisma/_seeder_list/desa/potensi/seed_potensi.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts b/prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts new file mode 100644 index 00000000..68af0dca --- /dev/null +++ b/prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/desa/profile-desa/seed_profile_perbekel.ts b/prisma/_seeder_list/desa/profile-desa/seed_profile_perbekel.ts new file mode 100644 index 00000000..1507fe32 --- /dev/null +++ b/prisma/_seeder_list/desa/profile-desa/seed_profile_perbekel.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts b/prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts new file mode 100644 index 00000000..41d4dc07 --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts @@ -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"); +} diff --git a/prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts b/prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts new file mode 100644 index 00000000..48374742 --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts @@ -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"); +} diff --git a/prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts b/prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts new file mode 100644 index 00000000..b1f2cb03 --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts b/prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts new file mode 100644 index 00000000..95494b24 --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts @@ -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"); +} diff --git a/prisma/_seeder_list/ekonomi/seed_pasar_desa.ts b/prisma/_seeder_list/ekonomi/seed_pasar_desa.ts new file mode 100644 index 00000000..f8ad4105 --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_pasar_desa.ts @@ -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, + }, + }); + } +} diff --git a/prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts b/prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts new file mode 100644 index 00000000..93a30a5e --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts @@ -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"); +} diff --git a/prisma/_seeder_list/ekonomi/seed_penduduk_usia_kerja_yang_menganggur.ts b/prisma/_seeder_list/ekonomi/seed_penduduk_usia_kerja_yang_menganggur.ts new file mode 100644 index 00000000..ac64954f --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_penduduk_usia_kerja_yang_menganggur.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts b/prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts new file mode 100644 index 00000000..748b1f8b --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts b/prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts new file mode 100644 index 00000000..0bfd469e --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts b/prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts new file mode 100644 index 00000000..47f92ed7 --- /dev/null +++ b/prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/inovasi/seed_ajukan.ts b/prisma/_seeder_list/inovasi/seed_ajukan.ts new file mode 100644 index 00000000..ee690fc0 --- /dev/null +++ b/prisma/_seeder_list/inovasi/seed_ajukan.ts @@ -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"); +} diff --git a/prisma/_seeder_list/inovasi/seed_desa_digital.ts b/prisma/_seeder_list/inovasi/seed_desa_digital.ts new file mode 100644 index 00000000..0c0b2209 --- /dev/null +++ b/prisma/_seeder_list/inovasi/seed_desa_digital.ts @@ -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"); +} diff --git a/prisma/_seeder_list/inovasi/seed_info_teknologi.ts b/prisma/_seeder_list/inovasi/seed_info_teknologi.ts new file mode 100644 index 00000000..768cde5b --- /dev/null +++ b/prisma/_seeder_list/inovasi/seed_info_teknologi.ts @@ -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"); +} diff --git a/prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts b/prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts new file mode 100644 index 00000000..7a725478 --- /dev/null +++ b/prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts b/prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts new file mode 100644 index 00000000..d233ca00 --- /dev/null +++ b/prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts @@ -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"); +} diff --git a/prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts b/prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts new file mode 100644 index 00000000..e98584b2 --- /dev/null +++ b/prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts b/prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts new file mode 100644 index 00000000..018dcb19 --- /dev/null +++ b/prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/keamanan/seed_kontak_darurat.ts b/prisma/_seeder_list/keamanan/seed_kontak_darurat.ts new file mode 100644 index 00000000..d108c03a --- /dev/null +++ b/prisma/_seeder_list/keamanan/seed_kontak_darurat.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/keamanan/seed_laporan_publik.ts b/prisma/_seeder_list/keamanan/seed_laporan_publik.ts new file mode 100644 index 00000000..a5056bb6 --- /dev/null +++ b/prisma/_seeder_list/keamanan/seed_laporan_publik.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts b/prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts new file mode 100644 index 00000000..1a11b9a7 --- /dev/null +++ b/prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts @@ -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"); +} diff --git a/prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts b/prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts new file mode 100644 index 00000000..1e8ff8de --- /dev/null +++ b/prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/keamanan/seed_tips_keamanan.ts b/prisma/_seeder_list/keamanan/seed_tips_keamanan.ts new file mode 100644 index 00000000..5b5a3abc --- /dev/null +++ b/prisma/_seeder_list/keamanan/seed_tips_keamanan.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/kesehatan/info-wabah-penyakit/seed_info_wabah_penyakit.ts b/prisma/_seeder_list/kesehatan/info-wabah-penyakit/seed_info_wabah_penyakit.ts new file mode 100644 index 00000000..922bb848 --- /dev/null +++ b/prisma/_seeder_list/kesehatan/info-wabah-penyakit/seed_info_wabah_penyakit.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/kesehatan/kontak-darurat/seed_kontak_darurat.ts b/prisma/_seeder_list/kesehatan/kontak-darurat/seed_kontak_darurat.ts new file mode 100644 index 00000000..d42aec3a --- /dev/null +++ b/prisma/_seeder_list/kesehatan/kontak-darurat/seed_kontak_darurat.ts @@ -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"); +} diff --git a/prisma/_seeder_list/kesehatan/penanganan-darurat/seed_penanganan_darurat.ts b/prisma/_seeder_list/kesehatan/penanganan-darurat/seed_penanganan_darurat.ts new file mode 100644 index 00000000..788b1dc7 --- /dev/null +++ b/prisma/_seeder_list/kesehatan/penanganan-darurat/seed_penanganan_darurat.ts @@ -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"); +} diff --git a/prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts b/prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts new file mode 100644 index 00000000..fce475ca --- /dev/null +++ b/prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/kesehatan/program-kesehatan/seed_program_kesehatan.ts b/prisma/_seeder_list/kesehatan/program-kesehatan/seed_program_kesehatan.ts new file mode 100644 index 00000000..4c898e44 --- /dev/null +++ b/prisma/_seeder_list/kesehatan/program-kesehatan/seed_program_kesehatan.ts @@ -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}`); + } +} \ No newline at end of file diff --git a/prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts b/prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts new file mode 100644 index 00000000..2c9a632b --- /dev/null +++ b/prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts @@ -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"); +} + diff --git a/prisma/_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi.ts b/prisma/_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi.ts new file mode 100644 index 00000000..5a1927fa --- /dev/null +++ b/prisma/_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa.ts b/prisma/_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa.ts new file mode 100644 index 00000000..18129dfb --- /dev/null +++ b/prisma/_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa.ts @@ -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 ..."); +} + \ No newline at end of file diff --git a/prisma/_seeder_list/landing-page/profil_landing_page/seed_media_sosial.ts b/prisma/_seeder_list/landing-page/profil_landing_page/seed_media_sosial.ts new file mode 100644 index 00000000..7636202a --- /dev/null +++ b/prisma/_seeder_list/landing-page/profil_landing_page/seed_media_sosial.ts @@ -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 ..."); +} + + + \ No newline at end of file diff --git a/prisma/_seeder_list/landing-page/profil_landing_page/seed_profile_lp.ts b/prisma/_seeder_list/landing-page/profil_landing_page/seed_profile_lp.ts new file mode 100644 index 00000000..a2f5f7a6 --- /dev/null +++ b/prisma/_seeder_list/landing-page/profil_landing_page/seed_profile_lp.ts @@ -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"); +} diff --git a/prisma/_seeder_list/landing-page/profil_landing_page/seed_program_inovasi.ts b/prisma/_seeder_list/landing-page/profil_landing_page/seed_program_inovasi.ts new file mode 100644 index 00000000..207045b2 --- /dev/null +++ b/prisma/_seeder_list/landing-page/profil_landing_page/seed_program_inovasi.ts @@ -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}`); + } +} \ No newline at end of file diff --git a/prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts b/prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts new file mode 100644 index 00000000..a8aac2a1 --- /dev/null +++ b/prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts b/prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts new file mode 100644 index 00000000..c317dad6 --- /dev/null +++ b/prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts b/prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts new file mode 100644 index 00000000..a182bf86 --- /dev/null +++ b/prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts @@ -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"); +} diff --git a/prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts b/prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts new file mode 100644 index 00000000..5d11b061 --- /dev/null +++ b/prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts b/prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts new file mode 100644 index 00000000..5278fbc5 --- /dev/null +++ b/prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts @@ -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 ..."); +} diff --git a/prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts b/prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts new file mode 100644 index 00000000..5940c79b --- /dev/null +++ b/prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts @@ -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"); +} diff --git a/prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts b/prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts new file mode 100644 index 00000000..0dbd8bc5 --- /dev/null +++ b/prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts b/prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts new file mode 100644 index 00000000..e6e927c0 --- /dev/null +++ b/prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts @@ -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)", + ); +} \ No newline at end of file diff --git a/prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts b/prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts new file mode 100644 index 00000000..632243f3 --- /dev/null +++ b/prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts @@ -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"); +} \ No newline at end of file diff --git a/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts b/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts new file mode 100644 index 00000000..5ee80718 --- /dev/null +++ b/prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts @@ -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)); + }); +} \ No newline at end of file diff --git a/prisma/_seeder_list/pendidikan/seed_info_program_pendidikan.ts b/prisma/_seeder_list/pendidikan/seed_info_program_pendidikan.ts new file mode 100644 index 00000000..cfa7798c --- /dev/null +++ b/prisma/_seeder_list/pendidikan/seed_info_program_pendidikan.ts @@ -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)"); +} diff --git a/prisma/_seeder_list/pendidikan/seed_info_sekolah.ts b/prisma/_seeder_list/pendidikan/seed_info_sekolah.ts new file mode 100644 index 00000000..39ba1154 --- /dev/null +++ b/prisma/_seeder_list/pendidikan/seed_info_sekolah.ts @@ -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"); +} diff --git a/prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts b/prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts new file mode 100644 index 00000000..151c8dbc --- /dev/null +++ b/prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts @@ -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)", + ); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ppid/daftar-informasi-publik-ppid/seed_daftar_informasi_publik_ppid.ts b/prisma/_seeder_list/ppid/daftar-informasi-publik-ppid/seed_daftar_informasi_publik_ppid.ts new file mode 100644 index 00000000..3bf6b77b --- /dev/null +++ b/prisma/_seeder_list/ppid/daftar-informasi-publik-ppid/seed_daftar_informasi_publik_ppid.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ppid/dasar-hukum-ppid/seed_dasar_hukum_ppid.ts b/prisma/_seeder_list/ppid/dasar-hukum-ppid/seed_dasar_hukum_ppid.ts new file mode 100644 index 00000000..7d14359a --- /dev/null +++ b/prisma/_seeder_list/ppid/dasar-hukum-ppid/seed_dasar_hukum_ppid.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ppid/ikm/seed_ikm.ts b/prisma/_seeder_list/ppid/ikm/seed_ikm.ts new file mode 100644 index 00000000..905b4f4d --- /dev/null +++ b/prisma/_seeder_list/ppid/ikm/seed_ikm.ts @@ -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 ..."); +} \ No newline at end of file diff --git a/prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts b/prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts new file mode 100644 index 00000000..bf666e2d --- /dev/null +++ b/prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts @@ -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 ..."); +} + \ No newline at end of file diff --git a/prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts b/prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts new file mode 100644 index 00000000..e2141e91 --- /dev/null +++ b/prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts @@ -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 ..."); +} + \ No newline at end of file diff --git a/prisma/_seeder_list/ppid/visi-misi-ppid/seed_visi_misi_ppid.ts b/prisma/_seeder_list/ppid/visi-misi-ppid/seed_visi_misi_ppid.ts new file mode 100644 index 00000000..e99087e3 --- /dev/null +++ b/prisma/_seeder_list/ppid/visi-misi-ppid/seed_visi_misi_ppid.ts @@ -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 ..."); + +} \ No newline at end of file diff --git a/prisma/data/category-pengumuman.json b/prisma/data/category-pengumuman.json deleted file mode 100644 index 5bacc742..00000000 --- a/prisma/data/category-pengumuman.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { "name": "Sosial & Kesehatan" }, - { "name": "Ekonomi & UMKM" }, - { "name": "Pendidikan & Kepemudaan" }, - { "name": "Lingkungan & Bencana" }, - { "name": "Adat & Budaya" }, - { "name": "Digitalisasi Desa" } -] diff --git a/prisma/data/desa/berita/berita.json b/prisma/data/desa/berita/berita.json new file mode 100644 index 00000000..904361fc --- /dev/null +++ b/prisma/data/desa/berita/berita.json @@ -0,0 +1,146 @@ +[ + { + "id": "cmk6ae8rz00003b6r06x7hsqi", + "judul": "TP. Posyandu Bali Gelar Aksi Sosial ‘Membina dan Berbagi’ di Desa Darmasaba", + "deskripsi": "

Kegiatan pembinaan dan bantuan kepada kader Posyandu Desa Darmasaba oleh TP Posyandu Provinsi Bali.

", + "content": "

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.

", + "kategoriBeritaId": "cmk69tghy000vvnv8xeouenv5", + "imageName": "Wp41ccw3yma9W8i6zBr6E-mobile.webp" + }, + { + "id": "cmk6af7vf00013b6rj2br4nv8", + "judul": "Desa Darmasaba Gelar Temu Sadar Hukum, Bahas Isu KDRT", + "deskripsi": "

Temu Sadar Hukum untuk meningkatkan kesadaran hukum warga Desa Darmasaba.

", + "content": "

Kegiatan ini membahas isu kekerasan dalam rumah tangga dengan narasumber dari Kanwil Kemenkum Bali, menjadi forum penting dalam membangun masyarakat yang melek hukum.

", + "kategoriBeritaId": "cmk69tghy000vvnv8xeouenv5", + "imageName": "IwedNmhjD_wGpY6PvYX7W-mobile.webp" + }, + { + "id": "cmk6afo0g00033b6rjc2pae67", + "judul": "Bicara Darmasaba Bahas Berbagai Persoalan, Dorong Warga Sampaikan Aspirasi", + "deskripsi": "

Ruang dialog terbuka di Desa Darmasaba untuk membahas persoalan sampah dan partisipasi publik.

", + "content": "

Forum dialog ini membantu pemerintahan desa menyusun kebijakan tepat sasaran, terutama terkait permasalahan lingkungan dan aspirasi warga.

", + "kategoriBeritaId": "cmk69tghy000vvnv8xeouenv5", + "imageName": "EVkMxPdoWyL3y31L7d7x1-mobile.webp" + }, + { + "id": "cmk6ag56y00053b6rj9481z6m", + "judul": "Bicara Darmasaba Bahas ‘Sampah Kita, Tanggung Jawab Siapa?’", + "deskripsi": "

Diskusi terbuka antara pemerintah desa dan warga mengenai isu sampah.

", + "content": "

Acara ini mendukung perumusan kebijakan desa yang sesuai kebutuhan warga dan meningkatkan partisipasi dalam pembangunan lingkungan.

", + "kategoriBeritaId": "cmk69tghx000tvnv8g2d206wv", + "imageName": "c_5xOKUbMiD8dTAbkAv9a-mobile.webp" + }, + { + "id": "cmk6agzxx00073b6rr3vhxcsj", + "judul": "Penutupan KKN-PMM Periode II Universitas Warmadewa di Desa Darmasaba", + "deskripsi": "

Penutupan program KKN-PMM yang berjalan dengan berbagai kegiatan pemberdayaan masyarakat.

", + "content": "

Kegiatan KKN meliputi edukasi kesehatan, pengelolaan sampah, literasi keuangan, dan upaya ekonomi lokal sebagai bagian dari pembangunan desa berkelanjutan.

", + "kategoriBeritaId": "cmk69tghx000tvnv8g2d206wv", + "imageName": "I9CDBqdeDXRbbzbPWhy6h-mobile.webp" + }, + { + "id": "cmk6agzxx00073b6rr3vhxasj", + "judul": "Desa Darmasaba Siap Kelola Sampah Mandiri, Anggarkan Rp1,5 Miliar", + "deskripsi": "

Desa Darmasaba mengalokasikan anggaran untuk pengelolaan sampah berbasis komunitas.

", + "content": "

Pengelolaan sampah mandiri melalui TPS3R, kader penyuluh, dan inovasi CINtA menjadi strategi desa dalam penanganan sampah sesuai kebijakan provinsi dan desa.

", + "kategoriBeritaId": "cmk69tghx000tvnv8g2d206wv", + "imageName": "Gq-gEDaGNb-FkKasVs7i4-mobile.webp" + }, + { + "id": "cmk6agzxx00073b6rr3vhxbsj", + "judul": "Sekda Adi Arnawa Buka Darmasaba Village Festival II", + "deskripsi": "

Pembukaan festival desa untuk mendorong UMKM dan ekonomi lokal.

", + "content": "

Kegiatan ini menampilkan berbagai UMKM yang membantu meningkatkan pendapatan masyarakat setempat.

", + "kategoriBeritaId": "cmk69tghx000uvnv847ppcxqh", + "imageName": "TymQ5xDH7vEUOA9BY63hr-mobile.webp" + }, + { + "id": "cmk6agzxx00073b6rr3vhxdsj", + "judul": "Membangun Desa Berkelanjutan Melalui Ekowisata dan Kuliner di Darmasaba", + "deskripsi": "

Program inovatif untuk memperkuat ekonomi lokal melalui ekowisata dan kuliner.

", + "content": "

Kegiatan mencakup pembangunan green house, edukasi pemasaran digital, literasi bahasa Inggris, dan pengembangan potensi kuliner desa.

", + "kategoriBeritaId": "cmk69tghx000uvnv847ppcxqh", + "imageName": "2nUvEBsMuigIJQWZIdcEJ-mobile.webp" + }, + { + "id": "cmk6agzxx00073b6rr3vhxesj", + "judul": "Inovasi Desa Darmasaba Lanjutkan Perjuangan ke Tingkat Nasional", + "deskripsi": "

Desa Darmasaba meraih penghargaan juara dalam evaluasi perkembangan desa untuk mendukung perekonomian dan pemerintahan lokal.

", + "content": "

Prestasi desa ditandai dengan keberhasilan dalam lomba evaluasi perkembangan desa di tingkat provinsi dan kabupaten, yang berdampak positif pada ekonomi desa.

", + "kategoriBeritaId": "cmk69tghx000uvnv847ppcxqh", + "imageName": "Hokgum3-nI_NWTWJRnWi3-mobile.webp" + }, + { + "id": "cmk6agzxx00073b6rr3vhxfsj", + "judul": "Desa Darmasaba Kembali Ukir Prestasi Internasional BAJRA", + "deskripsi": "

Prestasi desa dalam forum internasional mengenai penanggulangan rabies.

", + "content": "

Partisipasi dalam konferensi Rabies in Borneo menunjukkan kolaborasi lintas sektor dan penggunaan data dalam pelayanan publik desa.

", + "kategoriBeritaId": "cmk69tght000svnv8ok5rid2v", + "imageName": "T1fcksUoZSUqNMbzvr9WI-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp1z", + "judul": "Cegah Penyebaran Rabies, Darmasaba Keluarkan Larangan Membuang Hewan", + "deskripsi": "

Pemasangan spanduk larangan buang hewan sebagai langkah proteksi kesehatan masyarakat.

", + "content": "

Pemerintah desa bersama Tim Bajra aktif dalam kampanye dan regulasi untuk mencegah rabies di tingkat desa.

", + "kategoriBeritaId": "cmk69tght000svnv8ok5rid2v", + "imageName": "-M_tICRVz6ZxOfvkuHQgU-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp2z", + "judul": "TP. Posyandu Bali dan Pemerintah Desa Kolaborasi Tingkatkan Pelayanan", + "deskripsi": "

Kolaborasi pemerintahan desa dengan TP Posyandu untuk meningkatkan layanan masyarakat.

", + "content": "

Kegiatan ini menunjukkan peran pemerintahan desa dalam mendukung layanan kesehatan dan pemberdayaan masyarakat.

", + "kategoriBeritaId": "cmk69tght000svnv8ok5rid2v", + "imageName": "O11hmN9oNwFKs_ACpH9uV-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp3z", + "judul": "Membangun Desa Berkelanjutan Melalui Inovasi Pengelolaan Sampah", + "deskripsi": "

Inisiatif pengelolaan sampah desa sebagai bagian dari inovasi teknologi lokal.

", + "content": "

Penerapan metode pengelolaan sampah dan biopori menunjukkan upaya Desa Darmasaba dalam menggunakan solusi teknologi sederhana untuk masalah lingkungan.

", + "kategoriBeritaId": "cmk69tghz000xvnv8kxzzt24h", + "imageName": "rrgHHUYHDuq3jW94HCRsq-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp4z", + "judul": "Inovasi BAJRA Integrasikan Pelaporan Cepat Berbasis Data", + "deskripsi": "

Program BAJRA menerapkan mekanisme pelaporan cepat berbasis data untuk penanggulangan rabies.

", + "content": "

Penggunaan teknologi informasi dalam pelaporan kasus rabies membantu respons cepat pemerintahan desa dan komunitas.

", + "kategoriBeritaId": "cmk69tghz000xvnv8kxzzt24h", + "imageName": "igz0V0MCoLYqAgLIBRZdG-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp5z", + "judul": "Digitalisasi Desa Darmasaba", + "deskripsi": "

Digitalisasi Desa Darmasaba Bersama PT. Bali Interaktif Perkasa.

", + "content": "

Digitalisasi Desa Darmasaba Bersama PT. Bali Interaktif Perkasa

Dalam rangka mendukung transformasi digital dan inovasi desa, Desa Darmasaba bekerja sama dengan PT. Bali Interaktif Perkasa melaksanakan kegiatan Digitalisasi Desa.

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.

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.

Dengan kolaborasi ini, Desa Darmasaba menegaskan tekadnya untuk terus berinovasi, menghadirkan kemudahan bagi masyarakat, dan memperkuat tata kelola desa berbasis teknologi modern.

? Digitalisasi hari ini, kemajuan desa esok!

#DesaDarmasaba #DigitalisasiDesa #DesaCerdas #InovasiDesa #TransformasiDigital
#PemdesDarmasaba
#PerbekelDarmasaba
#DesaDarmasaba
#KitaDarmasaba
#DarmasabaBisa

@kostergubernurbali
@giri.prasta
@iwayanadiarnawa
@gus.bota
@puturasniathiadiarnawa
@yunita_oktarini
@surya.suamba
@budhi.argawakba
@pemkabbadung
@ppidbadung
@dinaspmddukcapilprovbali
@surya_prabhawa
@kecamatanabiansemal
@dpmdbadungkab
@pemprov_bali
@prokompimbadung
@seputar_darmasaba

", + "kategoriBeritaId": "cmk69tghz000xvnv8kxzzt24h", + "imageName": "DzVIfpiAP3OcCsZ_VJ02b-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp6z", + "judul": "Festival Desa Tingkatkan Kreativitas Digital UMKM", + "deskripsi": "

Festival Darmasaba Village Festival II melibatkan promosi digital produk UMKM.

", + "content": "

Promosi dan dokumentasi digital menjadi bagian dari strategi pemasaran UMKM dalam festival desa.

", + "kategoriBeritaId": "cmk69tghy000wvnv8umg2vloa", + "imageName": "xzM77A6bDW2silyp_8W7n-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp7z", + "judul": "Sekda Adi Arnawa dan Pementasan Seni Tradisional di Festival Darmasaba", + "deskripsi": "

Pementasan seni tradisional menjadi bagian dari Darmasaba Village Festival II.

", + "content": "

Kegiatan ini mengangkat warisan budaya lokal melalui pertunjukan dan lomba di festival desa.

", + "kategoriBeritaId": "cmk69tghy000wvnv8umg2vloa", + "imageName": "2wivBEDcVNxHGG8HUBsNH-mobile.webp" + }, + { + "id": "cmk6aih8f00093b6rqw63yp8z", + "judul": "Dialog Publik Tingkatkan Partisipasi Budaya Lokal", + "deskripsi": "

Forum dialog desa mengangkat tema partisipasi masyarakat dalam kegiatan budaya lokal.

", + "content": "

Diskusi ini memperkuat peran budaya dalam pembangunan desa melalui keterlibatan warga dalam kegiatan adat dan sosial.

", + "kategoriBeritaId": "cmk69tghy000wvnv8umg2vloa", + "imageName": "T1fcksUoZSUqNMbzvr9WI-mobile.webp" + } +] diff --git a/prisma/data/desa/berita/kategori-berita.json b/prisma/data/desa/berita/kategori-berita.json index 4d777965..e301a584 100644 --- a/prisma/data/desa/berita/kategori-berita.json +++ b/prisma/data/desa/berita/kategori-berita.json @@ -1,8 +1,8 @@ [ - { "name": "Pemerintahan" }, - { "name": "Pembangunan" }, - { "name": "Ekonomi" }, - { "name": "Sosial" }, - { "name": "Budaya" }, - { "name": "Teknologi" } + { "id": "cmk69tght000svnv8ok5rid2v", "name": "Pemerintahan" }, + { "id": "cmk69tghx000tvnv8g2d206wv", "name": "Pembangunan" }, + { "id": "cmk69tghx000uvnv847ppcxqh", "name": "Ekonomi" }, + { "id": "cmk69tghy000vvnv8xeouenv5", "name": "Sosial" }, + { "id": "cmk69tghy000wvnv8umg2vloa", "name": "Budaya" }, + { "id": "cmk69tghz000xvnv8kxzzt24h", "name": "Teknologi" } ] diff --git a/prisma/data/desa/gallery/foto/foto.json b/prisma/data/desa/gallery/foto/foto.json new file mode 100644 index 00000000..f1141c30 --- /dev/null +++ b/prisma/data/desa/gallery/foto/foto.json @@ -0,0 +1,20 @@ +[ + { + "id": "cml0aiiv1000004l754ldaf2v", + "name": "Kunjungan Ibu TP PKK Kabupaten Badung", + "deskripsi": "

Dokumentasi kunjungan Ibu TP PKK Kabupaten Badung ke Desa Darmasaba pada awal tahun 2026.

", + "imageName": "foto1Gallery.webp" + }, + { + "id": "cml0aiqd4000104l7f91ee3xu", + "name": "Darmasaba Village Festival II 2024", + "deskripsi": "

Foto kegiatan Darmasaba Village Festival II Tahun 2024 yang diselenggarakan di Lapangan Umum Desa Darmasaba, menampilkan lomba, pementasan seni, dan UMKM lokal.

", + "imageName": "foto2Gallery.webp" + }, + { + "id": "cml0aiyi7000204l7f2sy657c", + "name": "Lomba Mancing Air Deras Banjar Gulingan", + "deskripsi": "

Galeri foto kegiatan lomba mancing air deras Komunitas Pemancing Gulingan di Desa Darmasaba, bagian dari pemberdayaan masyarakat.

", + "imageName": "foto3Gallery.webp" + } +] diff --git a/prisma/data/desa/gallery/video/video.json b/prisma/data/desa/gallery/video/video.json new file mode 100644 index 00000000..e3ae007f --- /dev/null +++ b/prisma/data/desa/gallery/video/video.json @@ -0,0 +1,20 @@ +[ + { + "id": "cmk6kvn6b0000vn6qzg5z6qa6", + "judul": "TAHAP PENILAIAN VERIFIKASI LAPANGAN AJANG MANGUPURA AWARD TAHUN 2025", + "deskripsi": "

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

", + "linkVideo": "https://www.youtube.com/watch?v=e2tSRnNkYDE" + }, + { + "id": "cmk6kvn6b0000vn6qzg5z6qb7", + "judul": "Vaksinasi Rabies di Desa Darmasaba", + "deskripsi": "

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

", + "linkVideo": "https://www.youtube.com/watch?v=bc99y94FBx8" + }, + { + "id": "cmk6kvn6b0000vn6qzg5z6qc8", + "judul": "Musyawarah Perencanaan Pembangunan Desa Darmasaba Tahun 2026", + "deskripsi":"

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

", + "linkVideo": "https://www.youtube.com/watch?v=7pirwEmyP-4" + } +] diff --git a/prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json b/prisma/data/desa/layanan/pelayananPendudukNonPermanen.json similarity index 100% rename from prisma/data/desa/layanan/pelayanaPendudukNonPermanen.json rename to prisma/data/desa/layanan/pelayananPendudukNonPermanen.json diff --git a/prisma/data/desa/layanan/pelayananSuratKeterangan.json b/prisma/data/desa/layanan/pelayananSuratKeterangan.json index 9f13159c..87ea9f17 100644 --- a/prisma/data/desa/layanan/pelayananSuratKeterangan.json +++ b/prisma/data/desa/layanan/pelayananSuratKeterangan.json @@ -1,57 +1,79 @@ [ - { - "id" : "cmdxyb9zi0010vniiaeyi55ui", - "name" : "Surat Keterangan Beda Biodata Diri", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxycqz40014vniidftrixvf", - "name" : "Surat Keterangan Yatim Piatu", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdwx3wph0003vnr74us2t7h7", - "name" : "Surat Keterangan Domisili Organisasi", - "deskripsi" : "

Persyaratan Dokumen:

Alur Pelayanan:

" - }, - { - "id" : "cmdxxv3i80004vniidg1mrucc", - "name" : "Surat Keterangan Penghasilan", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxxwp070008vnii9jbdcto7", - "name" : "Surat Keterangan Tidak Mampu", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxxyfkl000cvnii1bxinnfi", - "name" : "Surat Keterangan Kelahiran", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxy23pl000gvniihsg38aq4", - "name" : "Surat Keterangan Usaha", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxy4mgt000kvniib1nemjem", - "name" : "Surat Keterangan Kematian", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxy61a1000ovniif4ytb9hs", - "name" : "Surat Keterangan Tempat Usaha", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxy754q000svniiiz8oqyo0", - "name" : "Surat Keterangan Belum Kawin", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - }, - { - "id" : "cmdxy8pi2000wvnii48fc1sxd", - "name" : "Surat Keterangan Kelakuan Baik", - "deskripsi" : "

Persyaratan Dokumen :

Alur Pelayanan :

" - } -] \ No newline at end of file + { + "id": "cmdxyb9zi0010vniiaeyi55ui", + "name": "Surat Keterangan Beda Biodata Diri", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "wFXYVHKHtU7posfhBKjZt-mobile.webp", + "image2Name": "kTiZ9uhRUEwu0WeASU00b-mobile.webp" + }, + { + "id": "cmdxycqz40014vniidftrixvf", + "name": "Surat Keterangan Yatim Piatu", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "ZkvjJ7Zx8uBy2-d8RWNEt-mobile.webp", + "image2Name": "hHfOWKz4USZK-z0nsD7Uz-mobile.webp" + }, + { + "id": "cmdwx3wph0003vnr74us2t7h7", + "name": "Surat Keterangan Domisili Organisasi", + "deskripsi": "

Persyaratan Dokumen:

Alur Pelayanan:

", + "imageName": "03WZn5JMKffo62cKzShF4-mobile.webp", + "image2Name": "dnTcxeYQACzY5yzq-Rjoz-mobile.webp" + }, + { + "id": "cmdxxv3i80004vniidg1mrucc", + "name": "Surat Keterangan Penghasilan", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "7er4OinxhuKRZUSkKeSYR-mobile.webp", + "image2Name": "8ZtJeCnKwMAcEUBU0QvE7-mobile.webp" + }, + { + "id": "cmdxxwp070008vnii9jbdcto7", + "name": "Surat Keterangan Tidak Mampu", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "Y7IuwpwjT3ZFYWpiXJZYw-mobile.webp", + "image2Name": "Fbi-6gnhokkwux9hvriS--mobile.webp" + }, + { + "id": "cmdxxyfkl000cvnii1bxinnfi", + "name": "Surat Keterangan Kelahiran", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "qtdJ39rIbjGTJJQZflrDL-mobile.webp", + "image2Name": "ldmfJBS2ZBhIda60yU2JW-mobile.webp" + }, + { + "id": "cmdxy23pl000gvniihsg38aq4", + "name": "Surat Keterangan Usaha", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "CAtRFHiM11gsY4ExcvGgc-mobile.webp", + "image2Name": "MLWp-38kKygXKVekCh0q7-mobile.webp" + }, + { + "id": "cmdxy4mgt000kvniib1nemjem", + "name": "Surat Keterangan Kematian", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "C0zE5lneKa888VJDHzgh--mobile.webp", + "image2Name": "hhDK7OL0wQLik5dsh1a-L-mobile.webp" + }, + { + "id": "cmdxy61a1000ovniif4ytb9hs", + "name": "Surat Keterangan Tempat Usaha", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "sVIfCUGBplNU8h1x99l8z-mobile.webp", + "image2Name": "MM-1CZMai3eXwbVr2HAEv-mobile.webp" + }, + { + "id": "cmdxy754q000svniiiz8oqyo0", + "name": "Surat Keterangan Belum Kawin", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "oNH9VvRPlAmTI_Ndu3slN-mobile.webp", + "image2Name": "Wze2x68vn-ShZAqgkSCMC-mobile.webp" + }, + { + "id": "cmdxy8pi2000wvnii48fc1sxd", + "name": "Surat Keterangan Kelakuan Baik", + "deskripsi": "

Persyaratan Dokumen :

Alur Pelayanan :

", + "imageName": "tK9T6BObEQEdYU-5y1xnJ-mobile.webp", + "image2Name": "sX9yfcM05OOx2ELhcSNWl-mobile.webp" + } +] diff --git a/prisma/data/desa/penghargaan/penghargaan.json b/prisma/data/desa/penghargaan/penghargaan.json new file mode 100644 index 00000000..6b073a23 --- /dev/null +++ b/prisma/data/desa/penghargaan/penghargaan.json @@ -0,0 +1,30 @@ +[ + { + "id": "cmkal9dd70002vnexhvn4j2fv", + "name": "Perbekel Darmasaba Terima Penghargaan pada Indonesia Alternative Dispute Resolution Awards 2025", + "juara": "Penghargaan IADRA 2025", + "deskripsi": "

Perbekel Darmasaba, Ida Bagus Surya Prabhawa Manuaba menerima penghargaan pada Indonesia Alternative Dispute Resolution Awards 2025 atas kontribusi dalam penyelesaian sengketa secara damai dan berkeadilan.

", + "imageName": "cte_gy0V8glhDcghMa3-B-mobile.webp" + }, + { + "id": "cmkalfbux0005vnexj8zplube", + "name": "Penghargaan Bhawana Sewaka Nugraha kepada Perbekel Darmasaba", + "juara": "Penghargaan Bhawana Sewaka Nugraha", + "deskripsi": "

Penghargaan Bhawana Sewaka Nugraha diberikan kepada Perbekel Desa Darmasaba atas dedikasi dan komitmen dalam menjaga kelestarian lingkungan serta menginspirasi masyarakat untuk menciptakan desa yang hijau, bersih, dan berkelanjutan.

", + "imageName": "hLoy_DjB_3Yie2vjaXDYz-mobile.webp" + }, + { + "id": "cmkalg6hk0008vnexmsqwd3n7", + "name": "Desa Darmasaba Raih Juara 1 Lomba Desa Tingkat Provinsi Bali Tahun 2025", + "juara": "Juara 1 Lomba Desa Provinsi Bali 2025", + "deskripsi": "

Desa Darmasaba meraih juara 1 dalam Lomba Desa Tingkat Provinsi Bali Tahun 2025 melalui sinergi elemen desa serta inovasi dalam pelayanan publik dan pemberdayaan masyarakat.

", + "imageName": "VxELyZ0ifdHOdRFu7aBSz-mobile.webp" + }, + { + "id": "cmkalgucu000bvnexw2blt1v3", + "name": "Pemerintah Desa Darmasaba Raih Peringkat IV dalam Ajang Mangupura Award Tahun 2025", + "juara": "Peringkat IV Mangupura Award 2025", + "deskripsi": "

Desa Darmasaba mendapatkan penghargaan Peringkat IV pada Mangupura Award 2025 dari Pemerintah Kabupaten Badung atas tata kelola pemerintahan, inovasi layanan, dan pemberdayaan masyarakat.

", + "imageName": "xgrBxDdo8g7KA-3zNbRGj-mobile.webp" + } +] diff --git a/prisma/data/desa/pengumuman/kategori-pengumuman.json b/prisma/data/desa/pengumuman/kategori-pengumuman.json new file mode 100644 index 00000000..dd97e2b8 --- /dev/null +++ b/prisma/data/desa/pengumuman/kategori-pengumuman.json @@ -0,0 +1,8 @@ +[ + { "id": "cmk69tghx111tvnv8g2d206wv", "name": "Digitalisasi Desa" }, + { "id": "cmk69tghx111uvnv847ppcxqh", "name": "Adat & Budaya" }, + { "id": "cmk69tght111svnv8ok5rid2v", "name": "Lingkungan & Bencana" }, + { "id": "cmk69tghy111wvnv8umg2vloa", "name": "Pendidikan & Kepemudaan" }, + { "id": "cmk69tghy111vvnv8xeouenv5", "name": "Ekonomi & UMKM" }, + { "id": "cmk69tghz111xvnv8kxzzt24h", "name": "Sosial & Kesehatan" } +] diff --git a/prisma/data/desa/pengumuman/pengumuman.json b/prisma/data/desa/pengumuman/pengumuman.json new file mode 100644 index 00000000..0ac49936 --- /dev/null +++ b/prisma/data/desa/pengumuman/pengumuman.json @@ -0,0 +1,37 @@ +[ + { + "id": "cmk6ae8rz00003b6r06x7hsqz", + "judul": "Pengumuman Lelang Pengadaan Pick UpDump TPS3R Pudak Mesari", + "deskripsi": "

Pengumuman lelang pengadaan alat Pick UpDump untuk TPS3R Pudak Mesari Desa Darmasaba.

", + "content": "

Desa Darmasaba membuka lelang pengadaan Pick UpDump untuk mendukung kegiatan pengelolaan sampah di TPS3R Pudak Mesari.

", + "categoryPengumumanId": "cmk69tghy111vvnv8xeouenv5" + }, + { + "id": "cmk6af7vf00013b6rj2br4nv7", + "judul": "Pengumuman Lelang Pembangunan Taman BR. Darmasaba", + "deskripsi": "

Pengumuman lelang pembangunan taman di Banjar BR. Darmasaba.

", + "content": "

Desa Darmasaba mengumumkan pelaksanaan lelang pembangunan taman sebagai bagian dari upaya memperindah ruang publik desa.

", + "categoryPengumumanId": "cmk69tghy111vvnv8xeouenv5" + }, + { + "id": "cmk6afo0g00033b6rjc2pae69", + "judul": "Pengumuman Lelang Penataan Lapangan Desa Darmasaba Tahun Anggaran 2024", + "deskripsi": "

Pengumuman lelang penataan lapangan Desa Darmasaba untuk tahun anggaran 2024.

", + "content": "

Lelang penataan lapangan desa dilaksanakan untuk menunjang kegiatan desa dan fasilitas masyarakat di Darmasaba.

", + "categoryPengumumanId": "cmk69tghy111vvnv8xeouenv5" + }, + { + "id": "cmk6ag56y00053b6rj9481z5l", + "judul": "Pengumuman Lelang Penataan Landscape Lapangan Desa", + "deskripsi": "

Pengumuman lelang penataan landscape lapangan Desa Darmasaba.

", + "content": "

Desa Darmasaba menyelenggarakan lelang untuk kegiatan penataan landscape lapangan desa dalam rangka peningkatan fasilitas umum.

", + "categoryPengumumanId": "cmk69tghx111tvnv8g2d206wv" + }, + { + "id": "cmk6agzxx00073b6rr3vhxcti", + "judul": "Penutupan KKN-PMM Periode II Universitas Warmadewa di Desa Darmasaba", + "deskripsi": "

Penutupan program KKN-PMM yang berjalan dengan berbagai kegiatan pemberdayaan masyarakat.

", + "content": "

Kegiatan KKN meliputi edukasi kesehatan, pengelolaan sampah, literasi keuangan, dan upaya ekonomi lokal sebagai bagian dari pembangunan desa berkelanjutan.

", + "categoryPengumumanId": "cmk69tghx111tvnv8g2d206wv" + } +] diff --git a/prisma/data/desa/potensi/kategori-potensi.json b/prisma/data/desa/potensi/kategori-potensi.json new file mode 100644 index 00000000..e8c2d753 --- /dev/null +++ b/prisma/data/desa/potensi/kategori-potensi.json @@ -0,0 +1,14 @@ +[ + { + "id": "cmk3pmwq10008vn9bqdquv153", + "nama": "Wisata" + }, + { + "id": "cmk3s1ks6000ivn9bcrv960ko", + "nama": "Ekonomi" + }, + { + "id": "cmk3s1r0m000jvn9b8tlhogwn", + "nama": "Lingkungan" + } +] \ No newline at end of file diff --git a/prisma/data/desa/potensi/potensi-desa.json b/prisma/data/desa/potensi/potensi-desa.json index 9c6c8b66..43b8dfbf 100644 --- a/prisma/data/desa/potensi/potensi-desa.json +++ b/prisma/data/desa/potensi/potensi-desa.json @@ -1,74 +1,90 @@ [ { - "id": "cmdyamai40004vnw3sdjbvn48", - "name": "TPS3R Pudak Mesari", - "deskripsi": "TPS 3R Pudak Mesari Darmasaba layak mendapat penghargaan demikian apresiasi dari Delterra Sosial Indonesia nie Semeton Darmasaba!, Hal tersebut dikarenakan walaupun baru berdiri namun TPS 3R kebanggaan Desa Darmasaba tersebut sudah berjalan dengan sangat baik.", - "content": "

TPS3R Pudak Mesari adalah Tempat Pengolahan Sampah dengan konsep Reduce, Reuse, dan Recycle (TPS3R) yang berlokasi di Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali. Fasilitas ini berperan penting dalam pengelolaan sampah berbasis masyarakat, dengan tujuan mengurangi volume sampah yang masuk ke Tempat Pembuangan Akhir (TPA) dan meningkatkan kesadaran warga tentang pentingnya pengelolaan sampah yang berkelanjutan.

Potensi Desa melalui TPS3R Pudak Mesari:

  1. Peningkatan Kesehatan Lingkungan:

    Dengan pengelolaan sampah yang efektif, desa dapat menjaga kebersihan lingkungan, mengurangi risiko penyakit, dan menciptakan suasana yang lebih nyaman bagi warga.

  2. Pemberdayaan Ekonomi Masyarakat:

    TPS3R membuka peluang usaha bagi warga melalui pemilahan dan pengolahan sampah, seperti produksi kompos dari sampah organik dan kerajinan tangan dari sampah anorganik yang dapat meningkatkan pendapatan masyarakat.

  3. Edukasi dan Kesadaran Lingkungan:

    Fasilitas ini dapat menjadi pusat edukasi bagi masyarakat tentang pentingnya pengelolaan sampah, mendorong partisipasi aktif dalam menjaga kelestarian lingkungan.

  4. Pengembangan Pariwisata Berkelanjutan:

    Dengan lingkungan yang bersih dan asri, Desa Darmasaba memiliki potensi untuk menarik wisatawan yang tertarik pada ekowisata dan budaya lokal, sehingga meningkatkan perekonomian desa.

" - }, - { - "id": "cmdyb7h440003vngjapbc84f7", - "name": "Bumdes Pudak Mesari", - "deskripsi": "Bumdes Pudak Mesari sangat membantu warga desa Darmasaba dalam mengelola dan membangun sebuah desa yang lebih baik lagi", - "content": "

Badan Usaha Milik Desa (BUMDes) Pudak Mesari adalah lembaga ekonomi desa yang berperan penting dalam pengembangan potensi dan kesejahteraan masyarakat Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali. BUMDes ini berfungsi sebagai motor penggerak perekonomian desa melalui berbagai unit usaha yang dikelola secara profesional.

Potensi dan Peran BUMDes Pudak Mesari:

  1. Pengembangan Usaha Mikro dan Kecil:

    BUMDes Pudak Mesari menyediakan layanan bagi pelaku usaha mikro dan kecil di desa, seperti penyediaan konsumsi dan snack kotak untuk berbagai acara.

  2. Pengelolaan Sampah Berbasis Masyarakat:

    Melalui kolaborasi dengan komunitas pemuda peduli lingkungan, BUMDes Pudak Mesari aktif dalam pengelolaan sampah berbasis masyarakat.

  3. Peningkatan Kapasitas dan Transparansi:

    Untuk memastikan pengelolaan yang akuntabel, BUMDes Pudak Mesari rutin mengadakan rapat koordinasi dan pendampingan penyusunan laporan pertanggungjawaban.

  4. Kolaborasi Internasional:

    Desa Darmasaba, melalui BUMDes Pudak Mesari, menerima kunjungan dari tim Osaki Jepang untuk memperkuat pengelolaan sampah dan lingkungan.

Dengan berbagai inisiatif tersebut, BUMDes Pudak Mesari menunjukkan perannya sebagai pilar utama dalam pengembangan ekonomi dan kesejahteraan masyarakat Desa Darmasaba, sekaligus menjaga kelestarian lingkungan melalui program-program inovatif dan kolaboratif.

" - }, - { - "id": "cmdybb53i0007vngjet38spn8", + "id": "cmk3rbc7s000evn9bvb8p0xk1", "name": "Taman Beji Cengana", - "deskripsi": "Tirta Klebutan di Pura Taman Beji Cengana di Desa Adat Darmasaba, Badung, selain dipercaya nunas Taksu serta pembersihan diri. Tersemat juga asal usul cerita ditemukannya Tirta Klebutan yang tepat berada di pinggir Tukad Cengana tersebut.", - "content": "

Taman Beji Cengana, terletak di Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali, adalah situs suci yang memiliki nilai spiritual dan sejarah yang tinggi. Tempat ini dikenal sebagai lokasi untuk ritual pembersihan diri (melukat) dan peribadatan oleh umat Hindu Bali. Keberadaan mata air suci (Tirta Klebutan) di Taman Beji Cengana dipercaya memberikan berkah dan penyucian bagi mereka yang datang untuk berdoa dan melakukan ritual.

Potensi Desa melalui Taman Beji Cengana:

  1. Pengembangan Pariwisata Spiritual:

    Taman Beji Cengana memiliki potensi besar sebagai destinasi wisata spiritual. Wisatawan yang mencari pengalaman spiritual dan ketenangan batin dapat tertarik untuk mengunjungi tempat ini, mengikuti ritual melukat, dan merasakan suasana sakral yang ditawarkan.

  2. Pelestarian Budaya dan Tradisi:

    Dengan mempromosikan Taman Beji Cengana sebagai pusat kegiatan budaya dan ritual tradisional, desa dapat memastikan bahwa warisan budaya dan tradisi lokal tetap lestari.

  3. Pendidikan dan Penelitian:

    Taman Beji Cengana dapat dijadikan sebagai pusat pendidikan dan penelitian bagi akademisi, peneliti, dan pelajar yang tertarik mempelajari budaya, agama, dan sejarah Bali.

  4. Pengembangan Ekonomi Kreatif:

    Dengan meningkatnya jumlah pengunjung ke Taman Beji Cengana, peluang bagi pengembangan ekonomi kreatif juga terbuka lebar. Masyarakat lokal dapat mengembangkan produk kerajinan tangan, kuliner khas, dan suvenir yang mencerminkan budaya dan tradisi desa.

  5. Konservasi Lingkungan:

    Sebagai situs suci dengan mata air alami, Taman Beji Cengana memiliki peran penting dalam konservasi lingkungan. Upaya menjaga kebersihan dan kelestarian mata air serta lingkungan sekitarnya dapat menjadi contoh praktik konservasi yang baik.

Dengan memanfaatkan potensi yang dimiliki Taman Beji Cengana, Desa Darmasaba dapat mengembangkan sektor pariwisata, budaya, pendidikan, ekonomi, dan lingkungan secara berkelanjutan, yang pada gilirannya akan meningkatkan kesejahteraan masyarakat dan pelestarian warisan budaya.

" + "deskripsi": "

Taman Beji Cengana adalah situs keagamaan dan budaya tradisional Bali

", + "content": "

Taman Beji Cengana adalah situs keagamaan dan budaya tradisional Bali yang memadukan unsur spiritual, ritual pembersihan (melukat), serta nilai budaya lokal yang berpadu dengan suasana alam yang tenang dan harmonis.

", + "kategoriId": "cmk3pmwq10008vn9bqdquv153", + "imageName": "yySqpA7ougpXi6cJPskg9-mobile.webp" }, { - "id": "cmdybckcz000avngjfzpy60uk", + "id": "cmk3q0sez000bvn9b3iyyct5m", "name": "Gumuh Sari Water Park", - "deskripsi": "Gumuh Sari Rekreasi atau waterpark, tempat wisata yang asyik dan seru untuk kamu sekeluarga! Tempat liburan di Bali memang seakan nggak ada habisnya. Selalu ada aja destinasi wisata seru yang bisa jadi wishlist. Ada banyak banget tempat wisata yang kamu kunjungi di Bali, mulai dari wisata alam, wisata modern, sampai wisata air.", - "content": "

Gumuh Sari Waterpark, terletak di Jl. Tegal Gumuh No. 9, Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali, adalah destinasi rekreasi yang menawarkan berbagai fasilitas untuk pengunjung dari segala usia. Taman rekreasi ini tidak hanya menyediakan wahana air yang menyenangkan, tetapi juga fasilitas olahraga dan kuliner, menjadikannya tempat ideal untuk rekreasi keluarga dan komunitas.

Potensi Desa melalui Gumuh Sari Waterpark:

  1. Pengembangan Pariwisata Lokal:

    Dengan adanya destinasi seperti Gumuh Sari Waterpark, Desa Darmasaba dapat menarik lebih banyak wisatawan lokal maupun mancanegara. Kehadiran pengunjung ini berpotensi meningkatkan pendapatan desa dan membuka peluang usaha baru bagi masyarakat setempat.

  2. Peningkatan Ekonomi Masyarakat:

    Fasilitas seperti restoran dan pusat olahraga di dalam kompleks waterpark memberikan peluang bagi warga lokal untuk terlibat dalam sektor jasa dan perdagangan. Hal ini dapat menciptakan lapangan pekerjaan dan mendukung pertumbuhan ekonomi desa.

  3. Pengembangan Fasilitas Olahraga dan Kesehatan:

    Dengan adanya pusat futsal dan gym, Gumuh Sari Waterpark mendorong masyarakat untuk berpartisipasi dalam kegiatan olahraga, yang dapat meningkatkan kesehatan dan kesejahteraan warga.

  4. Pemberdayaan Komunitas Melalui Event dan Acara:

    Waterpark ini sering menjadi tuan rumah berbagai acara komunitas, seperti pesta busa dan bola, yang dapat mempererat hubungan antarwarga dan menciptakan lingkungan yang harmonis.

  5. Peningkatan Infrastruktur dan Aksesibilitas:

    Dengan meningkatnya jumlah pengunjung, infrastruktur desa seperti jalan, transportasi, dan layanan umum lainnya akan berkembang untuk memenuhi kebutuhan tersebut, yang pada gilirannya meningkatkan kualitas hidup masyarakat setempat.

Melalui pengelolaan dan pengembangan yang tepat, Gumuh Sari Waterpark dapat menjadi motor penggerak bagi kemajuan Desa Darmasaba, meningkatkan kesejahteraan masyarakat, dan menjadikan desa ini sebagai destinasi wisata yang dikenal luas.

" + "deskripsi": "

Gumuh Sari merupakan tempat rekreasi nyaman di kawasan badung yang dapat dinikmati oleh semua umur dengan berbagai fasilitas hiburan, seperti Water Park, Pusat Kebugaran, Lapangan Futsal dan Restaurant.

", + "content": "

Gumuh Sari merupakan tempat rekreasi nyaman di kawasan badung yang dapat dinikmati oleh semua umur dengan berbagai fasilitas hiburan, seperti Water Park, Pusat Kebugaran, Lapangan Futsal dan Restaurant.

Buka setiap hari
Senin-Jumat: pukul 10.00-18.00 WITA (Weekday)
Sabtu-Minggu: pukul 09.00-18.00 WITA (Weekend)

Wahana:
- Kolam Renang Anak
- Kolam Renang Dewasa
- Kolam Renang Olympic
- Kids Playground Indoor
- Ayunan
- ATV

Fasilitas:
- Tempat ibadah
- Area parkir
- Gazebo
- Restoran dan kantin
- WiFi gratis
- Kamar mandi dan ruang bilas
- Ruang loker

Kategori Tiket:
Senin-Sabtu
- Dewasa 35.000
- Anak-anak (3-12 tahun) 20.000

Minggu & Libur Nasional
- Dewasa 40.000
- Anak-anak (3-12 tahun) 25.000

Hari Raya
- Dewasa 45.000
- Anak-anak (3-12 tahun) 35.000

Media Sosial:
- Instagram: gumuhsari_rekreasi
https://www.instagram.com/gumuhsari_rekreasi/

- Tiktok: gumuhsari_rekreasi
https://www.tiktok.com/@gumuhsari_rekreasi

", + "kategoriId": "cmk3pmwq10008vn9bqdquv153", + "imageName": "uki9Zy0DqrEtSg9Lrgyyk-mobile.webp" }, { - "id": "cmdyjuij40002vns5qyyjmzf4", + "id": "cmk3rsch1000hvn9bhmyhsjqm", + "name": "TPS3R Pudak Mesari", + "deskripsi": "

Sistem pengelolaan sampah terpadu di Desa Darmasaba yang dikenal dengan TPS3R Pudak Mesari.

", + "content": "

TPS3R Pudak Mesari adalah fasilitas pengelolaan sampah terpadu yang mengoptimalkan pemilahan, pengolahan dan daur ulang sampah masyarakat sehingga mendukung lingkungan yang bersih dan berkelanjutan di Desa Darmasaba.

", + "kategoriId": "cmk3pmwq10008vn9bqdquv153", + "imageName": "F8SuL9DjCqLXlSwmgSJv_-mobile.webp" + }, + { + "id": "cmk3s4kz7000mvn9b8thk98c4", + "name": "Bumdes Pudak Mesari", + "deskripsi": "

Badan Usaha Milik Desa yang bergerak dalam berbagai usaha ekonomi masyarakat Darmasaba.

", + "content": "

Bumdes Pudak Mesari merupakan badan usaha milik desa yang mengembangkan kegiatan usaha produktif untuk meningkatkan perekonomian masyarakat lokal, termasuk pengelolaan usaha jasa dan komoditas lokal yang dikelola secara partisipatif.

", + "kategoriId": "cmk3s1ks6000ivn9bcrv960ko", + "imageName": "daDa40Y8dxune0l_cxULD-mobile.webp" + }, + { + "id": "cmk3sbgge000pvn9bd3r8i0on", "name": "Pertanian", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi pertanian yang besar sebagai bagian dari warisan agraris yang telah diwariskan secara turun-temurun. Dengan kondisi tanah yang subur serta sistem irigasi tradisional subak, pertanian di Darmasaba memainkan peran penting dalam ekonomi dan keberlanjutan lingkungan desa.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi pertanian yang besar sebagai bagian dari warisan agraris yang telah diwariskan secara turun-temurun. Dengan kondisi tanah yang subur serta sistem irigasi tradisional subak, pertanian di Darmasaba memainkan peran penting dalam ekonomi dan keberlanjutan lingkungan desa.

Potensi Desa melalui Pertanian:

  1. Potensi dan Komoditas Unggulan

    Pertanian di Desa Darmasaba mengandalkan berbagai jenis tanaman yang memiliki nilai ekonomi tinggi, di antaranya:

    - Padi : Sebagai salah satu desa yang masih mempertahankan sistem subak, Darmasaba menjadi bagian dari lumbung pangan di Bali.

    - Sayur-mayur : Beberapa jenis sayuran seperti kangkung, bayam, cabai, dan tomat banyak dibudidayakan oleh petani lokal.

    - Buah-buahan tropis : Termasuk pisang, mangga, dan kelapa, yang menjadi sumber pendapatan tambahan bagi petani.

    - Tanaman obat dan rempah : Seperti jahe, kunyit, dan lengkuas, yang memiliki permintaan tinggi baik untuk kebutuhan rumah tangga maupun industri herbal.

  2. Sistem Irigasi Tradisional Subak:

    Sebagai bagian dari warisan budaya Bali, sistem irigasi subak masih diterapkan di Darmasaba. Sistem ini memungkinkan distribusi air yang adil di antara lahan pertanian dan membantu menjaga keberlanjutan produksi pangan desa.

  3. Pengembangan Pertanian Organik:

    Dengan meningkatnya kesadaran akan pentingnya produk sehat dan ramah lingkungan, beberapa petani di Darmasaba mulai beralih ke metode pertanian organik. Hal ini membuka peluang bagi desa untuk mengembangkan produk-produk pertanian yang memiliki nilai jual lebih tinggi.

  4. Agrowisata sebagai Sumber Pendapatan Baru:

    Potensi pertanian Darmasaba juga dapat dikembangkan menjadi agrowisata, di mana wisatawan dapat merasakan pengalaman langsung dalam bertani, mengikuti workshop bercocok tanam, serta menikmati hasil pertanian segar. Hal ini dapat menarik wisatawan lokal maupun mancanegara, meningkatkan perekonomian desa.

  5. Pemberdayaan Petani dan UMKM Berbasis Pertanian:

    Dengan adanya BUMDes Pudak Mesari dan dukungan dari pemerintah setempat, petani di Darmasaba dapat diberikan pelatihan dan akses pasar yang lebih luas. Produk pertanian dapat diolah menjadi berbagai produk turunan seperti keripik pisang, sambal khas desa, hingga minuman herbal yang dapat dipasarkan ke luar daerah.

Dengan berbagai potensi yang dimiliki, pertanian di Desa Darmasaba dapat terus berkembang melalui inovasi dan pemanfaatan teknologi pertanian modern. Dukungan dari masyarakat, pemerintah, dan lembaga terkait sangat penting untuk menjaga keberlanjutan sektor pertanian dan meningkatkan kesejahteraan petani di desa ini.

" + "deskripsi": "

Sektor pertanian sebagai salah satu potensi utama Desa Darmasaba.

", + "content": "

Pertanian di Darmasaba mencakup berbagai komoditas yang ditanam oleh masyarakat termasuk padi, tanaman hortikultura dan lain-lain, yang menjadi basis mata pencaharian dan kontribusi terhadap ekonomi desa.

", + "kategoriId": "cmk3s1ks6000ivn9bcrv960ko", + "imageName": "yms7mL_T6fy--Eze-mTwc-mobile.webp" }, { - "id": "cmdykerrq0002vn6fy4sv7uvm", - "name": "Kawasan Kuliner", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi besar dalam sektor kuliner. Sebagai desa yang strategis dan terus berkembang, Darmasaba mulai dikenal sebagai destinasi kuliner yang menawarkan beragam makanan khas Bali hingga makanan modern yang menarik minat wisatawan dan masyarakat lokal.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, Bali, memiliki potensi besar dalam sektor kuliner. Sebagai desa yang strategis dan terus berkembang, Darmasaba mulai dikenal sebagai destinasi kuliner yang menawarkan beragam makanan khas Bali hingga makanan modern yang menarik minat wisatawan dan masyarakat lokal.

Potensi Desa melalui Kawasan Kuliner:

  1. Ragam Kuliner Khas Bali

    Darmasaba memiliki banyak warung dan rumah makan yang menyajikan hidangan khas Bali yang otentik, seperti:

    - Babi Guling : Salah satu kuliner favorit di Bali yang banyak ditemukan di Darmasaba.

    - Ayam Betutu : Hidangan ayam berbumbu khas yang dimasak dengan teknik khas Bali.

    - Lawar : Campuran daging dan sayuran berbumbu khas Bali.

    - Sate Lilit : Sate khas Bali yang terbuat dari daging cincang yang dibalut pada batang serai.

  2. Wisata Kuliner Modern & Cafe Kekinian:

    Selain makanan tradisional, Darmasaba juga mulai berkembang dengan hadirnya cafe dan resto kekinian yang menyajikan menu modern seperti kopi spesial, burger, pizza, dan aneka dessert yang digemari anak muda. Keberadaan tempat-tempat ini menjadikan Darmasaba sebagai pilihan destinasi kuliner bagi wisatawan maupun warga sekitar.

  3. Pasar Kuliner Malam:

    Salah satu daya tarik Darmasaba adalah pusat kuliner malam yang menghadirkan aneka makanan kaki lima seperti nasi jinggo, tipat cantok, bakso, dan berbagai jajanan khas Bali. Suasana yang ramai dan harga yang terjangkau membuat pasar kuliner ini menjadi tempat favorit bagi masyarakat lokal.

  4. Potensi Ekonomi & UMKM Kuliner:

    Dengan berkembangnya sektor kuliner, banyak pelaku UMKM di Darmasaba mulai merintis usaha makanan, baik dalam bentuk warung makan, katering, hingga produksi makanan ringan seperti keripik, sambal, dan minuman tradisional. Potensi ini dapat terus dikembangkan dengan dukungan pemerintah desa dan promosi melalui media sosial.

  5. Kawasan Kuliner Berbasis Pariwisata:

    Untuk menarik lebih banyak pengunjung, Darmasaba berpotensi mengembangkan kawasan kuliner berbasis wisata yang menggabungkan pengalaman makan dengan konsep alam terbuka, pertunjukan seni, dan edukasi kuliner khas Bali. Hal ini dapat menjadi daya tarik tambahan bagi wisatawan yang ingin merasakan pengalaman kuliner yang lebih autentik.

Dengan kekayaan kuliner yang dimiliki, Desa Darmasaba berpotensi menjadi kawasan kuliner unggulan di Kabupaten Badung. Dukungan dari masyarakat, pemerintah desa, serta promosi yang lebih luas dapat menjadikan Darmasaba sebagai destinasi kuliner yang semakin dikenal dan berkembang.

" + "id": "cmk3smnj3000svn9b2ww5hesp", + "name": "Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma", + "deskripsi": "

Jalur jogging dan area rekreasi alam yang dimanfaatkan masyarakat untuk aktivitas sehat dan pariwisata lokal.

", + "content": "

Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma adalah area terbuka hijau yang dikembangkan sebagai fasilitas olahraga dan rekreasi, memberikan ruang beraktivitas fisik sambil menikmati suasana alam Desa Darmasaba.

", + "kategoriId": "cmk3pmwq10008vn9bqdquv153", + "imageName": "AFA1PZtHwKkwmWCC4M7lM-mobile.webp" }, { - "id": "cmdykhfhf0005vn6fnz25kify", - "name": "IKM Berbasis Pengolahan Pangan", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam Industri Kecil dan Menengah (IKM) berbasis pengolahan pangan. Dengan sumber daya alam yang melimpah dan warisan kuliner khas Bali, Darmasaba dapat mengembangkan sektor ini untuk meningkatkan kesejahteraan masyarakat dan menciptakan lapangan kerja baru.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam Industri Kecil dan Menengah (IKM) berbasis pengolahan pangan. Dengan sumber daya alam yang melimpah dan warisan kuliner khas Bali, Darmasaba dapat mengembangkan sektor ini untuk meningkatkan kesejahteraan masyarakat dan menciptakan lapangan kerja baru.

Potensi dan Peran IKM Berbasis Pengolahan Pangan:

  1. Produk Unggulan Pengolahan Pangan

    Beberapa produk olahan pangan yang potensial dikembangkan di Darmasaba meliputi:

    - Keripik dan Snack Tradisional : Seperti keripik pisang, keripik singkong, dan rengginang.

    - Sambal Khas Bali : Seperti sambal matah dan sambal embe yang banyak diminati pasar lokal dan nasional.

    - Minuman Herbal dan Jamu : Berbasis rempah seperti kunyit asam, beras kencur, dan wedang jahe.

    - Olahan Makanan Berbasis Kelapa : Seperti virgin coconut oil (VCO), serundeng, dan gula aren.

    - Kue Tradisional Bali : Seperti jaje laklak, jaje uli, dan klepon yang dapat dikemas secara modern.

  2. Peluang Ekonomi dan Pemberdayaan UMKM:

    IKM berbasis pengolahan pangan dapat membuka peluang bagi masyarakat, terutama ibu rumah tangga dan pemuda desa, untuk berwirausaha. Dengan dukungan modal dan pelatihan dari pemerintah desa atau BUMDes Pudak Mesari, usaha kecil ini dapat berkembang menjadi industri yang lebih besar.

  3. Digitalisasi dan Pemasaran Online:

    Darmasaba dapat mengembangkan kawasan sentra IKM sebagai pusat produksi, pelatihan, dan pemasaran produk olahan pangan. Dengan adanya fasilitas ini, para pelaku usaha dapat lebih mudah berkolaborasi, meningkatkan kualitas produk, serta mendapatkan akses ke permodalan dan distribusi yang lebih luas.

  4. Pengembangan Kawasan Sentra IKM:

    Dengan berkembangnya sektor kuliner, banyak pelaku UMKM di Darmasaba mulai merintis usaha makanan, baik dalam bentuk warung makan, katering, hingga produksi makanan ringan seperti keripik, sambal, dan minuman tradisional. Potensi ini dapat terus dikembangkan dengan dukungan pemerintah desa dan promosi melalui media sosial.

  5. Sinergi dengan Pariwisata dan Agrowisata:

    Dengan berkembangnya sektor wisata di Darmasaba, produk olahan pangan dapat dijadikan suvenir khas desa. Pengunjung dapat membeli oleh-oleh seperti sambal kemasan, jajanan khas, atau minuman herbal sebagai bagian dari pengalaman wisata mereka.

IKM berbasis pengolahan pangan memiliki potensi besar untuk menjadi sektor unggulan di Desa Darmasaba. Dengan inovasi, dukungan teknologi, serta pemasaran yang baik, produk-produk lokal dapat bersaing di pasar yang lebih luas, meningkatkan kesejahteraan masyarakat, dan menjadikan Darmasaba sebagai pusat industri pangan kreatif di Kabupaten Badung.

" - }, - { - "id": "cmdykjgmv0008vn6fwg0rr2nh", - "name": "Genteng", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam industri genteng yang dikelola oleh Usaha Mikro, Kecil, dan Menengah (UMKM). Sebagai desa yang masih mempertahankan nilai-nilai tradisional dalam pembangunan, industri genteng di Darmasaba berperan penting dalam penyediaan bahan bangunan berkualitas bagi masyarakat lokal maupun luar daerah.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam industri genteng yang dikelola oleh Usaha Mikro, Kecil, dan Menengah (UMKM). Sebagai desa yang masih mempertahankan nilai-nilai tradisional dalam pembangunan, industri genteng di Darmasaba berperan penting dalam penyediaan bahan bangunan berkualitas bagi masyarakat lokal maupun luar daerah.

Potensi dan Peran UMKM Genteng:

  1. Genteng Tradisional Berkualitas Tinggi

    UMKM di Darmasaba memproduksi genteng dari bahan baku pilihan seperti tanah liat berkualitas, yang menghasilkan genteng dengan daya tahan tinggi, kuat, dan cocok untuk iklim tropis. Beberapa jenis genteng yang dihasilkan meliputi:

    - Genteng Tanah Liat : Kuat, tahan lama, dan memiliki estetika khas tradisional.

    - Genteng Beton : Cocok untuk bangunan modern dengan ketahanan lebih tinggi.

    - Genteng Keramik : Memberikan tampilan elegan dan daya serap air yang lebih rendah.

  2. Peluang Ekonomi dan Pemberdayaan Masyarakat:

    Industri genteng di Darmasaba memberikan peluang kerja bagi masyarakat setempat, terutama dalam bidang produksi, distribusi, hingga pemasaran. UMKM genteng juga mendukung keberlanjutan ekonomi desa dengan meningkatkan pendapatan warga serta mengurangi angka pengangguran.

  3. Inovasi dan Pengembangan Teknologi

    Beberapa pengrajin genteng di Darmasaba telah mulai mengadopsi teknologi modern dalam proses produksi, seperti:

    - Penggunaan cetakan dan oven pembakaran efisien untuk meningkatkan kualitas dan kapasitas produksi.

    - Teknik pelapisan anti bocor dan anti lumut untuk membuat genteng lebih tahan lama.

    - Desain genteng inovatif yang lebih ringan dan mudah dipasang.

  4. Pemasaran dan Ekspansi Pasar

    Dengan meningkatnya pembangunan perumahan dan proyek konstruksi di Bali, permintaan akan genteng berkualitas terus bertambah. UMKM genteng Darmasaba dapat memperluas pasarnya dengan:

    - Menjalin kerja sama dengan kontraktor dan pengembang properti.

    - Mempromosikan produk melalui media sosial dan marketplace online.

    - Menyediakan layanan custom sesuai kebutuhan pelanggan.

  5. Keberlanjutan dan Ramah Lingkungan:

    Industri genteng di Darmasaba berpotensi dikembangkan secara lebih ramah lingkungan dengan menerapkan metode produksi yang mengurangi limbah dan emisi. Pemanfaatan energi alternatif serta daur ulang bahan limbah dapat membantu menciptakan industri yang lebih berkelanjutan.

UMKM genteng di Desa Darmasaba memiliki potensi besar untuk terus berkembang sebagai sektor industri unggulan. Dengan inovasi, pemasaran yang lebih luas, serta dukungan dari pemerintah dan masyarakat, industri ini dapat meningkatkan kesejahteraan warga dan memperkuat perekonomian desa.

" - }, - { - "id": "cmdyklax3000bvn6fdu53f3xq", - "name": "Peternakan Ikan Lele", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor peternakan lele. Dengan kondisi lingkungan yang mendukung serta meningkatnya permintaan ikan lele di pasaran, budidaya ikan lele dapat menjadi salah satu sektor ekonomi unggulan yang mampu meningkatkan kesejahteraan masyarakat desa.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor peternakan lele. Dengan kondisi lingkungan yang mendukung serta meningkatnya permintaan ikan lele di pasaran, budidaya ikan lele dapat menjadi salah satu sektor ekonomi unggulan yang mampu meningkatkan kesejahteraan masyarakat desa.

Potensi dan Peran Peternakan Ikan Lele:

  1. Kondisi Lingkungan yang Mendukung

    Darmasaba memiliki sumber air yang cukup serta iklim yang cocok untuk budidaya ikan lele. Kolam-kolam budidaya dapat dibuat dengan berbagai sistem, seperti:

    - Kolam Terpal : Mudah dibuat dan lebih efisien dalam perawatan.

    - Kolam Beton : Lebih tahan lama dan cocok untuk produksi skala besar.

    - Sistem Bioflok : Teknologi modern yang dapat meningkatkan kepadatan ikan dan mengurangi limbah.

  2. Permintaan Pasar yang Tinggi:

    Lele merupakan salah satu jenis ikan yang memiliki permintaan tinggi di Bali, baik untuk konsumsi rumah tangga, warung makan, hingga restoran. Produk olahan seperti lele goreng, pecel lele, dan abon lele semakin diminati, membuka peluang besar bagi peternak lele di Darmasaba.

  3. Inovasi dan Pengembangan Teknologi

    Beberapa pengrajin genteng di Darmasaba telah mulai mengadopsi teknologi modern dalam proses produksi, seperti:

    - Penggunaan cetakan dan oven pembakaran efisien untuk meningkatkan kualitas dan kapasitas produksi.

    - Teknik pelapisan anti bocor dan anti lumut untuk membuat genteng lebih tahan lama.

    - Desain genteng inovatif yang lebih ringan dan mudah dipasang.

  4. Pemasaran dan Ekspansi Pasar

    Dengan meningkatnya pembangunan perumahan dan proyek konstruksi di Bali, permintaan akan genteng berkualitas terus bertambah. UMKM genteng Darmasaba dapat memperluas pasarnya dengan:

    - Menjalin kerja sama dengan kontraktor dan pengembang properti.

    - Mempromosikan produk melalui media sosial dan marketplace online.

    - Menyediakan layanan custom sesuai kebutuhan pelanggan.

  5. Keberlanjutan dan Ramah Lingkungan:

    Industri genteng di Darmasaba berpotensi dikembangkan secara lebih ramah lingkungan dengan menerapkan metode produksi yang mengurangi limbah dan emisi. Pemanfaatan energi alternatif serta daur ulang bahan limbah dapat membantu menciptakan industri yang lebih berkelanjutan.

UMKM genteng di Desa Darmasaba memiliki potensi besar untuk terus berkembang sebagai sektor industri unggulan. Dengan inovasi, pemasaran yang lebih luas, serta dukungan dari pemerintah dan masyarakat, industri ini dapat meningkatkan kesejahteraan warga dan memperkuat perekonomian desa.

" - }, - { - "id": "cmdykpkwf000gvn6ftas2cjje", - "name": "Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi wisata olahraga dan rekreasi melalui Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa. Jalur jogging ini tidak hanya menjadi fasilitas olahraga bagi warga, tetapi juga berpotensi dikembangkan sebagai destinasi wisata sehat berbasis alam yang menarik bagi wisatawan lokal maupun luar daerah.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi wisata olahraga dan rekreasi melalui Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa. Jalur jogging ini tidak hanya menjadi fasilitas olahraga bagi warga, tetapi juga berpotensi dikembangkan sebagai destinasi wisata sehat berbasis alam yang menarik bagi wisatawan lokal maupun luar daerah.

Potensi dan Peran Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa:

  1. Keindahan Alam dan Udara Segar:

    Jogging track yang membentang di Tegeh Aban, Karang Gadon, dan Munduk Uma Desa menawarkan pemandangan alam yang asri dengan udara segar khas pedesaan. Jalur ini melewati area persawahan hijau, perkebunan, serta hutan kecil yang memberikan pengalaman jogging yang lebih menyenangkan dan menenangkan.

  2. Fasilitas Olahraga dan Rekreasi

    Selain untuk jogging, jalur ini juga cocok digunakan untuk:

    - Bersepeda santai : Jalur yang nyaman untuk pecinta sepeda.

    - Trekking ringan : Cocok bagi wisatawan yang ingin menikmati suasana pedesaan.

    - Meditasi dan Yoga : Area yang tenang dan alami, ideal untuk relaksasi.

  3. Destinasi Wisata Sehat dan Edukasi:

    Jogging track ini berpotensi dikembangkan sebagai wisata sehat berbasis alam, di mana pengunjung bisa menikmati udara segar sambil berolahraga. Selain itu, jalur ini dapat dijadikan sebagai rute edukasi lingkungan, mengenalkan keanekaragaman hayati, pertanian, serta kehidupan masyarakat desa.

  4. Potensi Ekonomi bagi Masyarakat

    Dengan meningkatnya jumlah pengunjung, masyarakat sekitar dapat memanfaatkan peluang usaha seperti:

    - Warung sehat dan kuliner lokal : Menyediakan makanan dan minuman sehat bagi para pengunjung.

    - Jasa penyewaan sepeda : Menarik bagi wisatawan yang ingin berkeliling lebih jauh.

    - Pemandu wisata lokal : Memberikan pengalaman lebih bagi wisatawan yang ingin mengenal sejarah dan budaya desa.

  5. Pengembangan Berkelanjutan:

    Agar semakin menarik, jogging track ini bisa dilengkapi dengan fasilitas tambahan seperti tempat istirahat, spot foto alami, papan informasi tentang flora dan fauna, serta area taman bunga untuk mempercantik jalur jogging.

Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa memiliki potensi besar sebagai destinasi wisata sehat dan olahraga berbasis alam. Dengan pengelolaan yang baik serta dukungan dari pemerintah desa dan masyarakat, jalur ini bisa menjadi ikon baru Desa Darmasaba yang menarik bagi wisatawan serta meningkatkan perekonomian warga setempat.

" - }, - { - "id": "cmdykr76v000jvn6fqngibbmq", + "id": "cmk3t8hgs000vvn9bna6pnw8r", "name": "Dam Tanah Putih", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi wisata olahraga dan rekreasi melalui Jogging Track Tegeh Aban, Karang Gadon, dan Munduk Uma Desa. Jalur jogging ini tidak hanya menjadi fasilitas olahraga bagi warga, tetapi juga berpotensi dikembangkan sebagai destinasi wisata sehat berbasis alam yang menarik bagi wisatawan lokal maupun luar daerah.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki Dam Tanah Putih sebagai salah satu potensi desa yang bernilai strategis. Selain berfungsi sebagai infrastruktur pengairan, dam ini juga memiliki potensi untuk dikembangkan sebagai destinasi wisata alam, edukasi, dan rekreasi bagi masyarakat lokal maupun wisatawan.

Potensi dan Peran Dam Tanah Putih:

  1. Fungsi Utama Sebagai Sumber Pengairan

    Dam Tanah Putih memiliki peran penting dalam sistem irigasi yang menopang sektor pertanian di Darmasaba. Air dari dam ini digunakan untuk:

    - Mengairi sawah dan ladang : Menjamin ketersediaan air bagi petani sepanjang tahun.

    - Menjaga keseimbangan ekosistem : Menjadi habitat bagi ikan air tawar dan berbagai biota air.

    - Menampung air hujan : Membantu mengurangi risiko banjir dan kekeringan.

  2. Potensi Wisata Alam dan Rekreasi

    Dengan pemandangan alam yang asri dan suasana yang sejuk, Dam Tanah Putih memiliki potensi besar untuk dikembangkan sebagai tempat wisata alam. Beberapa kegiatan yang bisa dikembangkan di area ini antara lain:

    - Trekking dan jogging di sekitar dam : Menikmati udara segar dan pemandangan indah.

    - Berkemah dan piknik : Cocok untuk keluarga dan komunitas yang ingin menikmati alam.

    - Wisata air : Seperti pemancingan atau wisata perahu kecil yang dapat menarik wisatawan.

    - Spot fotografi alam : Keindahan dam dan sekitarnya menjadi latar yang menarik bagi para fotografer.

  3. Potensi Ekonomi dan UMKM Lokal

    Dengan pengembangan dam sebagai destinasi wisata, masyarakat sekitar dapat memanfaatkan peluang usaha seperti:

    - Warung makan dan jajanan tradisional : Menyediakan makanan khas Bali bagi wisatawan.

    - Jasa penyewaan alat rekreasi : Seperti pancing atau perahu kecil.

    - Produk kerajinan tangan dan suvenir : Oleh-oleh khas Darmasaba yang menarik bagi pengunjung.

  4. Pengembangan Konservasi dan Edukasi Lingkungan

    Dam Tanah Putih juga bisa menjadi tempat edukasi lingkungan dengan konsep konservasi, di mana pengunjung bisa belajar tentang:

    - Pengelolaan sumber daya air yang berkelanjutan.

    - Keanekaragaman hayati di sekitar dam.

    - Pentingnya ekosistem perairan bagi pertanian dan kehidupan masyarakat.

Dengan berbagai fungsi dan keindahannya, Dam Tanah Putih memiliki potensi besar untuk dikembangkan sebagai destinasi wisata alam, rekreasi, serta edukasi lingkungan. Dengan pengelolaan yang baik dan dukungan dari masyarakat serta pemerintah desa, dam ini dapat menjadi aset penting bagi Darmasaba, baik dari sisi ekonomi maupun kelestarian lingkungan.

" + "deskripsi": "

Bendungan lokal yang berfungsi sebagai sumber irigasi pertanian.

", + "content": "

Dam Tanah Putih merupakan bendungan yang mendukung pengairan sawah dan lahan pertanian di Desa Darmasaba, yang membantu peningkatan produktivitas pertanian masyarakat setempat.

", + "kategoriId": "cmk3pmwq10008vn9bqdquv153", + "imageName": "84pnYeV2_9f4xplh9RBXW-mobile.webp" }, { - "id": "cmdyku9qh000mvn6ft76322sv", + "id": "cmk3tlrkb000yvn9bo1vse4c5", "name": "UMKM", - "deskripsi": "Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor Usaha Mikro, Kecil, dan Menengah (UMKM). Keberadaan UMKM di desa ini tidak hanya menjadi motor penggerak ekonomi lokal, tetapi juga mendukung pelestarian budaya dan kearifan lokal melalui berbagai produk unggulan.", - "content": "

Desa Darmasaba, yang terletak di Kecamatan Abiansemal, Kabupaten Badung, memiliki potensi besar dalam sektor Usaha Mikro, Kecil, dan Menengah (UMKM). Keberadaan UMKM di desa ini tidak hanya menjadi motor penggerak ekonomi lokal, tetapi juga mendukung pelestarian budaya dan kearifan lokal melalui berbagai produk unggulan.

Potensi dan Peran UMKM:

  1. Kerajinan Tangan dan Produk Lokal

    Darmasaba memiliki banyak pengrajin yang menghasilkan produk unik dengan nilai seni tinggi, seperti:

    - Genteng dan bahan bangunan tradisional : Genteng khas Darmasaba yang berkualitas tinggi.

    - Kerajinan anyaman dan ukiran : Produk berbasis rotan dan kayu yang banyak diminati pasar lokal dan internasional.

    - Pakaian adat dan kain tradisional : Mendukung pelestarian budaya Bali.

  2. Industri Kuliner Khas Darmasaba

    Kuliner khas desa ini memiliki potensi besar untuk dikembangkan sebagai bisnis UMKM, seperti:

    - Babi Guling : Salah satu kuliner favorit yang banyak diminati wisatawan.

    - Jajanan tradisional Bali : Seperti laklak, jaja uli, dan klepon yang masih dibuat dengan cara tradisional.

    - Olahan ikan lele : Seperti abon lele, lele asap, dan pecel lele yang memiliki pasar luas.

  3. UMKM Berbasis Pengolahan Pangan

    Beberapa UMKM di Darmasaba mengolah hasil pertanian dan peternakan menjadi produk bernilai tambah, seperti:

    - Keripik singkong dan pisang : Camilan sehat berbasis bahan lokal.

    - Olahan kelapa : Seperti minyak kelapa murni dan gula aren.

    - Produk herbal dan jamu : Menggunakan bahan-bahan alami dari tanaman lokal.

  4. Dukungan dan Pengembangan UMKM

    Agar UMKM di Darmasaba semakin berkembang, perlu adanya:

    - Pelatihan dan pendampingan usaha : Untuk meningkatkan kualitas produk dan manajemen usaha.

    - Pemasaran digital : Menggunakan media sosial dan e-commerce untuk menjangkau pasar lebih luas.

    - Kerja sama dengan BUMDes Pudak Mesari : Untuk membantu akses modal dan pengelolaan bisnis yang lebih profesional.

UMKM di Desa Darmasaba memiliki potensi besar dalam berbagai sektor, mulai dari kerajinan tangan, kuliner, hingga wisata berbasis masyarakat. Dengan inovasi, pemasaran yang lebih luas, dan dukungan dari pemerintah desa serta masyarakat, UMKM Darmasaba dapat berkembang pesat dan menjadi tulang punggung perekonomian desa.

" + "deskripsi": "

Usaha mikro, kecil, dan menengah masyarakat Desa Darmasaba.

", + "content": "

UMKM di Darmasaba mencakup berbagai usaha kecil produktif seperti kuliner, kerajinan tangan, dan jasa lainnya, yang menjadi pilar kegiatan ekonomi dan pemberdayaan masyarakat lokal.

", + "kategoriId": "cmk3s1ks6000ivn9bcrv960ko", + "imageName": "5VSeupjyjtam02PNJW3ZG-mobile.webp" + }, + { + "id": "cmk3tzpku0011vn9beq2kzyen", + "name": "Kawasan Kuliner", + "deskripsi": "

Wilayah yang menyediakan berbagai pilihan kuliner khas lokal Desa Darmasaba.

", + "content": "

Kawasan kuliner di Darmasaba menjadi tempat berkumpul dan menikmati makanan khas lokal, mendukung kegiatan ekonomi UMKM kuliner desa serta daya tarik wisata kuliner.

", + "kategoriId": "cmk3s1ks6000ivn9bcrv960ko", + "imageName": "gjwx1dcHKMJEICbLF6g88-mobile.webp" + }, + { + "id": "cmk3u7gzh0014vn9brvlz3fjp", + "name": "IKM berbasis Pengolahan Pangan", + "deskripsi": "

Industri kecil menengah yang fokus pada pengolahan produk pangan lokal.

", + "content": "

IKM berbasis pengolahan pangan di desa mencakup produksi makanan olahan yang dihasilkan oleh masyarakat setempat, memperkaya nilai tambah hasil pertanian lokal dan menciptakan peluang bisnis.

", + "kategoriId": "cmk3s1ks6000ivn9bcrv960ko", + "imageName": "j3q1NLvwPFrxAmgMXBmq2-mobile.webp" + }, + { + "id": "cmk3udr960017vn9bq33ce6mw", + "name": "Peternakan Ikan Lele", + "deskripsi": "

Usaha budidaya ikan lele sebagai potensi perikanan desa.

", + "content": "

Peternakan ikan lele menjadi salah satu bentuk usaha budidaya perikanan di Darmasaba, memberikan sumber pendapatan tambahan bagi petani ikan dan diversifikasi komoditas desa.

", + "kategoriId": "cmk3s1ks6000ivn9bcrv960ko", + "imageName": "eZCq49lXnx6zKslb6hLxz-mobile.webp" } ] diff --git a/prisma/data/desa/profile/profil_perbekel.json b/prisma/data/desa/profile/profil_perbekel.json index efaf0021..2ce61afe 100644 --- a/prisma/data/desa/profile/profil_perbekel.json +++ b/prisma/data/desa/profile/profil_perbekel.json @@ -1,9 +1,10 @@ [ - { - "id": "edit", - "biodata": "

I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.

", - "pengalaman": "", - "pengalamanOrganisasi": "", - "programUnggulan": "

Pemberdayaan Ekonomi dan UMKM

" - } -] \ No newline at end of file + { + "id": "edit", + "biodata": "

I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.

", + "pengalaman": "", + "pengalamanOrganisasi": "", + "programUnggulan": "

Pemberdayaan Ekonomi dan UMKM

", + "imageName": "perbekel-profile-desa.webp" + } +] diff --git a/prisma/data/desa/profile/profile-perbekel-lalu.json b/prisma/data/desa/profile/profile-perbekel-lalu.json new file mode 100644 index 00000000..22f49658 --- /dev/null +++ b/prisma/data/desa/profile/profile-perbekel-lalu.json @@ -0,0 +1,65 @@ +[ + { + "id": "cmk4yswiy0003vnwq97wem6t2", + "nama": "Si Gede Kania", + "periode": "Tahun 1943 - 1946", + "imageName": "120DDmtAkWEOlu_z56EVj-mobile.webp", + "daerah": "Perbekel Tegal" + }, + { + "id": "cmk4yvuxg0006vnwqa0nk7jcx", + "nama": "Si Gede Gandem", + "periode": "Tahun 1946 - 1950", + "imageName": "bJEAL2K_DcR9AhabklcdP-mobile.webp", + "daerah": "Perbekel Tegal" + }, + { + "id": "cmk4ywml70009vnwqnnrp0cpq", + "nama": "I Wayan Sama", + "periode": "Tahun 1950 - 1953", + "imageName": "szxGnXEFqa0k0nvZ-ALVG-mobile.webp", + "daerah": "Perbekel Tegal" + }, + { + "id": "cmk4yzix3000cvnwqn2pa57gs", + "nama": "I Wayan Nambreg", + "periode": "Tahun 1950 - 1960", + "imageName": "ectWSAf6wfzIPzKMf3wzo-mobile.webp", + "daerah": "Perbekel Darmasaba" + }, + { + "id": "cmk4z0sgd000fvnwqlgjasqx9", + "nama": "Ida Bagus Putu Oka", + "periode": "Tahun 1953 - 1974", + "imageName": "VwjN6P8HyaKpNngtLCt6z-mobile.webp", + "daerah": "Perbekel Tegal / Darmasaba" + }, + { + "id": "cmk4z1w8m000ivnwqe68w5vjg", + "nama": "I Nyoman Patra", + "periode": "Tahun 1974 - 1991", + "imageName": "f3tdB74WjhVVJXmAE7Sev-mobile.webp", + "daerah": "Perbekel Darmasaba" + }, + { + "id": "cmk4z2y3u000lvnwq7fbr3paz", + "nama": "I Made Rudja", + "periode": "Tahun 1991 - 2007", + "imageName": "X3FbO9ns3We5GM1uAwds1-mobile.webp", + "daerah": "Perbekel Darmasaba" + }, + { + "id": "cmk4z6esm000ovnwqacek3jlx", + "nama": "I Wayan Kaler, S.H., M.H.", + "periode": "Tahun 2007 - 2013", + "imageName": "AO-mB9JXm0ahmNnSAEDJ3-mobile.webp", + "daerah": "Perbekel Darmasaba" + }, + { + "id": "cmk4z7e77000rvnwqo2wkmbwl", + "nama": "I Made Taram, S.H.", + "periode": "Tahun 2013 - 2019", + "imageName": "ropKaJjSbK_XAyxd8mEYt-mobile.webp", + "daerah": "Perbekel Darmasaba" + } +] diff --git a/prisma/data/desa/profile/profileDesaImage.json b/prisma/data/desa/profile/profileDesaImage.json new file mode 100644 index 00000000..678198a6 --- /dev/null +++ b/prisma/data/desa/profile/profileDesaImage.json @@ -0,0 +1,26 @@ +[ + { + "id": "cmkz5lumu0008vn5gso2vrejt", + "label": "Bunga Pudak", + "imageName": "fpovTIXnnsX5duqLeoSx6-mobile.webp", + "maskotDesaId": "edit" + }, + { + "id": "cmkz5lumu0009vn5gcn3ah0ku", + "label": "Pohon Pudak", + "imageName": "I01xTmkCZo3u8-y0w07TG-mobile.webp", + "maskotDesaId": "edit" + }, + { + "id": "cmkz5lumu000avn5grvxbmaux", + "label": "Tari Sekar Pudak", + "imageName": "MlTQffVnQ-itl-vgO0BGM-mobile.webp", + "maskotDesaId": "edit" + }, + { + "id": "cmkz5lumu000bvn5gwujxhp2r", + "label": "Klimaks Tari Pudak", + "imageName": "PdIqEFHq5Q12PEi0NRUJ3-mobile.webp", + "maskotDesaId": "edit" + } +] diff --git a/prisma/data/ekonomi/demografi-pekerjaan/demografi-pekerjaan.json b/prisma/data/ekonomi/demografi-pekerjaan/demografi-pekerjaan.json new file mode 100644 index 00000000..344868c1 --- /dev/null +++ b/prisma/data/ekonomi/demografi-pekerjaan/demografi-pekerjaan.json @@ -0,0 +1,56 @@ +[ + { + "id": "cmkf3kv0b0004vnys6hh7ugj2", + "pekerjaan": "Petani/Pekebun", + "lakiLaki": 180, + "perempuan": 120 + }, + { + "id": "cmkf3kv0b0004vnys6hh8vhk3", + "pekerjaan": "Perajin Industri", + "lakiLaki": 95, + "perempuan": 140 + }, + { + "id": "cmkf3kv0b0004vnys6hh9wil4", + "pekerjaan": "Pedagang/UMKM", + "lakiLaki": 130, + "perempuan": 170 + }, + { + "id": "cmkf3kv0b0004vnys6hh0xjm5", + "pekerjaan": "Karyawan Swasta", + "lakiLaki": 260, + "perempuan": 310 + }, + { + "id": "cmkf3kv0b0004vnys6hh1ykn6", + "pekerjaan": "PNS/TNI/Polri", + "lakiLaki": 85, + "perempuan": 75 + }, + { + "id": "cmkf3kv0b0004vnys6hh2zlo7", + "pekerjaan": "Buruh Harian Lepas", + "lakiLaki": 140, + "perempuan": 90 + }, + { + "id": "cmkf3kv0b0004vnys6hh3amp8", + "pekerjaan": "Wiraswasta", + "lakiLaki": 165, + "perempuan": 110 + }, + { + "id": "cmkf3kv0b0004vnys6hh4bnq9", + "pekerjaan": "Pelajar/Mahasiswa", + "lakiLaki": 220, + "perempuan": 240 + }, + { + "id": "cmkf3kv0b0004vnys6hh5cor0", + "pekerjaan": "Belum/Tidak Bekerja", + "lakiLaki": 70, + "perempuan": 95 + } +] diff --git a/prisma/data/ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json b/prisma/data/ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json new file mode 100644 index 00000000..3bb2b1e5 --- /dev/null +++ b/prisma/data/ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json @@ -0,0 +1,27 @@ +[ + { + "id": "795b3e69-2bdb-4d2b-8331-9433fc604723", + "year": 2021, + "totalPoorPopulation": 480 + }, + { + "id": "0e61ed28-1789-4329-86bc-67bdb715d4fa", + "year": 2022, + "totalPoorPopulation": 450 + }, + { + "id": "0f80e31e-56d7-42aa-aede-e3104f1bca4f", + "year": 2023, + "totalPoorPopulation": 430 + }, + { + "id": "3668ecd4-4c4c-4a87-a39a-72661fce5a2c", + "year": 2024, + "totalPoorPopulation": 410 + }, + { + "id": "854a375e-4662-4e4c-abd8-59115b4910a9", + "year": 2025, + "totalPoorPopulation": 390 + } +] diff --git a/prisma/data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json b/prisma/data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json new file mode 100644 index 00000000..8ca5ab9d --- /dev/null +++ b/prisma/data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json @@ -0,0 +1,10 @@ +[ + { + "id": "cmkko983k000104l2ff4i8awk", + "SD": "35", + "SMP": "28", + "SMA": "20", + "D3": "8", + "S1": "15" + } +] diff --git a/prisma/data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json b/prisma/data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json new file mode 100644 index 00000000..3f4c4873 --- /dev/null +++ b/prisma/data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json @@ -0,0 +1,9 @@ +[ + { + "id": "cmkko8wam000004l2h99rekid", + "usia18_25": "25", + "usia26_35": "18", + "usia36_45": "12", + "usia46_keatas": "10" + } +] diff --git a/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json b/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json index f64b6977..3f5da844 100644 --- a/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json +++ b/prisma/data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json @@ -94,6 +94,24 @@ "educatedUnemployment": 78, "uneducatedUnemployment": 54, "percentageChange": -2.22 + }, + { + "id": "13f11e4f-c96e-4155-bf30-3f45076ee34c", + "month": "Jan", + "year": 2026, + "totalUnemployment": 350, + "educatedUnemployment": 200, + "uneducatedUnemployment": 150, + "percentageChange": 0.0 + }, + { + "id": "477b3b0c-af18-4816-a00b-dcac530d57cd", + "month": "Feb", + "year": 2026, + "totalUnemployment": 270, + "educatedUnemployment": 150, + "uneducatedUnemployment": 120, + "percentageChange": -22.9 } ] \ No newline at end of file diff --git a/prisma/data/ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json b/prisma/data/ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json new file mode 100644 index 00000000..4747aaa9 --- /dev/null +++ b/prisma/data/ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json @@ -0,0 +1,57 @@ +[ + { + "id": "3b3e817b-c136-4488-ac79-9a7d408939a2", + "posisi": "Lowongan TPS3R Pudak Mesari", + "namaPerusahaan": "TPS3R Pudak Mesari Desa Darmasaba", + "lokasi": "Desa Darmasaba, Abiansemal, Kabupaten Badung, Bali", + "tipePekerjaan": "Freelance", + "gaji": "1.500.000", + "deskripsi": "Menjalankan tugas di TPS3R Pudak Mesari.", + "kualifikasi": "Usia 18-30 tahun, SMA/SMK minimal", + "notelp": "089647037426" + }, + { + "id": "3b3e817b-c136-4488-bd80-9a7d408939a2", + "posisi": "Marketing Executive", + "namaPerusahaan": "PT Mitra Krida Mandiri (Dealer Honda MKM Darmasaba)", + "lokasi": "Jalan Raya Darmasaba No.169, Abiansemal, Badung, Bali", + "tipePekerjaan": "Full Time", + "gaji": "2.500.000", + "deskripsi": "Menjalankan tugas pemasaran dan penjualan produk Honda di area Darmasaba.", + "kualifikasi": "Usia 18-30 tahun, SMA/SMK minimal, memiliki sepeda motor Honda dan smartphone.", + "notelp": "081296001047" + }, + { + "id": "3b3e817b-c136-4488-ce91-9a7d408939a2", + "posisi": "Kasir", + "namaPerusahaan": "GOGO DARMASABA", + "lokasi": "Jl. Raya Darmasaba, Darmasaba, Kec. Abiansemal, Kabupaten Badung, Bali", + "tipePekerjaan": "Full Time", + "gaji": "2.500.000", + "deskripsi": "Melakukan pelayanan kasir dan administrasi pelanggan di restoran/food service.", + "kualifikasi": "Wanita, 18-30 tahun, SMA/SMK minimal pengalaman 1-3 tahun sebagai kasir atau Customer Service.", + "notelp": "089647037426" + }, + { + "id": "3b3e817b-c136-4488-df02-9a7d408939a2", + "posisi": "Kasir / Teknisi Handphone", + "namaPerusahaan": "Jaya Cell Darmasaba", + "lokasi": "Jl. Raya Darmasaba, Darmasaba, Abiansemal, Badung, Bali", + "tipePekerjaan": "Full Time", + "gaji": "2.000.000", + "deskripsi": "Melakukan pelayanan kasir serta teknisi ponsel termasuk troubleshooting dan perbaikan.", + "kualifikasi": "Tidak disebutkan pengalaman khusus, memiliki KTP dan keinginan kuat untuk bekerja.", + "notelp": "089647037426" + }, + { + "id": "3b3e817b-c136-4488-eg13-9a7d408939a2", + "posisi": "Guru Les (Pengajar Anak)", + "namaPerusahaan": "Bimba AIUEO Darmasaba", + "lokasi": "Darmasaba, Kabupaten Badung, Bali", + "tipePekerjaan": "Full Time", + "gaji": "2.000.000", + "deskripsi": "Mengajar calistung dan perkembangan dasar anak usia 3-6 tahun.", + "kualifikasi": "Minimal SMA/SMK, komunikasi baik, berinteraksi dengan anak-anak.", + "notelp": "089647037426" + } +] diff --git a/prisma/data/ekonomi/pasar-desa/kategori-produk.json b/prisma/data/ekonomi/pasar-desa/kategori-produk.json index c8c9b59e..7093047e 100644 --- a/prisma/data/ekonomi/pasar-desa/kategori-produk.json +++ b/prisma/data/ekonomi/pasar-desa/kategori-produk.json @@ -6,5 +6,17 @@ { "id": "5c06chf7-123f-6hfe-0663-5e9h76e55060", "nama": "Minuman" + }, + { + "id": "5c06chf7-123f-7igd-0663-5e9h76e55060", + "nama": "Sembako" + }, + { + "id": "5c06chf7-123f-8jhe-0663-5e9h76e55060", + "nama": "Sayur Mayur" + }, + { + "id": "5c06chf7-123f-9kif-0663-5e9h76e55060", + "nama": "Protein Hewani" } ] diff --git a/prisma/data/ekonomi/pasar-desa/kategori-to-pasar.json b/prisma/data/ekonomi/pasar-desa/kategori-to-pasar.json new file mode 100644 index 00000000..0ac0fae2 --- /dev/null +++ b/prisma/data/ekonomi/pasar-desa/kategori-to-pasar.json @@ -0,0 +1,42 @@ +[ + { + "id": "f6b52033-5016-45d9-b0fd-b9d4b6c4729b", + "kategoriId": "5c06chf7-123f-9kif-0663-5e9h76e55060", + "pasarDesaId": "d62660a2-ac6b-428a-acf6-58cc837ef789" + }, + { + "id": "d2ef373c-043c-44b5-adde-6a25a54199d3", + "kategoriId": "5c06chf7-123f-7igd-0663-5e9h76e55060", + "pasarDesaId": "d62660a2-ac6b-428a-acf6-58cc837ef789" + }, + { + "id": "ad427752-fea0-4ef3-a312-5961eefd5ee3", + "kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959", + "pasarDesaId": "d62660a2-ac6b-428a-acf6-58cc837ef789" + }, + { + "id": "bd00ab59-7ac8-4d40-94de-a86bb0eb4557", + "kategoriId": "5c06chf7-123f-8jhe-0663-5e9h76e55060", + "pasarDesaId": "24c6b992-49da-4c6e-aebb-72cf89f75438" + }, + { + "id": "b7d311a2-a23a-499d-a339-823c5e30849a", + "kategoriId": "5c06chf7-123f-7igd-0663-5e9h76e55060", + "pasarDesaId": "24c6b992-49da-4c6e-aebb-72cf89f75438" + }, + { + "id": "50ccc6c9-92c1-4d86-9585-85d48d35f640", + "kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959", + "pasarDesaId": "24c6b992-49da-4c6e-aebb-72cf89f75438" + }, + { + "id": "3b27f795-1d1d-4655-90f9-b779a009094e", + "kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959", + "pasarDesaId": "6dea2257-b710-4cd2-8d94-9b6737e658d8" + }, + { + "id": "d45873c5-5948-40f9-a88d-aa0861132bae", + "kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959", + "pasarDesaId": "1b7a17ea-83f7-4e73-a94d-96e2b4a623f2" + } +] diff --git a/prisma/data/ekonomi/pasar-desa/pasar-desa.json b/prisma/data/ekonomi/pasar-desa/pasar-desa.json new file mode 100644 index 00000000..5ab82ca5 --- /dev/null +++ b/prisma/data/ekonomi/pasar-desa/pasar-desa.json @@ -0,0 +1,46 @@ +[ + { + "id": "1b7a17ea-83f7-4e73-a94d-96e2b4a623f2", + "nama": "Warung Pasar Darmasaba", + "harga": 30000, + "imageName": "YdCBnK-bWxlyHjwsk4Qie-mobile.webp", + "rating": 4.3, + "alamatUsaha": "Br. Baler Pasar, Desa Darmasaba, Kec. Abiansemal", + "kontak": "081234567890", + "deskripsi": "Warung tradisional yang menjual kebutuhan pokok harian seperti sembako, jajanan pasar, dan minuman.", + "kategoriProdukId": "5c06chf7-123f-7igd-0663-5e9h76e55060" + }, + { + "id": "6dea2257-b710-4cd2-8d94-9b6737e658d8", + "nama": "Jajanan Pasar Bu Made", + "imageName": "TWdNTZZbTOhFTNJGGPDyG-mobile.webp", + "harga": 5000, + "rating": 4.6, + "alamatUsaha": "Jl. Raya Darmasaba, dekat Banjar Baler Pasar", + "kontak": "082145678901", + "deskripsi": "Menjual berbagai jajanan pasar tradisional Bali seperti laklak, klepon, dan pisang rai.", + "kategoriProdukId": "4b95bge6-012e-5ged-9552-4d8g65d44959" + }, + { + "id": "24c6b992-49da-4c6e-aebb-72cf89f75438", + "nama": "Sayur Segar Pak Wayan", + "imageName": "mtQsaKtQnhxIYVIooCkiQ-mobile.webp", + "harga": 20000, + "rating": 4.4, + "alamatUsaha": "Area Pasar Desa Darmasaba", + "kontak": "087865432109", + "deskripsi": "Lapak sayur segar yang menyediakan sayuran lokal hasil petani sekitar Desa Darmasaba.", + "kategoriProdukId": "5c06chf7-123f-8jhe-0663-5e9h76e55060" + }, + { + "id": "d62660a2-ac6b-428a-acf6-58cc837ef789", + "nama": "Ayam & Daging Segar Darmasaba", + "imageName": "Ez-SkRyf_F-1gksz_amNg-mobile.webp", + "harga": 80000, + "rating": 4.2, + "alamatUsaha": "Br. Baler Pasar, Desa Darmasaba", + "kontak": "081998877665", + "deskripsi": "Menjual ayam potong dan daging segar untuk kebutuhan rumah tangga dan upacara adat.", + "kategoriProdukId": "5c06chf7-123f-9kif-0663-5e9h76e55060" + } +] diff --git a/prisma/data/ekonomi/pendapatan-asli-desa/apbDesa.json b/prisma/data/ekonomi/pendapatan-asli-desa/apbDesa.json new file mode 100644 index 00000000..8bad2769 --- /dev/null +++ b/prisma/data/ekonomi/pendapatan-asli-desa/apbDesa.json @@ -0,0 +1,6 @@ +[ + { + "id": "6b52d644-09b7-4af5-a78b-3dad854973e7", + "tahun": 2025 + } +] \ No newline at end of file diff --git a/prisma/data/ekonomi/pendapatan-asli-desa/belanjaDesa.json b/prisma/data/ekonomi/pendapatan-asli-desa/belanjaDesa.json new file mode 100644 index 00000000..4293bdff --- /dev/null +++ b/prisma/data/ekonomi/pendapatan-asli-desa/belanjaDesa.json @@ -0,0 +1,7 @@ +[ + { + "id": "82b8c96f-7817-482f-ba5e-85ad78c7bd57", + "name": "Belanja Desa (Realisasi Semester I)", + "nilai": 43871400 + } +] \ No newline at end of file diff --git a/prisma/data/ekonomi/pendapatan-asli-desa/pembiayaanDesa.json b/prisma/data/ekonomi/pendapatan-asli-desa/pembiayaanDesa.json new file mode 100644 index 00000000..a11284f4 --- /dev/null +++ b/prisma/data/ekonomi/pendapatan-asli-desa/pembiayaanDesa.json @@ -0,0 +1,7 @@ +[ + { + "id": "cb593f99-9f15-4294-9c17-a93c8c149a25", + "name": "Pembiayaan Neto (SILPA 2024)", + "nilai": 69289666 + } +] diff --git a/prisma/data/ekonomi/pendapatan-asli-desa/pendapatanDesa.json b/prisma/data/ekonomi/pendapatan-asli-desa/pendapatanDesa.json new file mode 100644 index 00000000..82b0c1dd --- /dev/null +++ b/prisma/data/ekonomi/pendapatan-asli-desa/pendapatanDesa.json @@ -0,0 +1,7 @@ +[ + { + "id": "24576ca1-3dee-4608-9fb3-51f16efce901", + "name": "Pendapatan Asli Desa (PADes)", + "nilai": 10610011 + } +] \ No newline at end of file diff --git a/prisma/data/ekonomi/program-kemiskinan/program-kemiskinan.json b/prisma/data/ekonomi/program-kemiskinan/program-kemiskinan.json new file mode 100644 index 00000000..dfb09288 --- /dev/null +++ b/prisma/data/ekonomi/program-kemiskinan/program-kemiskinan.json @@ -0,0 +1,23 @@ +[ + { + "id": "c2760e40-f770-11f0-89ff-719f813f71b3", + "nama": "BLT-DD (Bantuan Langsung Tunai Dana Desa)", + "icon": "bantuan", + "deskripsi": "

Program pemberian Bantuan Langsung Tunai yang dibiayai dari Dana Desa untuk meringankan beban ekonomi keluarga miskin/prasejahtera di Desa Darmasaba.

", + "tahun": 2023 + }, + { + "id": "c7f0f2e0-f770-11f0-89ff-719f813f71b3", + "nama": "Penguatan Ketahanan Pangan", + "icon": "air", + "deskripsi": "

Kegiatan pemberdayaan masyarakat dalam ketahanan pangan untuk mendukung ketersediaan pangan keluarga kurang mampu dan meningkatkan kemampuan produksi pangan lokal.

", + "tahun": 2024 + }, + { + "id": "ccda45e0-f770-11f0-89ff-719f813f71b3", + "nama": "Peningkatan IKM berbasis E-commerce", + "icon": "ekonomi", + "deskripsi": "

Program peningkatan keterampilan usaha mikro kecil (IKM) termasuk pelatihan branding, pengemasan, dan promosi digital untuk memperkuat ekonomi rumah tangga melalui pemasaran online.

", + "tahun": 2025 + } +] diff --git a/prisma/data/ekonomi/program-kemiskinan/statistik-kemiskinan.json b/prisma/data/ekonomi/program-kemiskinan/statistik-kemiskinan.json new file mode 100644 index 00000000..475f015d --- /dev/null +++ b/prisma/data/ekonomi/program-kemiskinan/statistik-kemiskinan.json @@ -0,0 +1,17 @@ +[ + { + "id": "85d8a150-f770-11f0-89ff-719f813f71b3", + "tahun": 2023, + "jumlah": 20 + }, + { + "id": "993b8d20-f770-11f0-89ff-719f813f71b3", + "tahun": 2024, + "jumlah": 30 + }, + { + "id": "9eb3b2a0-f770-11f0-89ff-719f813f71b3", + "tahun": 2025, + "jumlah": 20 + } +] diff --git a/prisma/data/ekonomi/sektor-unggulan/sektor-unggulan.json b/prisma/data/ekonomi/sektor-unggulan/sektor-unggulan.json new file mode 100644 index 00000000..fca71eb1 --- /dev/null +++ b/prisma/data/ekonomi/sektor-unggulan/sektor-unggulan.json @@ -0,0 +1,44 @@ +[ + { + "id": "053999e8-e5c4-4a50-b587-0e0ce15aba1a", + "name": "Pertanian", + "description": "Sektor pertanian meliputi kegiatan bercocok tanam padi, palawija, dan tanaman lain di subak yang menjadi basis mata pencaharian warga", + "value": 90 + }, + { + "id": "8e0d2f2d-512d-4c05-8880-b6e7d144a34d", + "name": "UMKM Kecil", + "description": "Usaha Mikro Kecil Menengah termasuk IKM berbasis pengolahan pangan dan kuliner yang tumbuh di desa sebagai penggerak ekonomi lokal", + "value": 45 + }, + { + "id": "0378b10a-f0e3-421c-9272-225d931179ce", + "name": "Peternakan", + "description": "Peternakan ikan lele dan mata pencaharian lain yang mendukung ketahanan pangan dan ekonomi masyarakat desa", + "value": 30 + }, + { + "id": "4fa28680-8014-4c46-9dd0-1aa910630fd3", + "name": "BUMDes", + "description": "BUMDes Pudak Mesari sebagai lembaga usaha desa yang mengembangkan potensi lokal dan layanan ekonomi", + "value": 20 + }, + { + "id": "669464b2-dd7e-44be-b609-97a9b844df8b", + "name": "Kawasan Kuliner", + "description": "Potensi kawasan kuliner desa yang menjadi daya tarik ekonomi dan pariwisata kecil di daerah Darmasaba", + "value": 15 + }, + { + "id": "ef65e122-84ce-4483-93e9-c1a8bcee9b79", + "name": "Pariwisata", + "description": "Potensi wisata lokal seperti Jogging Track, taman dan water park yang memberikan nilai tambah ekonomi masyarakat", + "value": 35 + }, + { + "id": "08443c84-8ca9-4690-b900-e5e3e753cc97", + "name": "Kerajinan Genteng", + "description": "Kerajinan genteng press di Desa Adat Tegal yang merupakan usaha kerajinan lokal dengan kontribusi ekonomi", + "value": 25 + } +] diff --git a/prisma/data/fetchWithRetry.ts b/prisma/data/fetchWithRetry.ts new file mode 100644 index 00000000..e8a55d73 --- /dev/null +++ b/prisma/data/fetchWithRetry.ts @@ -0,0 +1,33 @@ +export default async function fetchWithRetry( + url: string, + retries = 3, + timeoutMs = 20000 +) { + if (!url || url.trim() === "") { + throw new Error("fetchWithRetry called with empty URL"); + } + for (let attempt = 1; attempt <= retries; attempt++) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const res = await fetch(url, { signal: controller.signal }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status} ${res.statusText}`); + } + + return res; + } catch (err) { + console.warn(`⚠️ Download attempt ${attempt} failed`); + + if (attempt === retries) { + throw err; + } + } finally { + clearTimeout(timeout); + } + } + + throw new Error("Unreachable"); +} diff --git a/prisma/data/file-storage.json b/prisma/data/file-storage.json index 53c27a0b..2f5267dd 100644 --- a/prisma/data/file-storage.json +++ b/prisma/data/file-storage.json @@ -1,137 +1,406 @@ [ { - "id": "cmff0rr4z0002vn0twp333m2", - "name": "S6RIjFaPvdQm3oq4rM4X9-desktop.webp", - "realName": "bares.png", - "path": "uploads/images", + "name": "42RCCpBZla4ZWxXcwx7kG-desktop.webp", + "path": "image/42RCCpBZla4ZWxXcwx7kG-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d05a9e22-feac-4955-b1a2-75faad37f0ac/42RCCpBZla4ZWxXcwx7kG-desktop.webp", "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/S6RIjFaPvdQm3oq4rM4X9-desktop.webp", + "link": "/api/fileStorage/findUnique/Gc79mlIlGuoRQuTqskFj--desktop.webp", "category": "image" + }, { - "id": "cmff0tnf00003vn0t3kgzi0u0", - "name": "_pVNEmThU5ICGa8gv3gh_-desktop.webp", - "realName": "bicara-darma.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/_pVNEmThU5ICGa8gv3gh_-desktop.webp", - "category": "image" + "name": "42RCCpBZla4ZWxXcwx7kG-mobile.webp", + "path": "image/42RCCpBZla4ZWxXcwx7kG-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/c91212b5-7408-4ec9-b7c0-6c0fde2a9fc5/42RCCpBZla4ZWxXcwx7kG-mobile.webp" }, { - "id": "cmff0uykf0004vn0trmmxpgfh", - "name": "bv6rdKvjxkkjUSGLQ0lvB-desktop.webp", - "realName": "daves.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/bv6rdKvjxkkjUSGLQ0lvB-desktop.webp", - "category": "image" + "name": "6DQbAvn0St-xHdPGW3vpY-desktop.webp", + "path": "image/6DQbAvn0St-xHdPGW3vpY-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/68cc521e-05e5-4258-af8d-c87fb76c927e/6DQbAvn0St-xHdPGW3vpY-desktop.webp" }, { - "id": "cmff0z34f0005vn0tjtvq519p", - "name": "Z4hWaV04CvoE20MjccQsV-desktop.webp", - "realName": "mangan.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/Z4hWaV04CvoE20MjccQsV-desktop.webp", - "category": "image" + "name": "6DQbAvn0St-xHdPGW3vpY-mobile.webp", + "path": "image/6DQbAvn0St-xHdPGW3vpY-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/15f7ddd6-448c-4e4a-9b24-0bdc4bce5ce1/6DQbAvn0St-xHdPGW3vpY-mobile.webp" }, { - "id": "cmff38cyq000bvn0t9f01cz3f", - "name": "LvLAtOqWojx4sn6NjJWB9-desktop.webp", - "realName": "gelah-melah.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/LvLAtOqWojx4sn6NjJWB9-desktop.webp", - "category": "image" + "name": "buku1 (1).jpeg", + "path": "image/buku1 (1).jpeg", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d72b02a6-91f5-4d64-9729-521b306328d3/buku1%20%281%29.jpeg" }, { - "id": "cmff0zqvd0007vn0tv6o5hjcq", - "name": "gR2mcvAQVgJ2-rM5coYJj-desktop.webp", - "realName": "inovasi-desa-darmasaba.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/gR2mcvAQVgJ2-rM5coYJj-desktop.webp", - "category": "image" + "name": "buku1.jpeg", + "path": "image/buku1.jpeg", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/912bd7e5-be8a-4c19-8ab8-df6114a38864/buku1.jpeg" }, { - "id": "cmff1013m0008vn0th7t0d64d", - "name": "JpL-9F8-IGztMn8E2ce02-desktop.webp", - "realName": "pdkt.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/JpL-9F8-IGztMn8E2ce02-desktop.webp", - "category": "image" + "name": "buku6.jpg", + "path": "image/buku6.jpg", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/ea56bc7c-094b-464e-859a-6c65bf65361d/buku6.jpg" }, { - "id": "cmff10cwq0009vn0tse8dzu3j", - "name": "bxAk4AsGbJTC705_IVdes-desktop.webp", - "realName": "sajjiana-dharma-raksaka.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/bxAk4AsGbJTC705_IVdes-desktop.webp", - "category": "image" + "name": "c7xWNyoYp8Cak28NG5NoG-desktop.webp", + "path": "image/c7xWNyoYp8Cak28NG5NoG-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d84fb066-4352-44f0-b7bc-5d8c5b8ffb61/c7xWNyoYp8Cak28NG5NoG-desktop.webp" }, { - "id": "cmff2w5ly000avn0telhct71k", - "name": "Vbj_osnMJUkGEQGDTLwV--desktop.webp", - "realName": "perbekel.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/Vbj_osnMJUkGEQGDTLwV--desktop.webp", - "category": "image" + "name": "c7xWNyoYp8Cak28NG5NoG-mobile.webp", + "path": "image/c7xWNyoYp8Cak28NG5NoG-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/9fb1c742-9a80-4788-9c73-9d06108e0051/c7xWNyoYp8Cak28NG5NoG-mobile.webp" }, { - "id": "cmff3joae0000vn6h8sgs0ilg", - "name": "7hox9spUxj56hY_EBYLnj-desktop.webp", - "realName": "youtube.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/7hox9spUxj56hY_EBYLnj-desktop.webp", - "category": "image" + "name": "cg78Sb_QzZFlli9s2FPVc-mobile.webp", + "path": "image/cg78Sb_QzZFlli9s2FPVc-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/c5a2cd18-806d-4dcb-b5c5-b0c6bcef7f35/cg78Sb_QzZFlli9s2FPVc-mobile.webp" }, { - "id": "cmff3ll130001vn6hkhls3f5y", - "name": "ChihV7_1eS-AGtSg9UwMv-desktop.webp", - "realName": "gmail.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/ChihV7_1eS-AGtSg9UwMv-desktop.webp", - "category": "image" + "name": "d3v1AgLoSJhf5xvmmO3oP-mobile.webp", + "path": "image/d3v1AgLoSJhf5xvmmO3oP-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/ae72c51f-9df7-4b07-9fd5-e945c775d9ab/d3v1AgLoSJhf5xvmmO3oP-mobile.webp" }, { - "id": "cmff3mtat0002vn6hs8vyyhdd", - "name": "z8v9ZREwOJHKGIRYauROt-desktop.webp", - "realName": "facebook.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/z8v9ZREwOJHKGIRYauROt-desktop.webp", - "category": "image" + "name": "d6hJgycQawWN3VEcHaqtR-desktop.webp", + "path": "image/d6hJgycQawWN3VEcHaqtR-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/4e5b9387-67b5-4c00-8f80-95ec6c54ff4a/d6hJgycQawWN3VEcHaqtR-desktop.webp" }, { - "id": "cmff3nv180003vn6h5jvedidq", - "name": "BLjMxTKoCNE31uOURR3IU-desktop.webp", - "realName": "telephone-call.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/BLjMxTKoCNE31uOURR3IU-desktop.webp", - "category": "image" + "name": "d6hJgycQawWN3VEcHaqtR-mobile.webp", + "path": "image/d6hJgycQawWN3VEcHaqtR-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/011f65c9-4273-43e8-b6d7-ce7a5319ae83/d6hJgycQawWN3VEcHaqtR-mobile.webp" }, { - "id": "cmff3oouh0004vn6hd94brzv9", - "name": "hkJYAeTNWK_vYaYS20w3I-desktop.webp", - "realName": "instagram.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/hkJYAeTNWK_vYaYS20w3I-desktop.webp", - "category": "image" + "name": "DyX82oztXbHfu6HEvbrpt-desktop.webp", + "path": "image/DyX82oztXbHfu6HEvbrpt-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/b715dce5-1606-44cf-976f-57d8142e218e/DyX82oztXbHfu6HEvbrpt-desktop.webp" }, { - "id": "cmff3q12g0005vn6h5ojov2qa", - "name": "6XEoZ9SFu59COpil03Gya-desktop.webp", - "realName": "tiktok.png", - "path": "uploads/images", - "mimeType": "image/webp", - "link": "/api/fileStorage/findUnique/6XEoZ9SFu59COpil03Gya-desktop.webp", - "category": "image" + "name": "DyX82oztXbHfu6HEvbrpt-mobile.webp", + "path": "image/DyX82oztXbHfu6HEvbrpt-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d0ff5925-ec84-4100-920c-93e2eb479f13/DyX82oztXbHfu6HEvbrpt-mobile.webp" + }, + { + "name": "EcQIGOF6LW1dIKE53vmba-desktop.webp", + "path": "image/EcQIGOF6LW1dIKE53vmba-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/7a97403e-9d6c-4a00-9c54-d5add0bd5915/EcQIGOF6LW1dIKE53vmba-desktop.webp" + }, + { + "name": "EcQIGOF6LW1dIKE53vmba-mobile.webp", + "path": "image/EcQIGOF6LW1dIKE53vmba-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/7e85b8e8-27f5-46c7-b4fc-590d66eef2ce/EcQIGOF6LW1dIKE53vmba-mobile.webp" + }, + { + "name": "Ez-SkRyf_F-1gksz_amNg-desktop.webp", + "path": "image/Ez-SkRyf_F-1gksz_amNg-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/c16de343-1297-4248-b104-83b3e3605f32/Ez-SkRyf_F-1gksz_amNg-desktop.webp" + }, + { + "name": "Ez-SkRyf_F-1gksz_amNg-mobile.webp", + "path": "image/Ez-SkRyf_F-1gksz_amNg-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/ece148ca-8aa1-43ef-a8de-ea0bde0a315a/Ez-SkRyf_F-1gksz_amNg-mobile.webp" + }, + { + "name": "g4ICsRrmOaIqS_yqlQLZK-desktop.webp", + "path": "image/g4ICsRrmOaIqS_yqlQLZK-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/31280b01-6cdb-4d96-9c96-0eecec6a238c/g4ICsRrmOaIqS_yqlQLZK-desktop.webp" + }, + { + "name": "g4ICsRrmOaIqS_yqlQLZK-mobile.webp", + "path": "image/g4ICsRrmOaIqS_yqlQLZK-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/af7f3e11-1f63-4307-88e9-64382a949279/g4ICsRrmOaIqS_yqlQLZK-mobile.webp" + }, + { + "name": "Gc79mlIlGuoRQuTqskFj--desktop.webp", + "path": "image/Gc79mlIlGuoRQuTqskFj--desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/05214122-51bb-4fba-9b7c-f69e072d8a0d/Gc79mlIlGuoRQuTqskFj--desktop.webp" + }, + { + "name": "Gc79mlIlGuoRQuTqskFj--mobile.webp", + "path": "image/Gc79mlIlGuoRQuTqskFj--mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d49ab5d2-7087-4088-8e59-51f116f21e27/Gc79mlIlGuoRQuTqskFj--mobile.webp" + }, + { + "name": "Gi8EX3pBmT719AfzXirDS-desktop.webp", + "path": "image/Gi8EX3pBmT719AfzXirDS-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/4b456bad-228a-4211-a1bb-431dc081ecc7/Gi8EX3pBmT719AfzXirDS-desktop.webp" + }, + { + "name": "Gi8EX3pBmT719AfzXirDS-mobile.webp", + "path": "image/Gi8EX3pBmT719AfzXirDS-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/68308100-d468-4e53-9cc6-3aa12035c8ab/Gi8EX3pBmT719AfzXirDS-mobile.webp" + }, + { + "name": "gyNi4s8TnK2UrViU-gN2C-desktop.webp", + "path": "image/gyNi4s8TnK2UrViU-gN2C-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/539debe4-f3fc-4574-a256-ac8c8dbf5a00/gyNi4s8TnK2UrViU-gN2C-desktop.webp" + }, + { + "name": "gyNi4s8TnK2UrViU-gN2C-mobile.webp", + "path": "image/gyNi4s8TnK2UrViU-gN2C-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/4971a22a-c0dd-4492-bff5-a8aa3e93f27f/gyNi4s8TnK2UrViU-gN2C-mobile.webp" + }, + { + "name": "h_Gd0SoeIJVTi_5TWUO-P-desktop.webp", + "path": "image/h_Gd0SoeIJVTi_5TWUO-P-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/8409886e-c01b-421a-ac44-6d3a4bfd0985/h_Gd0SoeIJVTi_5TWUO-P-desktop.webp" + }, + { + "name": "h_Gd0SoeIJVTi_5TWUO-P-mobile.webp", + "path": "image/h_Gd0SoeIJVTi_5TWUO-P-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/f4a5280d-bd2d-4c5a-8040-183f9e5d951b/h_Gd0SoeIJVTi_5TWUO-P-mobile.webp" + }, + { + "name": "hLeF0GRFZqDUngZnDMAAk-desktop.webp", + "path": "image/hLeF0GRFZqDUngZnDMAAk-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/6b33097e-8d9d-4864-964b-6dc49b62b4ae/hLeF0GRFZqDUngZnDMAAk-desktop.webp" + }, + { + "name": "hLeF0GRFZqDUngZnDMAAk-mobile.webp", + "path": "image/hLeF0GRFZqDUngZnDMAAk-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/7dff816b-65bb-429c-b87e-3ff892d547dc/hLeF0GRFZqDUngZnDMAAk-mobile.webp" + }, + { + "name": "hsHiD59dZQxr8G2SAfUYp-mobile.webp", + "path": "image/hsHiD59dZQxr8G2SAfUYp-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/55103cd4-6f57-491d-bd41-03c0068974ef/hsHiD59dZQxr8G2SAfUYp-mobile.webp" + }, + { + "name": "hyyTFi8EApjzFEZ9EvJgB-desktop.webp", + "path": "image/hyyTFi8EApjzFEZ9EvJgB-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/0ec4a67e-695e-4524-bf22-f823b80c7e6b/hyyTFi8EApjzFEZ9EvJgB-desktop.webp" + }, + { + "name": "hyyTFi8EApjzFEZ9EvJgB-mobile.webp", + "path": "image/hyyTFi8EApjzFEZ9EvJgB-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/c982541e-f8b2-455a-a3cd-f856cd954bed/hyyTFi8EApjzFEZ9EvJgB-mobile.webp" + }, + { + "name": "isTT2LmPbeOWD5wAdqleX-mobile.webp", + "path": "image/isTT2LmPbeOWD5wAdqleX-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/ef6f8237-0803-422a-83af-4daca61c7065/isTT2LmPbeOWD5wAdqleX-mobile.webp" + }, + { + "name": "JhJigMo269K1TFGzSB1OS-desktop.webp", + "path": "image/JhJigMo269K1TFGzSB1OS-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/668d2f32-3277-4842-8a01-c3cb5ca0852b/JhJigMo269K1TFGzSB1OS-desktop.webp" + }, + { + "name": "JhJigMo269K1TFGzSB1OS-mobile.webp", + "path": "image/JhJigMo269K1TFGzSB1OS-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/df8c52a3-189a-48a0-b7e7-98efe2479414/JhJigMo269K1TFGzSB1OS-mobile.webp" + }, + { + "name": "jYxEXspWH5g6eTTVqK72c-desktop.webp", + "path": "image/jYxEXspWH5g6eTTVqK72c-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/a13572b4-2e22-4d76-8826-64a8e6ae4e13/jYxEXspWH5g6eTTVqK72c-desktop.webp" + }, + { + "name": "jYxEXspWH5g6eTTVqK72c-mobile.webp", + "path": "image/jYxEXspWH5g6eTTVqK72c-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/55f171fa-585f-4a94-bfeb-d2bad2f7ee39/jYxEXspWH5g6eTTVqK72c-mobile.webp" + }, + { + "name": "K0wY911212dinYA3AFB_f-desktop.webp", + "path": "image/K0wY911212dinYA3AFB_f-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/31ea22bf-9ce1-4dc6-b901-caaec86c35c4/K0wY911212dinYA3AFB_f-desktop.webp" + }, + { + "name": "K0wY911212dinYA3AFB_f-mobile.webp", + "path": "image/K0wY911212dinYA3AFB_f-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/ea061aa6-7e5e-447c-bec8-be1d927cc578/K0wY911212dinYA3AFB_f-mobile.webp" + }, + { + "name": "l4qsUEw2JiclGAkkrXp9g-desktop.webp", + "path": "image/l4qsUEw2JiclGAkkrXp9g-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/05088eb1-44bc-44d6-9e67-08dd6bca00ae/l4qsUEw2JiclGAkkrXp9g-desktop.webp" + }, + { + "name": "l4qsUEw2JiclGAkkrXp9g-mobile.webp", + "path": "image/l4qsUEw2JiclGAkkrXp9g-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/cf411a77-b7fc-4ac7-a98d-f9f89bc27e85/l4qsUEw2JiclGAkkrXp9g-mobile.webp" + }, + { + "name": "M9QlgVKIEfCdY3g4F_tRZ-desktop.webp", + "path": "image/M9QlgVKIEfCdY3g4F_tRZ-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/dfa58064-c17b-453a-b0a1-613109757844/M9QlgVKIEfCdY3g4F_tRZ-desktop.webp" + }, + { + "name": "M9QlgVKIEfCdY3g4F_tRZ-mobile.webp", + "path": "image/M9QlgVKIEfCdY3g4F_tRZ-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/4be366ca-1a39-46e9-adf2-d1d28fd83961/M9QlgVKIEfCdY3g4F_tRZ-mobile.webp" + }, + { + "name": "mtQsaKtQnhxIYVIooCkiQ-desktop.webp", + "path": "image/mtQsaKtQnhxIYVIooCkiQ-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/5015a484-ac9a-485f-9fb0-a29e3824a6ce/mtQsaKtQnhxIYVIooCkiQ-desktop.webp" + }, + { + "name": "mtQsaKtQnhxIYVIooCkiQ-mobile.webp", + "path": "image/mtQsaKtQnhxIYVIooCkiQ-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/10ddd132-5b85-4a8f-b129-56a10540fc8c/mtQsaKtQnhxIYVIooCkiQ-mobile.webp" + }, + { + "name": "NBPAqjPXn7GQmYTDBI5hu-desktop.webp", + "path": "image/NBPAqjPXn7GQmYTDBI5hu-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/fd8cfea3-5cf2-489e-9f21-e7985335dc98/NBPAqjPXn7GQmYTDBI5hu-desktop.webp" + }, + { + "name": "NBPAqjPXn7GQmYTDBI5hu-mobile.webp", + "path": "image/NBPAqjPXn7GQmYTDBI5hu-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/f5b02049-95db-4e1b-8bfd-b1a4c431ee49/NBPAqjPXn7GQmYTDBI5hu-mobile.webp" + }, + { + "name": "NyPGo-1AtfNm5wkAq7Om6-mobile.webp", + "path": "image/NyPGo-1AtfNm5wkAq7Om6-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/b7361eb8-37e8-4baa-a6d6-01b6131dd788/NyPGo-1AtfNm5wkAq7Om6-mobile.webp" + }, + { + "name": "OsMY3AYPyGC_CoN1xUjOn-desktop.webp", + "path": "image/OsMY3AYPyGC_CoN1xUjOn-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/509a3603-5d11-4857-baa5-d439da43825a/OsMY3AYPyGC_CoN1xUjOn-desktop.webp" + }, + { + "name": "OsMY3AYPyGC_CoN1xUjOn-mobile.webp", + "path": "image/OsMY3AYPyGC_CoN1xUjOn-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/cbe9798a-4cf0-45f7-b041-70269490128b/OsMY3AYPyGC_CoN1xUjOn-mobile.webp" + }, + { + "name": "pps1ZgzJxDb4VZxEvtZeu-desktop.webp", + "path": "image/pps1ZgzJxDb4VZxEvtZeu-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/500fa1f3-d412-4f48-bc5f-465b91149c6e/pps1ZgzJxDb4VZxEvtZeu-desktop.webp" + }, + { + "name": "pps1ZgzJxDb4VZxEvtZeu-mobile.webp", + "path": "image/pps1ZgzJxDb4VZxEvtZeu-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/19c2b1a6-8c9b-4be9-981c-72d7a3ab6e38/pps1ZgzJxDb4VZxEvtZeu-mobile.webp" + }, + { + "name": "r_gBF0FuFpFPfSENHc4XI-desktop.webp", + "path": "image/r_gBF0FuFpFPfSENHc4XI-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/2e4ab451-df52-4f04-8af0-db2e789e58bb/r_gBF0FuFpFPfSENHc4XI-desktop.webp" + }, + { + "name": "r_gBF0FuFpFPfSENHc4XI-mobile.webp", + "path": "image/r_gBF0FuFpFPfSENHc4XI-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/1e2e8884-9da3-4610-9c57-8925740d4128/r_gBF0FuFpFPfSENHc4XI-mobile.webp" + }, + { + "name": "SQqSobKRg3ShvgPw_H41h-desktop.webp", + "path": "image/SQqSobKRg3ShvgPw_H41h-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/4360cb84-f82a-4a68-afd3-65decd912f30/SQqSobKRg3ShvgPw_H41h-desktop.webp" + }, + { + "name": "SQqSobKRg3ShvgPw_H41h-mobile.webp", + "path": "image/SQqSobKRg3ShvgPw_H41h-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/c808dff8-2e8a-4358-af03-4a838c9a6d6c/SQqSobKRg3ShvgPw_H41h-mobile.webp" + }, + { + "name": "TDQReg1lQ73s39crXW0ra-desktop.webp", + "path": "image/TDQReg1lQ73s39crXW0ra-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/04d15638-31ed-440f-9fa0-bb30d71bbc59/TDQReg1lQ73s39crXW0ra-desktop.webp" + }, + { + "name": "TDQReg1lQ73s39crXW0ra-mobile.webp", + "path": "image/TDQReg1lQ73s39crXW0ra-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/da174313-329c-4156-88e2-fc929325dfff/TDQReg1lQ73s39crXW0ra-mobile.webp" + }, + { + "name": "TTur8BttDlAS9UgZVe3M8-desktop.webp", + "path": "image/TTur8BttDlAS9UgZVe3M8-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/32fe30ab-0d7b-4ada-8d5d-993baf23545c/TTur8BttDlAS9UgZVe3M8-desktop.webp" + }, + { + "name": "TTur8BttDlAS9UgZVe3M8-mobile.webp", + "path": "image/TTur8BttDlAS9UgZVe3M8-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/85c5ecb0-2fb5-4431-af11-0c851e52de4e/TTur8BttDlAS9UgZVe3M8-mobile.webp" + }, + { + "name": "TWdNTZZbTOhFTNJGGPDyG-desktop.webp", + "path": "image/TWdNTZZbTOhFTNJGGPDyG-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d606d946-fdf9-41b9-b0bd-1421b2ec6843/TWdNTZZbTOhFTNJGGPDyG-desktop.webp" + }, + { + "name": "TWdNTZZbTOhFTNJGGPDyG-mobile.webp", + "path": "image/TWdNTZZbTOhFTNJGGPDyG-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/46859e6a-ebfc-4124-adfe-320953256fe5/TWdNTZZbTOhFTNJGGPDyG-mobile.webp" + }, + { + "name": "TXknK9CSRSxwvM2hPW6BO-desktop.webp", + "path": "image/TXknK9CSRSxwvM2hPW6BO-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/af38b038-0d38-4222-be29-09cb81054ce7/TXknK9CSRSxwvM2hPW6BO-desktop.webp" + }, + { + "name": "TXknK9CSRSxwvM2hPW6BO-mobile.webp", + "path": "image/TXknK9CSRSxwvM2hPW6BO-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/eab1c384-aafa-438f-ac0f-003ddd51c9a5/TXknK9CSRSxwvM2hPW6BO-mobile.webp" + }, + { + "name": "U7rePDZq5E59z-Eo9tLBe-desktop.webp", + "path": "image/U7rePDZq5E59z-Eo9tLBe-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/a2858ed9-8bb6-47cd-9e1b-831928a0389f/U7rePDZq5E59z-Eo9tLBe-desktop.webp" + }, + { + "name": "U7rePDZq5E59z-Eo9tLBe-mobile.webp", + "path": "image/U7rePDZq5E59z-Eo9tLBe-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/1f4a189d-d312-46ef-a68c-c0c7261860d0/U7rePDZq5E59z-Eo9tLBe-mobile.webp" + }, + { + "name": "uDxAalFV0qRv_RrW9flM8-mobile.webp", + "path": "image/uDxAalFV0qRv_RrW9flM8-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/5f006c3c-47f5-4e48-8bdd-7fbe005bf810/uDxAalFV0qRv_RrW9flM8-mobile.webp" + }, + { + "name": "uE2QwpbcXyBWxVYqCWQQT-desktop.webp", + "path": "image/uE2QwpbcXyBWxVYqCWQQT-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/edf73617-214b-44df-960a-dd68f0bad97a/uE2QwpbcXyBWxVYqCWQQT-desktop.webp" + }, + { + "name": "uE2QwpbcXyBWxVYqCWQQT-mobile.webp", + "path": "image/uE2QwpbcXyBWxVYqCWQQT-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/d7f5d738-b18f-44c8-92bb-8e526f47d9ee/uE2QwpbcXyBWxVYqCWQQT-mobile.webp" + }, + { + "name": "v7Ac2xQvTiJy-HYh1AxF4-desktop.webp", + "path": "image/v7Ac2xQvTiJy-HYh1AxF4-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/3e1b8dd9-2bf5-4daf-9dd9-dd687e9b2f2c/v7Ac2xQvTiJy-HYh1AxF4-desktop.webp" + }, + { + "name": "v7Ac2xQvTiJy-HYh1AxF4-mobile.webp", + "path": "image/v7Ac2xQvTiJy-HYh1AxF4-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/44c78026-f802-46f3-8ec2-271f0f001f7a/v7Ac2xQvTiJy-HYh1AxF4-mobile.webp" + }, + { + "name": "wh79hF4HTZMEFtYc-OfZg-mobile.webp", + "path": "image/wh79hF4HTZMEFtYc-OfZg-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/b4b4378e-76ad-4b15-84cf-00178553b3d4/wh79hF4HTZMEFtYc-OfZg-mobile.webp" + }, + { + "name": "x0_-siY2V8IehBzo4_uph-desktop.webp", + "path": "image/x0_-siY2V8IehBzo4_uph-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/7f82bcea-e7c6-4cde-9701-8d3afe49c0f8/x0_-siY2V8IehBzo4_uph-desktop.webp" + }, + { + "name": "x0_-siY2V8IehBzo4_uph-mobile.webp", + "path": "image/x0_-siY2V8IehBzo4_uph-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/c4b23ebc-3915-4102-a25c-4e2f6da0d097/x0_-siY2V8IehBzo4_uph-mobile.webp" + }, + { + "name": "y78xZ2axTOjz87gRKjVAf-desktop.webp", + "path": "image/y78xZ2axTOjz87gRKjVAf-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/180d0c29-c93e-4bbe-b399-7e6a34fbeb49/y78xZ2axTOjz87gRKjVAf-desktop.webp" + }, + { + "name": "y78xZ2axTOjz87gRKjVAf-mobile.webp", + "path": "image/y78xZ2axTOjz87gRKjVAf-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/71b5091a-ebcd-4d4c-9aae-1b7e548051fc/y78xZ2axTOjz87gRKjVAf-mobile.webp" + }, + { + "name": "YdCBnK-bWxlyHjwsk4Qie-desktop.webp", + "path": "image/YdCBnK-bWxlyHjwsk4Qie-desktop.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/bf6f2e11-b328-4da1-bda7-81af2336d03f/YdCBnK-bWxlyHjwsk4Qie-desktop.webp" + }, + { + "name": "YdCBnK-bWxlyHjwsk4Qie-mobile.webp", + "path": "image/YdCBnK-bWxlyHjwsk4Qie-mobile.webp", + "downloadUrl": "https://cld-dkr-makuro-seafile.wibudev.com/seafhttp/files/9d7e5232-3b30-4ead-9482-1fed3a86245a/YdCBnK-bWxlyHjwsk4Qie-mobile.webp" } ] diff --git a/prisma/data/inovasi/ajukan-ide/ajukan-ide.json b/prisma/data/inovasi/ajukan-ide/ajukan-ide.json new file mode 100644 index 00000000..e2418571 --- /dev/null +++ b/prisma/data/inovasi/ajukan-ide/ajukan-ide.json @@ -0,0 +1,38 @@ +[ + { + "id": "cmkm710n3000xvnsw1ni0vlam", + "name": "I Made Rudi", + "alamat": "Darmasaba, Abiansemal, Badung, Bali", + "namaIde": "Program Bersama Jaga Rabies (BAJRA)", + "deskripsi": "Inovasi berbasis komunitas yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan, serta koordinasi lintas sektor untuk penanggulangan rabies secara efektif di Desa Darmasaba.", + "masalah": "Tingginya kasus rabies dan rendahnya pelaporan serta koordinasi penanganan antar warga sehingga perlunya pendekatan komunitas yang terstruktur.", + "benefit": "Meningkatkan kesadaran vaksinasi, respons cepat terhadap kasus gigitan, serta memperkuat sistem kesiapsiagaan kesehatan hewan dan manusia di komunitas." + }, + { + "id": "cmkm710n3000xvnsw1ni1wmbn", + "name": "I Made Rudja", + "alamat": "Darmasaba, Abiansemal, Badung, Bali", + "namaIde": "Program LESTARI – Ekowisata dan Edukasi Lingkungan", + "deskripsi": "Inovasi pengembangan ekowisata berbasis persawahan yang menggabungkan edukasi lingkungan dan potensi desa untuk menarik wisatawan serta meningkatkan kesejahteraan masyarakat.", + "masalah": "Kurangnya sarana edukasi lingkungan dan kurang optimalnya pemanfaatan potensi lahan persawahan sebagai sumber pendapatan masyarakat.", + "benefit": "Menambah peluang ekonomi desa, meningkatkan kesadaran lingkungan, dan memperkuat daya tarik wisata lokal Desa Darmasaba." + }, + { + "id": "cmkm710n3000xvnsw1ni2xnco", + "name": "I Wayan Sumi", + "alamat": "Darmasaba, Abiansemal, Badung, Bali", + "namaIde": "Darmasaba Digital Project", + "deskripsi": "Ide inovatif untuk mengembangkan satu platform digital terpadu bagi layanan pemerintahan desa serta administrasi kerja desa yang lebih cepat dan transparan.", + "masalah": "Proses administrasi dan pelayanan publik desa yang masih belum terintegrasi secara digital sehingga kurang efisien.", + "benefit": "Meningkatkan efisiensi kerja perangkat desa, transparansi data, dan kecepatan layanan kepada masyarakat." + }, + { + "id": "cmkm710n3000xvnsw1ni3yodp", + "name": "I Ketut Surya", + "alamat": "Darmasaba, Abiansemal, Badung, Bali", + "namaIde": "Program CINTA – Cara Indah Tangani Sampah", + "deskripsi": "Inovasi pengelolaan sampah melalui mekanisme pilah, kompos, dan bursa sampah (BARES) untuk mengurangi volume sampah di desa dan meningkatkan nilai ekonomi dari sampah yang ternyata bernilai jual.", + "masalah": "Volume sampah meningkat yang berdampak pada lingkungan, kurangnya sistem pengelolaan sampah berbasis komunitas.", + "benefit": "Lingkungan desa menjadi bersih, sampah bernilai ekonomi dan peningkatan keterlibatan masyarakat dalam pengelolaan sampah." + } +] diff --git a/prisma/data/inovasi/desa-digital/desa-digital.json b/prisma/data/inovasi/desa-digital/desa-digital.json new file mode 100644 index 00000000..8ec4f5ee --- /dev/null +++ b/prisma/data/inovasi/desa-digital/desa-digital.json @@ -0,0 +1,32 @@ +[ + { + "id": "cmkkshcox000504l88lp54coc", + "name": "Darmasaba Digital App", + "deskripsi": "

Aplikasi digital desa yang dikembangkan oleh Pemerintah Desa Darmasaba pada tahun 2024 untuk mempermudah pelayanan publik dan informasi pemerintahan berbasis digital.

", + "imageName": "r_gBF0FuFpFPfSENHc4XI-mobile.webp" + }, + { + "id": "cmkkshln8000604l8c9b5b4il", + "name": "D’DAMART (Darmasaba Digital Market)", + "deskripsi": "

Sistem pasar UMKM digital berbasis website yang dikembangkan untuk meningkatkan akses pasar dan pemasaran produk UMKM Desa Darmasaba melalui platform digital.

", + "imageName": "uE2QwpbcXyBWxVYqCWQQT-mobile.webp" + }, + { + "id": "cmkm1a1g80007vnsw8ejmj816", + "name": "Media Aspirasi dan Pengaduan Warga", + "deskripsi": "

Media aspirasi dan pengaduan warga disediakan sebagai wadah partisipasi masyarakat dalam menyampaikan saran, masukan, maupun keluhan secara transparan dan terstruktur. Fitur ini memperkuat komunikasi dua arah antara pemerintah desa dan masyarakat, sehingga setiap aspirasi dapat ditindaklanjuti secara lebih cepat dan akuntabel.

", + "imageName": "c7xWNyoYp8Cak28NG5NoG-mobile.webp" + }, + { + "id": "cmkm0w0s50003vnswmwpnqsi5", + "name": "Website Desa Resmi", + "deskripsi": "

Website Desa Darmasaba berfungsi sebagai sarana utama penyampaian informasi resmi kepada masyarakat. Melalui website ini, pemerintah desa menghadirkan keterbukaan informasi publik, mempermudah akses warga terhadap berita, pengumuman, serta agenda kegiatan desa, sekaligus menjadi pusat data dan referensi terkait profil dan struktur pemerintahan desa.

", + "imageName": "kN09yF3sahmy-d5EaeGqA-mobile.webp" + }, + { + "id": "cmkm1c8wx000avnswksc56orq", + "name": "Publikasi Kegiatan Desa Secara Digital", + "deskripsi": "

Publikasi kegiatan desa secara digital bertujuan untuk mendokumentasikan dan menyebarluaskan berbagai aktivitas serta program kerja pemerintah desa. Melalui artikel dan dokumentasi foto, masyarakat dapat mengetahui perkembangan kegiatan desa secara terbuka, sekaligus meningkatkan kepercayaan publik terhadap pelaksanaan program desa.

", + "imageName": "h_Gd0SoeIJVTi_5TWUO-P-mobile.webp" + } +] diff --git a/prisma/data/inovasi/info-teknologi/info-teknologi.json b/prisma/data/inovasi/info-teknologi/info-teknologi.json new file mode 100644 index 00000000..aac5d02f --- /dev/null +++ b/prisma/data/inovasi/info-teknologi/info-teknologi.json @@ -0,0 +1,26 @@ +[ + { + "id": "cmkm2xlqr000mvnswdaymiho6", + "name": "Darmasaba Digital App", + "deskripsi": "

Aplikasi layanan desa berbasis teknologi untuk transparansi informasi dan layanan publik di Desa Darmasaba yang membantu warga mendapatkan informasi administratif, berita desa, dan pelayanan digital lainnya secara cepat dan mudah.

", + "imageName": "xVrwJgdwtcoABPU6DB__Y-mobile.webp" + }, + { + "id": "cmkm3b1fw000pvnswpr7hgzhp", + "name": "Program Digitalisasi Desa", + "deskripsi": "

Program kerja sama Desa Darmasaba bersama PT. Bali Interaktif Perkasa untuk memperkuat kapasitas pemanfaatan teknologi informasi dan komunikasi dalam administrasi desa, pelayanan publik, serta pemberdayaan digital masyarakat.

", + "imageName": "JjUDrfqxuEMYSAza-s7A8-mobile.webp" + }, + { + "id": "cmkm3fwmq000tvnswejmhm7yc", + "name": "Pengembangan Sistem Informasi Desa", + "deskripsi": "

Inisiatif pengembangan Sistem Informasi Desa yang mendukung pengelolaan data desa secara digital, termasuk data publik, laporan, dan statistik warga, sebagai bagian dari peningkatan kapabilitas teknologi informasi desa.

", + "imageName": "42RCCpBZla4ZWxXcwx7kG-mobile.webp" + }, + { + "id": "cmkm3hjp6000wvnswkuylnf53", + "name": "Pelayanan Kependudukan Berbasis Digital", + "deskripsi": "

Program untuk menyediakan layanan kependudukan secara digital, termasuk integrasi sistem administrasi kependudukan desa dengan sistem nasional, guna mempercepat layanan e-KTP, kartu keluarga, dan berkas kependudukan lainnya.

", + "imageName": "TrbkwnYM5rKZeHlISHCX4-mobile.webp" + } +] diff --git a/prisma/data/inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json b/prisma/data/inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json new file mode 100644 index 00000000..bb7ddccc --- /dev/null +++ b/prisma/data/inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json @@ -0,0 +1,26 @@ +[ + { + "id": "cmklzhfjb0000vnswml6z7xnj", + "name": "Bersama Jaga Rabies (BAJRA)", + "tahun": 2025, + "slug": "Program kolaborasi penanggulangan rabies", + "deskripsi": "

Inovasi BAJRA (Bersama Jaga Rabies) merupakan program kolaboratif Desa Darmasaba bersama berbagai pihak, termasuk Sahabat Anti Rabies Indonesia dan Dinas Pertanian dan Pangan Kabupaten Badung, yang fokus pada edukasi publik, pelaporan cepat, serta strategis vaksinasi anjing guna upaya eliminasi rabies. Inovasi ini diangkat hingga tingkat internasional di Konferensi Rabies in Borneo 2025.

", + "kolaborator": "Sahabat Anti Rabies Indonesia, Bidang Kesehatan Hewan Dinas Pertanian dan Pangan Kabupaten Badung" + }, + { + "id": "cmklzhfjb0000vnswml6z8yok", + "name": "Inovasi Eco-Enzyme dan Optimalisasi Tata Kelola BUMDes", + "tahun": 2025, + "slug": "Kolaborasi inovasi ekonomi & lingkungan", + "deskripsi": "

Program kerja sama Desa Darmasaba dengan Universitas Warmadewa dan BUMDes setempat yang berfokus pada pelatihan produksi eco-enzyme ramah lingkungan dan pengembangan tata kelola BUMDes modern untuk meningkatkan kesejahteraan masyarakat berbasis usaha hijau dan ekonomi kreatif.

", + "kolaborator": "Universitas Warmadewa, Pengurus BUMDes Darmasaba" + }, + { + "id": "cmklzhfjb0000vnswml6z9zpl", + "name": "Ekowisata dan Kuliner Berkelanjutan", + "tahun": 2025, + "slug": "Kolaborasi pengembangan ekowisata dan kuliner berbasis budaya", + "deskripsi": "

Kegiatan yang melibatkan desa adat, BUMDes, UMKM, akademisi, dan pemerintah desa untuk mengembangkan ekowisata dan kuliner lokal berbasis keberlanjutan lingkungan serta potensi budaya setempat, sebagai wujud sinergi inovatif di Darmasaba.

", + "kolaborator": "Desa Adat Darmasaba, BUMDes, UMKM lokal, Akademisi" + } +] diff --git a/prisma/data/inovasi/kolaborasi-inovasi/mitra-kolaborasi.json b/prisma/data/inovasi/kolaborasi-inovasi/mitra-kolaborasi.json new file mode 100644 index 00000000..91f49e24 --- /dev/null +++ b/prisma/data/inovasi/kolaborasi-inovasi/mitra-kolaborasi.json @@ -0,0 +1,17 @@ +[ + { + "id": "cmkm1ziyi000dvnsweg8lp3f7", + "name": "TP Posyandu Bali", + "imageName": "qJFWokQLCaO60j0XJU_33-mobile.webp" + }, + { + "id": "cmkm1ziyi000dvnsweg8lq4g8", + "name": "BRI Peduli", + "imageName": "nzLJoEAfl7HkpUcYa8Y1E-mobile.webp" + }, + { + "id": "cmkm1ziyi000dvnsweg8lr5h9", + "name": "Universitas Warmadewa (KKN-PMM)", + "imageName": "JFd5C2FoaZcgDQUmvp-AO-mobile.webp" + } +] diff --git a/prisma/data/inovasi/layanan-online-desa/administrasi-online.json b/prisma/data/inovasi/layanan-online-desa/administrasi-online.json new file mode 100644 index 00000000..e5a72af6 --- /dev/null +++ b/prisma/data/inovasi/layanan-online-desa/administrasi-online.json @@ -0,0 +1,44 @@ +[ + { + "id": "cmkkt91ma000004lb4dpq7ll1", + "name": "Surat Keterangan Domisili Organisasi", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "nomorTelepon": "0361-9876543", + "jenisLayananId": "dnllt91ma000004lb4dpq7ll1" + }, + { + "id": "cmkkt91ma000004lb4dpq8mm2", + "name": "Surat Keterangan Penghasilan", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "nomorTelepon": "0361-9776543", + "jenisLayananId": "dnllt91ma000004lb4dpq8mm2" + }, + { + "id": "cmkkt91ma000004lb4dpq9nn3", + "name": "Surat Keterangan Tidak Mampu", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "nomorTelepon": "0361-9676543", + "jenisLayananId": "dnllt91ma000004lb4dpq9nn3" + }, + { + "id": "cmkkt91ma000004lb4dpq0oo4", + "name": "Surat Keterangan Kelahiran", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "nomorTelepon": "0361-9576543", + "jenisLayananId": "dnllt91ma000004lb4dpq0oo4" + }, + { + "id": "cmkkt91ma000004lb4dpq1pp5", + "name": "Surat Keterangan Usaha", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "nomorTelepon": "0361-9476543", + "jenisLayananId": "dnllt91ma000004lb4dpq1pp5" + }, + { + "id": "cmkkt91ma000004lb4dpq2qq6", + "name": "Perizinan Berusaha OSS", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "nomorTelepon": "0361-9376543", + "jenisLayananId": "dnllt91ma000004lb4dpq2qq6" + } +] diff --git a/prisma/data/inovasi/layanan-online-desa/jenis-layanan.json b/prisma/data/inovasi/layanan-online-desa/jenis-layanan.json new file mode 100644 index 00000000..acf3ce17 --- /dev/null +++ b/prisma/data/inovasi/layanan-online-desa/jenis-layanan.json @@ -0,0 +1,32 @@ +[ + { + "id": "dnllt91ma000004lb4dpq7ll1", + "nama": "Surat Keterangan Domisili Organisasi", + "deskripsi": "Administrasi Online Surat Keterangan Domisili Organisasi" + }, + { + "id": "dnllt91ma000004lb4dpq8mm2", + "nama": "Surat Keterangan Penghasilan", + "deskripsi": "Administrasi Online Surat Keterangan Penghasilan" + }, + { + "id": "dnllt91ma000004lb4dpq9nn3", + "nama": "Surat Keterangan Tidak Mampu", + "deskripsi": "Administrasi Online Surat Keterangan Tidak Mampu" + }, + { + "id": "dnllt91ma000004lb4dpq0oo4", + "nama": "Surat Keterangan Kelahiran", + "deskripsi": "Administrasi Online Surat Keterangan Kelahiran" + }, + { + "id": "dnllt91ma000004lb4dpq1pp5", + "nama": "Surat Keterangan Usaha", + "deskripsi": "Administrasi Online Surat Keterangan Usaha" + }, + { + "id": "dnllt91ma000004lb4dpq2qq6", + "nama": "Perizinan Berusaha OSS", + "deskripsi": "Administrasi Online Perizinan Usaha Online Single Submission (OSS)" + } +] \ No newline at end of file diff --git a/prisma/data/inovasi/layanan-online-desa/jenis-pengaduan.json b/prisma/data/inovasi/layanan-online-desa/jenis-pengaduan.json new file mode 100644 index 00000000..31292ae9 --- /dev/null +++ b/prisma/data/inovasi/layanan-online-desa/jenis-pengaduan.json @@ -0,0 +1,26 @@ +[ + { + "id": "eommt91ma000004lb4dpq7ll1", + "nama": "Lingkungan" + }, + { + "id": "eommt91ma000004lb4dpq8mm2", + "nama": "Ketertiban Umum" + }, + { + "id": "eommt91ma000004lb4dpq9nn3", + "nama": "Kesehatan Masyarakat" + }, + { + "id": "eommt91ma000004lb4dpq0oo4", + "nama": "Pendidikan" + }, + { + "id": "eommt91ma000004lb4dpq1pp5", + "nama": "Pembangunan Infrastuktur" + }, + { + "id": "eommt91ma000004lb4dpq2qq6", + "nama": "Perizinan Berusaha" + } +] \ No newline at end of file diff --git a/prisma/data/inovasi/layanan-online-desa/pengaduan-masyarakat.json b/prisma/data/inovasi/layanan-online-desa/pengaduan-masyarakat.json new file mode 100644 index 00000000..7c915a85 --- /dev/null +++ b/prisma/data/inovasi/layanan-online-desa/pengaduan-masyarakat.json @@ -0,0 +1,38 @@ +[ + { + "id": "cmkkrxmub0004vni41cwyhid4", + "name": "Nyoman Putra", + "email": "nyoman.putra@example.com", + "nomorTelepon": "081234567890", + "nik": "3175011234567890", + "judulPengaduan": "Permintaan Pemasangan Spanduk Larangan Bagi Hewan", + "deskripsiPengaduan": "

Permintaan Pemasangan Spanduk Larangan Bagi Hewan

", + "lokasiKejadian": "Banjar Darmasaba Tengah", + "jenisPengaduanId": "eommt91ma000004lb4dpq7ll1", + "imageName": "gyNi4s8TnK2UrViU-gN2C-mobile.webp" + }, + { + "id": "cmkkrxmub0004vni41cwyhid5", + "name": "I Made Sari", + "email": "imade.sari@example.com", + "nomorTelepon": "087654321098", + "nik": "3175010987654321", + "judulPengaduan": "Laporan Anjing Liar Sering Menyerang Warga", + "deskripsiPengaduan": "

Laporan Anjing Liar Sering Menyerang Warga

", + "lokasiKejadian": "Jl. Raya Darmasaba", + "jenisPengaduanId": "eommt91ma000004lb4dpq8mm2", + "imageName": "SQqSobKRg3ShvgPw_H41h-mobile.webp" + }, + { + "id": "cmkkrxmub0004vni41cwyhid6", + "name": "Ketut Widi", + "email": "ketut.widi@example.com", + "nomorTelepon": "085612347890", + "nik": "3175011122334455", + "judulPengaduan": "Pengelolaan Sampah Rumah Tangga Belum Efektif", + "deskripsiPengaduan": "

Pengelolaan Sampah Rumah Tangga Belum Efektif

", + "lokasiKejadian": "Banjar Bucu", + "jenisPengaduanId": "eommt91ma000004lb4dpq7ll1", + "imageName": "y78xZ2axTOjz87gRKjVAf-mobile.webp" + } +] diff --git a/prisma/data/inovasi/program-kreatif-desa/program-kreatif-desa.json b/prisma/data/inovasi/program-kreatif-desa/program-kreatif-desa.json new file mode 100644 index 00000000..45632974 --- /dev/null +++ b/prisma/data/inovasi/program-kreatif-desa/program-kreatif-desa.json @@ -0,0 +1,44 @@ +[ + { + "id": "cmkkyux9x000104jo4gwi9frv", + "name": "Pelatihan Pembuatan Eco-Enzyme", + "slug": "Pelatihan pembuatan cairan eco-enzyme dari limbah organik rumah tangga", + "deskripsi": "

Program ini mengajarkan masyarakat Desa Darmasaba cara membuat eco-enzyme dari limbah organik yang dapat digunakan sebagai pembersih alami, pupuk organik, dan produk ramah lingkungan untuk meningkatkan nilai tambah ekonomi lokal.

", + "icon": "lingkunganSehat" + }, + { + "id": "cmkkyux9x000104jo4gwi0gsw", + "name": "Green House dan Biopori Berkelanjutan", + "slug": "Perancangan rumah kaca dan lubang biopori di TPS3R", + "deskripsi": "

Program perancangan dan pembangunan green house di kawasan TPS3R Pudak Mesari serta pembuatan lubang biopori untuk pengelolaan sampah organik dan konservasi air yang ramah lingkungan sebagai bagian dari pengembangan ekonomi hijau desa.

", + "icon": "lingkunganSehat" + }, + { + "id": "cmkkyux9x000104jo4gwi1htx", + "name": "Ekowisata dan Eco-Kuliner Desa", + "slug": "Pengembangan paket ekowisata dan kuliner berbasis potensi lokal", + "deskripsi": "

Kolaborasi antara desa, BUMDes, dan akademisi untuk mengembangkan potensi ekowisata berkelanjutan serta produk kuliner khas desa dengan pendekatan pemasaran digital untuk memperkuat ekonomi kreatif dan pariwisata lokal.

", + "icon": "ekowisata" + }, + { + "id": "cmkkyux9x000104jo4gwi2iuy", + "name": "Sosialisasi Kewirausahaan", + "slug": "Sosialisasi kewirausahaan bagi masyarakat desa", + "deskripsi": "

Program ini memberikan pengetahuan dan motivasi kepada warga Darmasaba tentang kewirausahaan, pemasaran digital, pencatatan usaha, dan strategi pengembangan UMKM agar produk lokal lebih produktif dan bernilai ekonomi.

", + "icon": "ekonomi" + }, + { + "id": "cmkkyux9x000104jo4gwi3jvz", + "name": "E-Book Kuliner Lokal", + "slug": "Publikasi e-book tentang kuliner khas Desa Darmasaba", + "deskripsi": "

Produksi dan distribusi e-book yang mengangkat resep dan cerita kuliner khas Desa Darmasaba sebagai media edukatif dan promosi budaya pangan lokal yang kreatif serta bisa diperluas untuk pemasaran UMKM desa.

", + "icon": "book" + }, + { + "id": "cmkkyux9x000104jo4gwi4kwa", + "name": "Tes Kompetensi", + "slug": "Program pemetaan minat dan keterampilan masyarakat untuk mendukung pengembangan potensi dan kesiapan kerja.", + "deskripsi": "

Tes Kompetensi merupakan program untuk membantu masyarakat, khususnya generasi muda, dalam mengenali minat, bakat, dan keterampilan yang dimiliki. Hasil tes dapat menjadi dasar dalam perencanaan pelatihan, pengembangan potensi diri, serta kesiapan menghadapi dunia kerja dan wirausaha.

", + "icon": "layananPublik" + } +] diff --git a/prisma/data/keamanan/keamanan-lingkungan/keamanan-lingkungan.json b/prisma/data/keamanan/keamanan-lingkungan/keamanan-lingkungan.json new file mode 100644 index 00000000..472d4c03 --- /dev/null +++ b/prisma/data/keamanan/keamanan-lingkungan/keamanan-lingkungan.json @@ -0,0 +1,20 @@ +[ + { + "id": "cmkc2tcs00002vnt9c0ssj05n", + "name": "Sosialisasi dan Pembinaan Keamanan Lingkungan Desa Darmasaba", + "deskripsi": "

Pemerintah Desa Darmasaba melaksanakan Sosialisasi dan Pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai di Wantilan Perum Darmasaba Permai, Desa Darmasaba. Kegiatan ini melibatkan Perbekel Darmasaba, Bhabinkamtibmas, Babinsa, anggota BPD, LPM Desa, KBD dan KBA untuk mengajak warga berperan aktif dalam menjaga keamanan lingkungan, serta mendukung pemasangan lampu penerangan jalan guna mencegah kriminalitas dan kecelakaan di wilayah lingkungan.

", + "imageName": "K0wY911212dinYA3AFB_f-mobile.webp" + }, + { + "id": "cmkc2xmdh0005vnt9ri6f4nk8", + "name": "Sinergi Aparat dan Masyarakat untuk Keamanan Lingkungan", + "deskripsi": "

Desa Darmasaba bersama aparat seperti Polres Badung dan elemen masyarakat berkomitmen menjalin sinergi untuk menciptakan keamanan dan ketertiban lingkungan yang kondusif, memperkuat kepedulian serta tindakan nyata dalam menjaga situasi kamtibmas desa.

", + "imageName": "x0_-siY2V8IehBzo4_uph-mobile.webp" + }, + { + "id": "cmkc36qbl0008vnt9odvekex6", + "name": "Peran Sistem Keamanan Lingkungan (Siskamling) dan Pecalang di Bali", + "deskripsi": "

Sistem keamanan lingkungan (Siskamling) di Bali termasuk di Desa Darmasaba melibatkan kolaborasi antara pemerintah desa, satlinmas, dan pecalang sebagai pranata adat Bali. Sinergi ini penting untuk menjaga ketertiban masyarakat serta harmoni sosial berdasarkan kearifan lokal seperti Tri Hita Karana, meskipun perlu pembinaan dan koordinasi terus menerus dari desa dan aparat terkait.

", + "imageName": "TXknK9CSRSxwvM2hPW6BO-mobile.webp" + } +] diff --git a/prisma/data/keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json b/prisma/data/keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json new file mode 100644 index 00000000..989d7ab9 --- /dev/null +++ b/prisma/data/keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json @@ -0,0 +1,20 @@ +[ + { + "id": "keamanan-polisi", + "nama": "Kepolisian", + "icon": "keamanan", + "kategoriId": "item-polisi" + }, + { + "id": "keamanan-damkar", + "nama": "Pemadam Kebakaran", + "icon": "pemadam", + "kategoriId": "item-damkar" + }, + { + "id": "keamanan-sar", + "nama": "SAR & Evakuasi", + "icon": "sar", + "kategoriId": "item-sar" + } +] diff --git a/prisma/data/keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json b/prisma/data/keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json new file mode 100644 index 00000000..b3e18d3b --- /dev/null +++ b/prisma/data/keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json @@ -0,0 +1,17 @@ +[ + { + "id": "map-polisi-1", + "kontakDaruratId": "keamanan-polisi", + "kontakItemId": "item-polsek-darmasaba" + }, + { + "id": "map-polisi-2", + "kontakDaruratId": "keamanan-polisi", + "kontakItemId": "item-polres-badung" + }, + { + "id": "map-damkar-1", + "kontakDaruratId": "keamanan-damkar", + "kontakItemId": "item-damkar-badung" + } +] diff --git a/prisma/data/keamanan/kontak-darurat-keamanan/kontakItem.json b/prisma/data/keamanan/kontak-darurat-keamanan/kontakItem.json new file mode 100644 index 00000000..1c00152d --- /dev/null +++ b/prisma/data/keamanan/kontak-darurat-keamanan/kontakItem.json @@ -0,0 +1,45 @@ +[ + { + "id": "item-polisi", + "nama": "Polisi", + "nomorTelepon": "110", + "icon": "keamanan" + }, + { + "id": "item-damkar", + "nama": "Pemadam Kebakaran", + "nomorTelepon": "113", + "icon": "pemadam" + }, + { + "id": "item-sar", + "nama": "BASARNAS", + "nomorTelepon": "115", + "icon": "sar" + }, + + { + "id": "item-polsek-darmasaba", + "nama": "Polsek Darmasaba", + "nomorTelepon": "0361123456", + "icon": "bangunan" + }, + { + "id": "item-polres-badung", + "nama": "Polres Badung", + "nomorTelepon": "0361123999", + "icon": "bangunan" + }, + { + "id": "item-damkar-badung", + "nama": "Damkar Kabupaten Badung", + "nomorTelepon": "0361900113", + "icon": "pemadam" + }, + { + "id": "item-bpbd-badung", + "nama": "BPBD Badung", + "nomorTelepon": "0361900113", + "icon": "sar" + } +] diff --git a/prisma/data/keamanan/laporan-publik/laporan-publik.json b/prisma/data/keamanan/laporan-publik/laporan-publik.json new file mode 100644 index 00000000..2cb9f817 --- /dev/null +++ b/prisma/data/keamanan/laporan-publik/laporan-publik.json @@ -0,0 +1,16 @@ +[ + { + "id": "cmkdt14my0000vn4lrvfv6jxr", + "judul": "LAPORAN REALISASI APBDES SEMESTER I TAHUN ANGGARAN 2025", + "lokasi": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung", + "tanggalWaktu": "2025-08-04T08:58:55.080Z", + "kronologi": "

Pemerintah Desa Darmasaba menyampaikan realisasi pendapatan dan belanja desa hingga semester I tahun 2025 yang mencakup berbagai sumber pendapatan dan rincian belanja desa.

" + }, + { + "id": "cmkdt14my0000vn4lrvfv7kys", + "judul": "Aksi Penanganan Maraknya Pembuangan Sampah Liar di Wilayah Desa Darmasaba", + "lokasi": "Desa Darmasaba, Kabupaten Badung", + "tanggalWaktu": "2025-11-24T08:58:55.080Z", + "kronologi": "

Tim mendatangi rumah pihak yang diduga melakukan pembuangan sampah liar, melakukan pendataan dan penelusuran, serta koordinasi lintas wilayah untuk memastikan penanganan yang tepat.

" + } +] diff --git a/prisma/data/keamanan/laporan-publik/penanganan-laporan.json b/prisma/data/keamanan/laporan-publik/penanganan-laporan.json new file mode 100644 index 00000000..170c9527 --- /dev/null +++ b/prisma/data/keamanan/laporan-publik/penanganan-laporan.json @@ -0,0 +1,12 @@ +[ + { + "id": "cmkdt41lx0001vn4lrlcqz735", + "laporanId": "cmkdt14my0000vn4lrvfv6jxr", + "deskripsi": "

Laporan ini disampaikan sebagai bentuk komitmen transparansi pengelolaan keuangan desa dan dapat dimonitor oleh masyarakat.

" + }, + { + "id": "cmkdt41lx0002vn4lrlcqz846", + "laporanId": "cmkdt14my0000vn4lrvfv7kys", + "deskripsi": "

Pemerintah Desa bersama Penyidik Lingkungan Hidup melakukan investigasi lapangan terhadap laporan masyarakat mengenai aksi pembuangan sampah liar, serta melakukan koordinasi untuk penindakan sesuai ketentuan.

" + } +] \ No newline at end of file diff --git a/prisma/data/keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json b/prisma/data/keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json new file mode 100644 index 00000000..8854bbd1 --- /dev/null +++ b/prisma/data/keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json @@ -0,0 +1,23 @@ +[ + { + "id": "cmh48mn850003qq091pvs7rf1", + "judul": "Maling Motor di Darmasaba, Residivis Begal Didor", + "deskripsi": "

Maling Motor di Darmasaba, Residivis Begal Didor David Andiansyah kembali terancam dibui lantaran melakukan aksi pencurian.

", + "deskripsiSingkat": "

Maling Motor di Darmasaba, Residivis Begal Didor David Andiansyah kembali terancam dibui lantaran melakukan aksi pencurian. Mantan narapidana kasus begal ini diciduk polisi usai mencuri sepeda motor di garase rumah, kawasan Banjar Cabe, Desa Darmasaba, Abiansemal, Badung, Kamis (24/6). Kasi Humas Polres Badung Iptu Ketut Sudana menerangkan, peristiwa tersebut bermula ketika korban Ni Made Desniari berkunjung ke rumah tetangganya Putu Juliati. Saat itu korban memarkirkan sepeda motor Honda Beat miliknya di garase (TKP), tetapi kunci motornya masih nyantol di motor. Ketika korban kembali sekitar pukul 13.00, didapati motornya telah raib. Atas kejadian ini wanita asli Banjar Cabe melapor ke polisi.

", + "linkVideo": "https://www.youtube.com/embed/2rxK5A-KoeY" + }, + { + "id": "cmh59no850003qq091pvs7rf1", + "judul": "Integrasi Digital & Akuntabilitas Desa (Pencegahan Penyalahgunaan Wewenang)", + "deskripsi": "

Video ini membahas bagaimana integrasi digital seperti Simpades, Siskeudes, dan program Jaga Desa memperkuat akuntabilitas pemerintahan desa Darmasaba. Program tersebut mendukung transparansi dan pencegahan tindak pidana administrasi maupun penyalahgunaan wewenang karena tata kelola data desa yang baik membantu mencegah korupsi atau kecurangan dalam pengelolaan dana dan layanan desa.

", + "deskripsiSingkat": "

Integrasi digital meningkatkan transparansi dan mencegah penyalahgunaan wewenang di pemerintahan desa.

", + "linkVideo": "https://www.youtube.com/embed/l7NIqjA2b_k" + }, + { + "id": "cmh60op850003qq091pvs7rf1", + "judul": "PROFIL DESA DARMASABA", + "deskripsiSingkat": "PROFIL DESA DARMASABA Desa Darmasaba adalah permata di ujung selatan Kecamatan Abiansemal, Kabupaten Badung", + "deskripsi": "

PROFIL DESA DARMASABA Desa Darmasaba adalah permata di ujung selatan Kecamatan Abiansemal, Kabupaten Badung, yang dikenal dengan kekayaan budaya, inovasi desa, dan potensi ekonomi yang terus berkembang. Berbatasan langsung dengan Kota Denpasar, Desa Darmasaba memiliki luas wilayah 567 hektar dengan jumlah penduduk 10.141 jiwa (per akhir Desember 2024). Video ini menampilkan potensi dan keunggulan Desa Darmasaba: 🏞️ Keindahan alam dan kawasan persawahan yang dikelola empat subak aktif. 🍽️ Keberhasilan UMKM dan kuliner lokal, termasuk produk unggulan ACK yang merambah pasar nasional. 🌱 Inovasi ketahanan pangan, pengolahan sampah ramah lingkungan (TPS3R Pudak Mesari, CINTA Darmasaba), hingga Graha Sari Boga dengan program makan bergizi gratis. 🎭 Pelestarian seni, budaya, dan tradisi, termasuk maskot Sekar Pudak dan tradisi Ngerebeg. 🏆 Prestasi desa tingkat nasional dan internasional. 📱 Transformasi digital dengan aplikasi Darmasaba Digital App, talkshow Bicara Darmasaba, hingga perpustakaan digital Pustaka Ananta Loka. Dengan motto \\\"Menggali Warisan, Merangkai Inovasi\\\", Desa Darmasaba menghadirkan 13 inovasi unggulan di bidang pemerintahan, kewilayahan, UMKM, dan kemasyarakatan. Desa ini membuktikan bahwa kolaborasi masyarakat dan pemerintah mampu mewujudkan desa yang mandiri, berdaya saing, dan berkelanjutan. 🎥 Produksi: Tim Media Kreatif Desa Darmasaba 🤝 Dukungan: Seluruh Lembaga & Elemen Masyarakat Desa Darmasaba

", + "linkVideo": "https://www.youtube.com/watch?v=9eCnhJvdv6A" + } +] diff --git a/prisma/data/keamanan/polsek-terdekat/layanan-polsek.json b/prisma/data/keamanan/polsek-terdekat/layanan-polsek.json new file mode 100644 index 00000000..0d64f8c7 --- /dev/null +++ b/prisma/data/keamanan/polsek-terdekat/layanan-polsek.json @@ -0,0 +1,14 @@ +[ + { + "id": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478", + "nama": "Layanan Administrasi & Surat-Menyurat" + }, + { + "id": "b5af284c-6aa1-4442-935f-869d78eb7ecf", + "nama": "Penanganan Laporan & Pengaduan" + }, + { + "id": "56b37d7f-d717-4e33-b05d-ea22b5f7af91", + "nama": "Perlindungan & Pengayoman Masyarakat" + } +] diff --git a/prisma/data/keamanan/polsek-terdekat/layanan-to-polsek.json b/prisma/data/keamanan/polsek-terdekat/layanan-to-polsek.json new file mode 100644 index 00000000..299631cf --- /dev/null +++ b/prisma/data/keamanan/polsek-terdekat/layanan-to-polsek.json @@ -0,0 +1,62 @@ +[ + { + "id": "a50d52f2-e70f-4f29-9133-e294c40d14d3", + "layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478", + "polsekTerdekatId": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d" + }, + { + "id": "012454f8-f5d7-41c0-9dce-2754c0356523", + "layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf", + "polsekTerdekatId": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d" + }, + { + "id": "50736038-4ba6-47f8-8399-35b73b389f12", + "layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91", + "polsekTerdekatId": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d" + }, + { + "id": "e2dc3487-1f62-4f63-9a12-49ac30da3372", + "layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478", + "polsekTerdekatId": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2" + }, + { + "id": "47fe1f9c-4072-4203-90f9-3294d1369ac5", + "layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf", + "polsekTerdekatId": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2" + }, + { + "id": "2cc1ba81-6b62-42ff-af21-09f8165a2dd0", + "layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91", + "polsekTerdekatId": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2" + }, + { + "id": "3ca2ce42-2e7d-4750-87b7-f1f52ed141de", + "layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478", + "polsekTerdekatId": "9a3fff54-8854-4929-b9b5-b5b2751011ea" + }, + { + "id": "90472bca-cf3d-47ca-92e5-db43c4c7e579", + "layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf", + "polsekTerdekatId": "9a3fff54-8854-4929-b9b5-b5b2751011ea" + }, + { + "id": "41cfc1fe-a193-446d-b574-64b1124c6f55", + "layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91", + "polsekTerdekatId": "9a3fff54-8854-4929-b9b5-b5b2751011ea" + }, + { + "id": "f35443e1-6aca-416d-8c55-00e3f4a0f5f9", + "layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478", + "polsekTerdekatId": "c2d272e1-737d-44f5-bd85-ae268cb06cbf" + }, + { + "id": "e09797f6-82e5-4b77-946e-319eee431c8f", + "layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf", + "polsekTerdekatId": "c2d272e1-737d-44f5-bd85-ae268cb06cbf" + }, + { + "id": "bd9b1f27-cd1b-4e23-b162-3a757745f78a", + "layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91", + "polsekTerdekatId": "c2d272e1-737d-44f5-bd85-ae268cb06cbf" + } +] diff --git a/prisma/data/keamanan/polsek-terdekat/polsek-terdekat.json b/prisma/data/keamanan/polsek-terdekat/polsek-terdekat.json new file mode 100644 index 00000000..97a41792 --- /dev/null +++ b/prisma/data/keamanan/polsek-terdekat/polsek-terdekat.json @@ -0,0 +1,54 @@ +[ + { + "id": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d", + "nama": "Kantor Polisi Abian Semal", + "jarakKeDesa": "9,6 Km", + "alamat": "ABIAN SEMAL POLICE STAT, JL. Pasar, Blahkiuh, Kec. Abiansemal, Kabupaten Badung, Bali 80352", + "nomorTelepon": "0361813972", + "jamOperasional": "Buka 24 Jam", + "embedMapUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d63127.118683990586!2d115.16592643905703!3d-8.553143643198624!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23ceb4f6e5363%3A0xa353662f070f47d8!2sAbian%20Semal%20Police%20Station!5e0!3m2!1sid!2sid!4v1768376981008!5m2!1sid!2sid", + "namaTempatMaps": "Abian Semal Police Station", + "alamatMaps": "ABIAN SEMAL POLICE STAT, JL. Pasar, Blahkiuh, Kec. Abiansemal, Kabupaten Badung, Bali 80352", + "linkPetunjukArah": "https://maps.app.goo.gl/GhHVNQqffNrXSMFX7", + "layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478" + }, + { + "id": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2", + "nama": "Polres Badung", + "jarakKeDesa": "5,8 Km", + "alamat": "Jl. Kebo Iwa No.1, Mengwitani, Kec. Mengwi, Kabupaten Badung, Bali 80351", + "nomorTelepon": "0361829949", + "jamOperasional": "Buka 24 Jam", + "embedMapUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d15780.907469872707!2d115.17829950197888!3d-8.574172113520685!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd238cade1488c3%3A0x918ba5cac3ef00b7!2sPolres%20Badung!5e0!3m2!1sid!2sid!4v1768377100998!5m2!1sid!2sid", + "namaTempatMaps": "Polres Badung", + "alamatMaps": "Jl. Kebo Iwa No.1, Mengwitani, Kec. Mengwi, Kabupaten Badung, Bali 80351", + "linkPetunjukArah": "https://maps.app.goo.gl/7yQQod4PFhpqqe7Z8", + "layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478" + }, + { + "id": "9a3fff54-8854-4929-b9b5-b5b2751011ea", + "nama": "Polsek Mengwi", + "jarakKeDesa": "9,7 Km", + "alamat": "Jl. I Gusti Ngurah Rai No.110, Werdi Bhuwana, Kec. Mengwi, Kabupaten Badung, Bali 80351", + "nomorTelepon": "0361411270", + "jamOperasional": "Buka 24 Jam", + "embedMapUrl": "https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d63126.100310757916!2d115.1716545!3d-8.5592871!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23b9ddc3980af%3A0xa1f4f195483537b!2sPolsek%20Mengwi!5e0!3m2!1sid!2sid!4v1768377317955!5m2!1sid!2sid", + "namaTempatMaps": "Polsek Mengwi", + "alamatMaps": "Jl. I Gusti Ngurah Rai No.110, Werdi Bhuwana, Kec. Mengwi, Kabupaten Badung, Bali 80351", + "linkPetunjukArah": "https://maps.app.goo.gl/cJD44NSUdpA7Ly2m6", + "layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478" + }, + { + "id": "c2d272e1-737d-44f5-bd85-ae268cb06cbf", + "nama": "Pos Polisi Ahmad Yani", + "jarakKeDesa": "7 Km", + "alamat": "Jl. Ahmad Yani Utara No.5, Peguyangan, Kec. Denpasar Utara, Kota Denpasar, Bali 80239", + "nomorTelepon": "-", + "jamOperasional": "-", + "embedMapUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d31558.650325984465!2d115.18791122296605!3d-8.612190901728288!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23f635f1186b1%3A0xcfdde508d897fb04!2sPos%20Polisi%20Simpang%20Ahmad%20Yani!5e0!3m2!1sid!2sid!4v1768377470154!5m2!1sid!2sid", + "namaTempatMaps": "Pos Polisi Simpang Ahmad Yani", + "alamatMaps": "Jl. Ahmad Yani Utara No.5, Peguyangan, Kec. Denpasar Utara, Kota Denpasar, Bali 80239", + "linkPetunjukArah": "https://maps.app.goo.gl/D8HGs4mSAQqJm9KRA", + "layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478" + } +] diff --git a/prisma/data/keamanan/tips-keamanan/tips-keamanan.json b/prisma/data/keamanan/tips-keamanan/tips-keamanan.json new file mode 100644 index 00000000..fcf6d4d5 --- /dev/null +++ b/prisma/data/keamanan/tips-keamanan/tips-keamanan.json @@ -0,0 +1,14 @@ +[ + { + "id": "cmkp70zau0002vnu9o1jtpi1i", + "judul": "Keamanan Rumah", + "deskripsi": "

", + "imageName": "dSe0xyvNLkP2t2f6iq-Hk-mobile.webp" + }, + { + "id": "cmkp71pzo0005vnu9p3n9646d", + "judul": "Keamanan Lingkungan Tanggungjawab Bersama", + "deskripsi": "

Pemerintah Desa Darmasaba melaksanakan sosialisasi dan pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai. Warga diajak berperan aktif dalam menjaga keamanan lingkungan serta mendukung penyediaan lampu penerangan jalan untuk mencegah tindak kriminal dan kecelakaan. Bhabinkamtibmas dan Babinsa turut memberikan materi keamanan dan ketertiban kepada warga, menekankan pentingnya partisipasi masyarakat dalam menjaga keamanan desa.

", + "imageName": "vwZsaxcoFWDlxG1PW7FC0-mobile.webp" + } +] \ No newline at end of file diff --git a/prisma/data/kesehatan/infowabahpenyakit/infowabahpenyakit.json b/prisma/data/kesehatan/infowabahpenyakit/infowabahpenyakit.json new file mode 100644 index 00000000..62a96e1a --- /dev/null +++ b/prisma/data/kesehatan/infowabahpenyakit/infowabahpenyakit.json @@ -0,0 +1,37 @@ +[ + { + "id": "cmkax3ptc000tvn6ytq1lpb2z", + "name": "Diare dan Kolera", + "deskripsiSingkat": "

Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?

Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.

", + "deskripsiLengkap": "

Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?

Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.

", + "imageName": "5giLSHSnWEFoZoMEcjhL7-mobile.webp" + }, + { + "id": "cmkax5urc000wvn6yxfw0970w", + "name": "TBC (Tuberkulosis)", + "deskripsiSingkat": "

Apa itu TBC penyebab, gejala dan cara penanganannya?

Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.

", + "deskripsiLengkap": "

Apa itu TBC penyebab, gejala dan cara penanganannya?

Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.

Penyebab: Bakteri Mycobacterium tuberculosis yang menyebar melalui udara.

Gejala: Batuk lebih dari 2 minggu, berkeringat di malam hari, dan berat badan turun.

Pencegahan: Vaksin BCG, pola hidup sehat, dan pengobatan bagi penderita agar tidak menular.

", + "imageName": "3faPo-1wjhVDVU6S7S8sS-mobile.webp" + }, + { + "id": "cmkax72s7000zvn6yz3nmvrry", + "name": "Demam Berdarah Dengue (DBD)", + "deskripsiSingkat": "

Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.

", + "deskripsiLengkap": "

Apa itu DBD penyebab, gejala dan cara penanganannya?

Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.

Penyebab: Virus dengue yang ditularkan oleh nyamuk Aedes aegypti.

Gejala: Demam tinggi, nyeri sendi, ruam kulit, dan pendarahan ringan.

Pencegahan: Menguras tempat air, menutup wadah air, fogging, dan menggunakan lotion anti-nyamuk.

", + "imageName": "DyX82oztXbHfu6HEvbrpt-mobile.webp" + }, + { + "id": "cmkbyny4f0002vn67kmjmjrpl", + "name": "Fogging sebagai Pencegah DBD di Br. Umahanyar Desa Darmasaba", + "deskripsiSingkat": "

Pemerintah Desa Darmasaba melaksanakan fogging di wilayah Br. Umahanyar sebagai upaya pencegahan DBD di Desa Darmasaba.

", + "deskripsiLengkap": "

Pemerintah Desa Darmasaba melaksanakan fogging (pengasapan) di wilayah Br. Umahanyar Desa Darmasaba Kecamatan Abiansemal Kabupaten Badung dari tanggal 12 sampai dengan 13 April 2023.

Fogging ini merupakan salah satu metode yang dilakukan oleh Pemdes Darmasaba dalam pencegahan penyakit Demam Berdarah Dengue (DBD) dengan menargetkan nyamuk Aedes aegypti sebagai vektor penyebabnya.

", + "imageName": "pps1ZgzJxDb4VZxEvtZeu-mobile.webp" + }, + { + "id": "cmkbyr3rx0005vn674uhycsxc", + "name": "Gerakan Serentak Penyemprotan Pencegahan PMK di Desa Darmasaba", + "deskripsiSingkat": "

Penyemprotan serentak dilakukan di Desa Darmasaba untuk mencegah Penyakit Mulut dan Kaki (PMK) pada hewan ternak.

", + "deskripsiLengkap": "

Setelah dilakukan vaksinasi Penyakit Mulut dan Kaki (PMK) pada hewan ternak yaitu sapi di wilayah Desa Darmasaba, Pemerintah Desa Darmasaba melaksanakan gerakan serentak penyemprotan pencegahan PMK pada hari Rabu (20/7/2022) di seputaran wilayah Desa Darmasaba.

Upaya ini dilakukan sebagai bentuk pencegahan terhadap penyebaran PMK dan menjaga kesehatan hewan ternak di desa.

", + "imageName": "JhJigMo269K1TFGzSB1OS-mobile.webp" + } +] diff --git a/prisma/data/kesehatan/kontak-darurat/kontak-darurat.json b/prisma/data/kesehatan/kontak-darurat/kontak-darurat.json new file mode 100644 index 00000000..91d74dcf --- /dev/null +++ b/prisma/data/kesehatan/kontak-darurat/kontak-darurat.json @@ -0,0 +1,30 @@ +[ + { + "id": "cmkax1vks000qvn6yyxuvfsi8", + "name": "Puskesmas Pembantu Darmasaba", + "deskripsi": "

Puskesmas Pembantu Darmasaba merupakan fasilitas kesehatan tingkat pertama yang berada di Desa Darmasaba, melayani berbagai layanan kesehatan masyarakat termasuk pemeriksaan umum dan imunisasi.

", + "imageName": "g4ICsRrmOaIqS_yqlQLZK-mobile.webp", + "whatsapp": "089647037430" + }, + { + "id": "cmkawzrvg000nvn6ywyx529em", + "name": "UPTD Puskesmas Abiansemal III (melayani Darmasaba)", + "deskripsi": "

Puskesmas Abiansemal III adalah fasilitas kesehatan utama di kecamatan Abiansemal yang melayani wilayah Desa Darmasaba dan sekitarnya. Puskesmas ini memiliki layanan 24 jam serta pelayanan darurat kesehatan dasar.

", + "imageName": "1NkzPzQailqE5yNOiUjB9-mobile.webp", + "whatsapp": "03618463263" + }, + { + "id": "cmkawy5in000kvn6yza82pkkg", + "name": "UPTD Puskesmas Abiansemal I", + "deskripsi": "

Puskesmas Abiansemal I melayani masyarakat di wilayah kecamatan Abiansemal, termasuk pelayanan kesehatan darurat dan program kesehatan masyarakat.

", + "imageName": "NBPAqjPXn7GQmYTDBI5hu-mobile.webp", + "whatsapp": "087858367111" + }, + { + "id": "cmkb6ehu20003vn14ca4xr057", + "name": "Kantor Desa Darmasaba (Kontak Informasi Kesehatan)", + "deskripsi": "

Kantor Pemerintahan Desa Darmasaba dapat menjadi saluran kontak awal untuk rujukan layanan kesehatan darurat atau informasi lebih lanjut mengenai fasilitas kesehatan di wilayah desa.

", + "imageName": "EcQIGOF6LW1dIKE53vmba-mobile.webp", + "whatsapp": "081239580000" + } +] diff --git a/prisma/data/kesehatan/penanganan-darurat/penganan-darurat.json b/prisma/data/kesehatan/penanganan-darurat/penganan-darurat.json new file mode 100644 index 00000000..d2c9cf38 --- /dev/null +++ b/prisma/data/kesehatan/penanganan-darurat/penganan-darurat.json @@ -0,0 +1,26 @@ +[ + { + "id": "cmkawso7y000evn6ygob15cqb", + "name": "Rembug Stunting di Desa Darmasaba", + "deskripsi": "

Pemerintah Desa Darmasaba melaksanakan kegiatan rembug stunting dengan melibatkan bidan desa, kader posyandu, dan tokoh masyarakat. Tujuan kegiatan ini adalah untuk memperkuat upaya pencegahan kekerdilan (stunting) melalui koordinasi layanan kesehatan, edukasi gizi, serta percepatan penanganan gizi buruk di lingkungan desa sebagai bagian dari respons terhadap kondisi kesehatan yang mendesak.

", + "imageName": "Gi8EX3pBmT719AfzXirDS-mobile.webp" + }, + { + "id": "cmkawq3ef000bvn6y387vub0y", + "name": "Posko Kesehatan Darurat dan Bencana", + "deskripsi": "

Posko Kesehatan Darurat dan Bencana Desa Darmasaba dibentuk sebagai pusat koordinasi dan pertolongan bagi warga yang terdampak situasi darurat seperti banjir, tanah longsor, atau wabah penyakit. Posko ini dilengkapi dengan tenaga medis, obat-obatan dasar, serta dukungan logistik untuk memastikan penanganan cepat dan tepat sasaran. Kegiatan ini juga melibatkan kader kesehatan desa dan karang taruna sebagai relawan lapangan.

", + "imageName": "v7Ac2xQvTiJy-HYh1AxF4-mobile.webp" + }, + { + "id": "cmkawso7y000evn6ygob14bpa", + "name": "Layanan Ambulans Desa Darmasaba", + "deskripsi": "

Layanan Ambulans Desa Darmasaba disiapkan untuk membantu masyarakat yang membutuhkan transportasi medis darurat ke fasilitas kesehatan terdekat. Layanan ini beroperasi 24 jam dan dapat dihubungi melalui nomor darurat desa. Tim ambulans terdiri dari relawan terlatih dan tenaga medis yang siap memberikan pertolongan pertama di lokasi kejadian sebelum dirujuk ke rumah sakit atau puskesmas.

", + "imageName": "jYxEXspWH5g6eTTVqK72c-mobile.webp" + }, + { + "id": "cmkawu7te000hvn6yh3pdnv4w", + "name": "Penanganan Darurat Sosial & Kesehatan Desa Darmasaba", + "deskripsi": "

Program Penanganan Darurat Sosial & Kesehatan Desa Darmasaba bertujuan memberikan respon cepat terhadap situasi darurat seperti warga sakit mendadak, kecelakaan, bencana alam, maupun kondisi sosial yang membutuhkan bantuan segera. Tim Siaga Desa Darmasaba berkoordinasi dengan Puskesmas Abiansemal dan BPBD untuk memastikan penanganan yang cepat, tepat, dan manusiawi. Program ini juga mencakup layanan ambulans desa, posko kesehatan darurat, serta bantuan logistik bagi warga terdampak.

", + "imageName": "3tNQ9J8I3Ewq5H8CWuqvp-mobile.webp" + } +] diff --git a/prisma/data/kesehatan/posyandu/posyandu.json b/prisma/data/kesehatan/posyandu/posyandu.json new file mode 100644 index 00000000..8c3aac84 --- /dev/null +++ b/prisma/data/kesehatan/posyandu/posyandu.json @@ -0,0 +1,10 @@ +[ + { + "id": "cmkanjnmx0006vntz1cn7owpb", + "name": "Posyandu Pudak Amara", + "nomor": "(0361) 8463263", + "deskripsi": "

Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.

Kegiatan ini melibatkan kader posyandu serta didampingi pihak desa dan puskesmas setempat untuk meningkatkan pelayanan kesehatan ibu dan anak.

", + "jadwalPelayanan": "

Setiap bulan pada satu hari tertentu (mis. minggu ke-2): 08:00 – 12:00 WITA (posyandu balita & ibu hamil)

", + "imageName": "TDQReg1lQ73s39crXW0ra-mobile.webp" + } +] diff --git a/prisma/data/kesehatan/program-kesehatan/program-kesehatan.json b/prisma/data/kesehatan/program-kesehatan/program-kesehatan.json new file mode 100644 index 00000000..770b44d1 --- /dev/null +++ b/prisma/data/kesehatan/program-kesehatan/program-kesehatan.json @@ -0,0 +1,51 @@ +[ + { + "id": "cmkawkji50002vn6yzyrlqhh1", + "name": "Gerakan Kulkul PKK dan Posyandu Desa Darmasaba", + "deskripsiSingkat": "

Kegiatan bersama PKK dan Posyandu untuk meningkatkan pelayanan kesehatan masyarakat.

", + "deskripsi": "

Pada hari Minggu, 11 Januari 2025, Pemerintah Desa Darmasaba melalui TP PKK dan TP Posyandu melaksanakan kegiatan Gerakan Kulkul PKK dan Posyandu yang berlangsung serentak di seluruh wilayah Desa Darmasaba untuk memperkuat pelayanan kesehatan dasar dan peningkatan partisipasi masyarakat dalam program Posyandu.

", + "imageName": "hLeF0GRFZqDUngZnDMAAk-mobile.webp" + }, + { + "id": "cmkawmlg40005vn6yja2xiev0", + "name": "Pendampingan Kunjungan Rumah oleh Puskesmas Abiansemal 3", + "deskripsiSingkat": "

Pendataan kesehatan penyandang disabilitas lewat kunjungan rumah di Desa Darmasaba.

", + "deskripsi": "

Pemerintah Desa Darmasaba bersama Kelian Banjar Dinas dan kader kesehatan mendampingi kegiatan kunjungan rumah yang dilaksanakan oleh Puskesmas Abiansemal 3 pada 21 Juli 2025, difokuskan pada pendataan dan pemantauan kondisi kesehatan penyandang disabilitas di Banjar Bersih, Desa Darmasaba.

", + "imageName": "hyyTFi8EApjzFEZ9EvJgB-mobile.webp" + }, + { + "id": "cmkawnr9k0008vn6ymwv0foiv", + "name": "Kegiatan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali di Desa Darmasaba", + "deskripsiSingkat": "

Aksi sosial TP Posyandu Bali untuk memperkuat pelayanan posyandu di desa.

", + "deskripsi": "

Pada 10 Desember 2025, Desa Darmasaba menjadi lokasi pelaksanaan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali yang bertujuan memperkuat pelayanan Posyandu serta meningkatkan kesejahteraan masyarakat, khususnya keluarga dan balita.

", + "imageName": "l4qsUEw2JiclGAkkrXp9g-mobile.webp" + }, + { + "id": "cmkawnr9k0008vn6ymwv0dpjw", + "name": "Inovasi BAJRA dalam Penanggulangan Rabies", + "deskripsiSingkat": "

Program BAJRA untuk penanggulangan rabies di Desa Darmasaba.

", + "deskripsi": "

Desa Darmasaba mengembangkan inovasi BAJRA (Bersama Jaga Rabies), sebuah program berbasis komunitas untuk penanggulangan rabies yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan dan koordinasi lintas sektor antara kesehatan hewan, manusia, dan pemerintahan desa.

", + "imageName": "Gc79mlIlGuoRQuTqskFj--mobile.webp" + }, + { + "id": "cmkawnr9k0008vn6ymwv0eqkx", + "name": "Posyandu Pudak Amara Berkompetisi", + "deskripsiSingkat": "

Partisipasi Posyandu Pudak Amara dalam lomba prestasi Posyandu tingkat provinsi.

", + "deskripsi": "

Kader Posyandu Pudak Amara Br. Cabe mendapat pendampingan dari Perbekel Darmasaba, Dinas Kesehatan Kab. Badung, Puskesmas Abiansemal III, dan Pustu Desa Darmasaba dalam ajang lomba kader dan Posyandu berprestasi tingkat Provinsi Bali tahun 2025.

", + "imageName": "OsMY3AYPyGC_CoN1xUjOn-mobile.webp" + }, + { + "id": "cmkawnr9k0008vn6ymwv1frly", + "name": "Outbound Kader Posyandu Darmasaba", + "deskripsiSingkat": "

Program pembinaan dan pengembangan kapasitas kader Posyandu.

", + "deskripsi": "

Pemdes Darmasaba melaksanakan kegiatan Outbound Posyandu untuk meningkatkan kapasitas dan wawasan Kader Posyandu se-Desa Darmasaba sebagai bagian dari upaya peningkatan kualitas pelayanan kesehatan dasar di masyarakat.

", + "imageName": "M9QlgVKIEfCdY3g4F_tRZ-mobile.webp" + }, + { + "id": "cmkdu8ki10004vn4lpbxm2zqo", + "name": "PEMBANGUNAN JAMBAN BAGI MASYARAKAT", + "deskripsiSingkat": "

Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.

", + "deskripsi": "

Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat. Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.

Pemberian bantuan jamban ini dilaksanakan di 11 banjar dengan menyasar 22 keluarga yang memang belum memiliki jamban yang sumber dananya sepenuhnya dari APBDes Darmasaba T. A. 2023. Pembangunan Jamban bagi Masyarakat ini juga menjadi bukti komitmen Pemerintah Desa Darmasaba dalam melaksanakan salah satu visi mewujudkan masyarakat yang sejahtera dan berbudaya untuk menjaga lingkungan yang bersih dan sehat.

", + "imageName": "6DQbAvn0St-xHdPGW3vpY-mobile.webp" + } +] diff --git a/prisma/data/kesehatan/puskesmas/jam-puskesmas/jam.json b/prisma/data/kesehatan/puskesmas/jam-puskesmas/jam.json new file mode 100644 index 00000000..b59aa3e6 --- /dev/null +++ b/prisma/data/kesehatan/puskesmas/jam-puskesmas/jam.json @@ -0,0 +1,14 @@ +[ + { + "id": "cmkao2zwx0008vntzmvqdsdzo", + "workDays": "09:00", + "weekDays": "17:00", + "holiday": "08:00 - 16:00" + }, + { + "id": "cmkao2zwx0008vntzmvqdseal", + "workDays": "08:00", + "weekDays": "12:00", + "holiday": "–" + } +] diff --git a/prisma/data/kesehatan/puskesmas/kontak-puskesmas/kontak.json b/prisma/data/kesehatan/puskesmas/kontak-puskesmas/kontak.json new file mode 100644 index 00000000..df7263ae --- /dev/null +++ b/prisma/data/kesehatan/puskesmas/kontak-puskesmas/kontak.json @@ -0,0 +1,16 @@ +[ + { + "id": "cmkao2zxc0009vntz00kev051", + "kontakPuskesmas": "(0361) 8463263", + "email": "puskesmas@gmail.com", + "facebook": "puskesmas@gmail.com", + "kontakUGD": "(0361) 8463263" + }, + { + "id": "cmkao2zxc0009vntz00kev162", + "kontakPuskesmas": "–", + "email": "–", + "facebook": "–", + "kontakUGD": "–" + } +] diff --git a/prisma/data/kesehatan/puskesmas/puskesmas.json b/prisma/data/kesehatan/puskesmas/puskesmas.json new file mode 100644 index 00000000..df818aac --- /dev/null +++ b/prisma/data/kesehatan/puskesmas/puskesmas.json @@ -0,0 +1,18 @@ +[ + { + "id": "cmkao2zxk000bvntzbavkbg6p", + "name": "Puskesmas Abiansemal III", + "alamat": "Jl. Ratna, Sibang Kaja, Abiansemal, Badung, Bali 80352", + "jamId": "cmkao2zwx0008vntzmvqdsdzo", + "imageName": "d6hJgycQawWN3VEcHaqtR-mobile.webp", + "kontakId": "cmkao2zxc0009vntz00kev051" + }, + { + "id": "cmkao2zxk000bvntzbavkbh7q", + "name": "Puskesmas Pembantu Darmasaba", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "jamId": "cmkao2zwx0008vntzmvqdseal", + "imageName": "cg78Sb_QzZFlli9s2FPVc-mobile.webp", + "kontakId": "cmkao2zxc0009vntz00kev162" + } +] diff --git a/prisma/data/landing-page/prestasi-desa/prestasi-desa.json b/prisma/data/landing-page/prestasi-desa/prestasi-desa.json index f9d9bff7..645964d7 100644 --- a/prisma/data/landing-page/prestasi-desa/prestasi-desa.json +++ b/prisma/data/landing-page/prestasi-desa/prestasi-desa.json @@ -1,20 +1,30 @@ [ { - "id": "cmdwrrxkh0005vnd3p5rxkiev", - "name": "Tim Bola Voli Putri Dharma Temaja meraih juara 3 dalam Turnamen Bola Voli Mangupura Cup 2024 kategori Putri Se-Bali", - "deskripsi": "

Tim Bola Voli Putri Dharma Temaja meraih juara 3 dalam Turnamen Bola Voli Mangupura Cup 2024 kategori Putri Se-Bali

", - "kategoriId": "cmdwrolsl0000vnd3e24q5440" + "id": "cmdws0sgq000bvnd32o7m94im", + "name": "Desa Darmasaba Raih Juara 1 Lomba Desa Tingkat Provinsi Bali Tahun 2025", + "deskripsi": "

Pemerintah Desa Darmasaba meraih prestasi gemilang dengan menyabet Juara 1 Lomba Desa Tingkat Provinsi Bali Tahun 2025 dalam Peringatan Hari Jadi Pemerintah Provinsi Bali ke-67. Inovasi pelayanan publik dan sinergi masyarakat menjadi kunci keberhasilan.

", + "kategoriId": "cmdwrp0pr0002vnd35w6nkjh0", + "imageName": "_CIAQk1Yem-_6LFjrkmVg-mobile.webp" }, { "id": "cmdwrzs740008vnd329ysez5x", - "name": "Prestasi Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung Tahun 2024", - "deskripsi": "

Prestasi Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung Tahun 2024

", - "kategoriId": "cmdwrot900001vnd30b5kj96g" + "name": "Desa Darmasaba Raih Juara 1 Lomba Desa Tingkat Kabupaten Badung Tahun 2025", + "deskripsi": "

Desa Darmasaba berhasil meraih Juara 1 Lomba Desa Tingkat Kabupaten Badung Tahun 2025 yang diserahkan langsung oleh Bupati Badung dalam rangka memperingati HUT RI ke-80. Prestasi ini menjadi simbol kerja keras dan inovasi desa.

", + "kategoriId": "cmdwrp0pr0002vnd35w6nkjh0", + "imageName": "pxpQFGBkTRpFOwJFTwjbD-mobile.webp" }, { - "id": "cmdws0sgq000bvnd32o7m94im", - "name": "Peringkat 5 Dalam Ajang Bergengsi Mangupura Award", - "deskripsi": "

Peringkat 5 Dalam Ajang Bergengsi Mangupura Award

", - "kategoriId": "cmdwrp0pr0002vnd35w6nkjh0" + "id": "cmdwrrxkh0005vnd3p5rxkiev", + "name": "TPS3R Pudak Mesari Desa Darmasaba Raih Juara 2 dalam Kompetisi TPS3R Kabupaten Badung", + "deskripsi": "

TPS3R Pudak Mesari Desa Darmasaba meraih Juara 2 dalam Kompetisi TPS3R tingkat Kabupaten Badung pada acara HUT Mangupura ke-16 sebagai wujud komitmen pengelolaan sampah berbasis masyarakat.

", + "kategoriId": "cmdwrp0pr0002vnd35w6nkjh0", + "imageName": "3rshaPxCgk5unArWbKPpk-mobile.webp" + }, + { + "id": "cmk3nxfkd0005vn9bcw8d10sh", + "name": "Keluarga Sadar Hukum Desa Darmasaba Raih Prestasi Gemilang (Juara 3)", + "deskripsi": "

KADARKUM Desa Darmasaba berhasil meraih Juara 3 dalam Lomba Keluarga Sadar Hukum Kabupaten Badung tahun 2024, bukti semangat kolaborasi masyarakat dengan perangkat desa dalam pendidikan hukum.

", + "kategoriId": "cmdwrp0pr0002vnd35w6nkjh0", + "imageName": "D4za5fHh_y92Bj9qZkp5j-mobile.webp" } ] \ No newline at end of file diff --git a/prisma/data/landing-page/profile/mediaSosial.json b/prisma/data/landing-page/profile/mediaSosial.json index 9af092a0..799990bb 100644 --- a/prisma/data/landing-page/profile/mediaSosial.json +++ b/prisma/data/landing-page/profile/mediaSosial.json @@ -3,24 +3,24 @@ "id": "cmds9023u0008vnbe3oxmhwyf", "name": "Desa Darmasaba", "iconUrl": "https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg", - "imageId": "cmff3joae0000vn6h8sgs0ilg" + "imageName": "q1G995W7cLkC_qquLTlKN-mobile.webp" }, { "id": "cmds90oul000bvnbe2bqkptoi", "name": "Pemerintah Desa Darmasaba", "iconUrl": "https://www.facebook.com/DarmasabaDesaku", - "imageId": "cmff3mtat0002vn6hs8vyyhdd" + "imageName": "I6mlQ4nRmPX26gm79C_rM-mobile.webp" }, { "id": "cmds91i4e000evnbe8gtf1gub", "name": "ddarmasaba", "iconUrl": "https://www.instagram.com/ddarmasaba/", - "imageId": "cmff3oouh0004vn6hd94brzv9" + "imageName": "WArLC_yvU33MjoqEnQeQ1-mobile.webp" }, { "id": "cmds92de5000hvnbemlu6sq5x", "name": "desa.darmasaba", "iconUrl": "https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc", - "imageId": "cmff3q12g0005vn6h5ojov2qa" + "imageName": "D3RPbNiaNSCjacLjeR_qO-mobile.webp" } ] diff --git a/prisma/data/landing-page/profile/profile.json b/prisma/data/landing-page/profile/profile.json index a83a8f62..31cab010 100644 --- a/prisma/data/landing-page/profile/profile.json +++ b/prisma/data/landing-page/profile/profile.json @@ -3,6 +3,6 @@ "id": "edit", "name": "I.B Surya Prabhawa Manuaba, S.H., M.H.", "position": "Perbekel Darmasaba periode 2021-2027", - "imageId": "cmff2w5ly000avn0telhct71k" + "imageName": "Eqlrr1W-pK8ShMGqgPGL3-mobile.webp" } ] diff --git a/prisma/data/landing-page/profile/programInovasi.json b/prisma/data/landing-page/profile/programInovasi.json index f6603f6d..aadd4e79 100644 --- a/prisma/data/landing-page/profile/programInovasi.json +++ b/prisma/data/landing-page/profile/programInovasi.json @@ -1,51 +1,58 @@ [ { - "id": "cmdr755pf0005vn5rp8tyuubw", - "name": "Dmangan", - "description": "Darmasaba Aman Pangan", - "link": "https://darmasaba.desa.id/berita/61452-kader-d-mangan-berhasil-meraih-prestasi-dalam-ajang-lomba-banjar-bali-quis-bbq-tahun-2024", - "imageId" : "cmff0z34f0005vn0tjtvq519p" + "id": "cmdr77vbw000bvn5rvpmoq31s", + "name": "Bares", + "description": "

BARES (Darmasaba Recycling Waste Stock/Exchange) adalah Bursa Sampah yang merupakan sebuah sistem inovatif untuk mengelola dan menukar sampah daur ulang menjadi nilai ekonomis melalui TPS3R Pudak Mesari, dengan tujuan memberdayakan masyarakat dan menjaga kebersihan lingkungan, menjadikannya bagian dari upaya lebih besar seperti program desa pintar (smart village) dan pengelolaan sampah terintegrasi.

", + "link": "https://darmasaba.desa.id/berita/56722-bares#:~:text=Sampah%20jadi%20uang%20nie%20Semeton,masing%20banjar%20se%2DDesa%20Darmasaba.", + "imageName" : "thpgPSJkBxUIRajZt3AVo-mobile.webp" }, { "id": "cmdr76nqk0008vn5rdddvcxnr", "name": "Bicara Darmasaba", - "description": "Bicara Darmasaba", - "link": "https://darmasaba.desa.id/berita/42506-bicara-darmasaba", - "imageId" : "cmff0tnf00003vn0t3kgzi0u0" + "description": "

Bicara Darmasaba adalah program ruang dialog terbuka yang digagas Pemerintah Desa Darmasaba sebagai forum strategis untuk menggali ide, menyampaikan aspirasi masyarakat, serta merangkai aksi nyata dalam menyelesaikan persoalan desa, seperti pengelolaan sampah. Kegiatan ini diselenggarakan secara berkala dengan pendekatan partisipatif dan ditayangkan secara live streaming agar transparan dan melibatkan banyak pihak, sehingga mendukung tata kelola desa yang responsif terhadap kebutuhan warga.

", + "link": "https://badungkab.go.id/kab/berita/67159-bicara-darmasaba-bahas-berbagai-persoalan-warga-didorong-berani-speak-up-sampaikan-aspirasi-dan-cari-solusi-nyata", + "imageName" : "ubna9N6r7RgVWN5plO5mq-mobile.webp" }, { - "id": "cmdr77vbw000bvn5rvpmoq31s", - "name": "Bares", - "description": "Darmasaba Recycling Stock/Exchange", - "link": "http://darmasaba.desa.id/berita/56722-bares", - "imageId" : "cmff0rr4z0002vn0twp333m2" + "id": "cmdr755pf0005vn5rp8tyuubw", + "name": "Dmangan", + "description": "

Dmangan (Darmasaba Aman Pangan) adalah program inovasi Desa Darmasaba yang fokus pada ketahanan pangan masyarakat desa melalui pengembangan produksi hasil pangan lokal, peningkatan kemandirian pangan, serta pembinaan dan pemberdayaan warga dalam praktik pertanian dan pangan berkelanjutan.

", + "link": "https://darmasaba.desa.id/berita/51787-desa-darmasaba-juara-1-lomba-desa-pangan-aman-provinsi-bali-tahun-2023#:~:text=Salah%20satu%20program%20inovasi%20Pemdes%20Darmasaba%20yaitu,Darmasaba%20dan%20seluruh%20stakeholder%20terkait%20yang%20telah", + "imageName" : "y4yaE4XdUP1TSUGhWPW9h-mobile.webp" }, { - "id": "cmdr7bxtp000evn5rmy85wihx", - "name": "Sajjana Dharma Raksaka", - "description": "Sajjana Dharma Raksaka", - "link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf", - "imageId" : "cmff10cwq0009vn0tse8dzu3j" - }, - { - "id": "cmdr7dlnk000hvn5r9lur3z35", - "name": "PDKT", - "description": "Perangkat Desa Kuat Teknologi", - "link": "https://darmasaba.desa.id/berita/53752-p-d-k-t", - "imageId" : "cmff1013m0008vn0th7t0d64d" + "id": "cmk228ust0009vnev5p8i377o", + "name": "Davest", + "description": "

DAVEST (Darmasaba Investment) merupakan program inovasi Desa Darmasaba yang bertujuan mempromosikan potensi investasi desa secara terintegrasi melalui media digital dan pendampingan langsung. Program ini menjadi sarana penghubung antara pemerintah desa, pelaku usaha, dan investor dalam rangka mendorong pertumbuhan ekonomi desa yang berkelanjutan.

DAVEST menyajikan informasi potensi unggulan desa seperti sektor UMKM, pariwisata, ekonomi kreatif, serta peluang investasi berbasis sumber daya lokal dengan prinsip transparansi dan kemudahan akses informasi.

Di tahun 2024 ini Davest (Darmasaba Village Festival) akan diadakan lagi, dengan berbagai kegiatan pemerdayaan, edukasi dan hiburan yang tentunya lebih waahhhh dari dua tahun lalu. Untuk memantapkan hal tersebut, Pemdes Darmasaba melakukan rapat koordinasi (rakor) Davest 2024 yang dipimpin langsung oleh Perbekel Darmasaba I. B. Surya Prabhawa Manuaba, S.H.,M.H. pada hari Senin (22/1/2024) bertempat di Ruang Shanti Gosana Kantor Perbekel Darmasaba.


Tujuan Program


Sasaran Program


Bentuk Inovasi


Ruang Lingkup Kegiatan

", + "link": "https://darmasaba.desa.id/berita/55862-rakor-davest-2024", + "imageName" : "Z4i2RRnnlHq2iWj94ldyo-mobile.webp" }, { "id": "cmdr7ftob000mvn5rfhgdtg8v", "name": "GM", - "description": "Galah Melah", - "link": "https://darmasaba.desa.id/berita/52880-galah-melah", - "imageId" : "cmff38cyq000bvn0t9f01cz3f" + "description": "

Galah Melah (Gerak dan Langkah Memilah Sampah) adalah program inovasi pengelolaan sampah yang dikembangkan oleh Pemerintah Desa Darmasaba melalui TPS3R Pudak Mesari, dengan fokus pada praktik pemisahan sampah dari sumbernya (di rumah, banjar, maupun fasilitas umum).

", + "link": "https://mcinews.id/2025/09/12/serius-tangani-sampah-desa-darmasaba-terapkan-inovasi-dan-osaki-composting-system-di-tps-3r-pudak-mesari-dari-2023", + "imageName" : "Vr7CoaYDpk2dIkHx9PxRj-mobile.webp" }, { "id": "cmdr7glue000pvn5r6onzslju", "name": "Inovasi Desa Darmasaba", - "description": "Inovasi Desa Darmasaba", - "link": "https://darmasaba.desa.id/produk-lokal-desa", - "imageId" : "cmff0zqvd0007vn0tv6o5hjcq" + "description": "

Inovasi Desa Darmasaba adalah kumpulan program inovatif yang dikembangkan oleh Pemerintah Desa Darmasaba untuk memperkuat penyelenggaraan pemerintahan desa, pemberdayaan masyarakat, pengelolaan lingkungan, serta peningkatan kualitas sosial-ekonomi dan budaya desa.

", + "link": "https://mcinews.id/2025/09/11/inovasi-desa-darmasaba-lanjutkan-perjuangan-ke-tingkat-nasional-dan-diakui-negara-tetangga", + "imageName" : "ceoB_sg-HOzljN8j_2nZA-mobile.webp" + }, + { + "id": "cmdr7dlnk000hvn5r9lur3z35", + "name": "PDKT", + "description": "

PDKT (Perangkat Desa Kuat Teknologi) merupakan program inovasi Desa Darmasaba yang dirancang untuk menguatkan kapasitas dan kompetensi perangkat desa dalam memanfaatkan teknologi informasi dan komunikasi guna mendukung tata kelola pemerintahan desa yang modern, transparan, cepat, dan responsif.

", + "link": "https://darmasaba.desa.id/berita/67168-desa-darmasaba-masuk-5-besar-nasional-dalam-ajang-pemerintah-desa-dan-kelurahan-award-2025", + "imageName" : "vOy5YVUXfHXfiFOHylIN7-mobile.webp" + }, + { + "id": "cmdr7bxtp000evn5rmy85wihx", + "name": "Sajjana Dharma Raksaka", + "description": "

Sajjana Dharma Raksaka adalah program inovasi Desa Darmasaba yang bertujuan untuk memperkuat akses perlindungan hukum dan pendampingan hukum non-litigasi bagi masyarakat desa. Melalui inovasi ini, desa menyediakan layanan pendampingan, mediasi, dan bantuan dalam menyelesaikan persoalan hukum secara bijaksana, humanis, dan berperspektif keadilan sosial

", + "link": "https://ppid.badungkab.go.id/storage/dokumen/5RS9dldGkrgzMQq6bKdZsqsVRHI8gffWv4PGfb3r.pdf", + "imageName" : "gE_qcqIbY0mqI6FV9V4CL-mobile.webp" } ] diff --git a/prisma/data/landing-page/sdgs-desa/sdgs-desa.json b/prisma/data/landing-page/sdgs-desa/sdgs-desa.json index 9f1fc623..d8969097 100644 --- a/prisma/data/landing-page/sdgs-desa/sdgs-desa.json +++ b/prisma/data/landing-page/sdgs-desa/sdgs-desa.json @@ -3,109 +3,109 @@ "id": "cmdsjzdl30002vneknuvo4irv", "name": "Desa Tanpa Kemiskinan", "jumlah": "52.62", - "imageId": "" + "imageName": "kxpXwgyx74-HkAGXML7Hv-mobile.webp" }, { "id": "cmdskargd0005vnek0mu2ofk9", "name": "Desa Tanpa Kelaparan", "jumlah": "35.75", - "imageId": "" + "imageName": "D0DEORcSkSWXEpY29mh_x-mobile.webp" }, { "id": "cmdskbvl0008vnek5dmieatb", "name": "Desa Sehat Dan Sejahtera", "jumlah": "77.37", - "imageId": "" + "imageName": "jrUol97rV5whAUtpljbyF-mobile.webp" }, { "id": "cmdskcx91000bvneko7tuaoqa", "name": "Pendidikan Desa Berkualitas", "jumlah": "34.11", - "imageId": "" + "imageName": "Kh8RTS4ac1bG4ZXy3L2-y-mobile.webp" }, { "id": "cmdskjare000evnek1hglu0x8", "name": "Keterlibatan Perempuan Desa", "jumlah": "45.70", - "imageId": "" + "imageName": "8bAk-cuzJ73W1dgj4lVqM-mobile.webp" }, { "id": "cmdskqcpc0002vnvnqjkqgm92", "name": "Desa Layak Air Bersih Dan Sanitasi", "jumlah": "48.54", - "imageId": "" + "imageName": "k4zOPRqxi6PS0W_rHgiVp-mobile.webp" }, { "id": "cmdsktl3x0005vnvne15seefw", "name": "Desa Berenergi Bersih Dan Terbarukan", "jumlah": "99.64", - "imageId": "" + "imageName": "txtX9hDmrHrRIkmpyQ6rp-mobile.webp" }, { "id": "cmdskuncw0008vnvcsdqoeog", "name": "Pertumbuhan Ekonomi Desa Merata", "jumlah": "40.92", - "imageId": "" + "imageName": "Yx7ruGLJ_0QtD5RCvYhEC-mobile.webp" }, { "id": "cmdskw83j000bvvn9szqrea6", "name": "Infrastruktur Dan Inovasi Desa Sesuai Kebutuhan", "jumlah": "35.37", - "imageId": "" + "imageName": "j1kZLRY4HLFBxdRt24XQw-mobile.webp" }, { "id": "cmdskwrq7000envnvy0c5nbgf", "name": "Desa Tanpa Kesenjangan", "jumlah": "35.47", - "imageId": "" + "imageName": "Iqx5__9VtAQiScvvGb21S-mobile.webp" }, { "id": "cmdskxivx000hnvnvsx520gv1", "name": "Kawasan Pemukiman Desa Aman Dan Nyaman", "jumlah": "40.35", - "imageId": "" + "imageName": "Eelib_Lr7GYv0nPj3XHVo-mobile.webp" }, { "id": "cmdskzg4c000kvnnkiv61gkt", "name": "Konsumsi Dan Produksi Desa Sadar Lingkungan", "jumlah": "16.67", - "imageId": "" + "imageName": "6MzAfXZyjsSTOkckqYJug-mobile.webp" }, { "id": "cmdsl07lk000nvnnvnrepsdy5m", "name": "Desa Tanggap Perubahan Iklim", "jumlah": "0.00", - "imageId": "" + "imageName": "f05zcftt1hdvkhCeN7wxk-mobile.webp" }, { "id": "cmdsl10rq000qvnvnlch9c1yv", "name": "Desa Peduli Lingkungan Laut", "jumlah": "50.00", - "imageId": "" + "imageName": "3tuC1Bq5WvH7QEBHQVDaM-mobile.webp" }, { "id": "cmdsl1mc2000tvnvn357n8usi", "name": "Desa Peduli Lingkungan Darat", "jumlah": "0.00", - "imageId": "" + "imageName": "oTKQeqAydsV5fXelI0S7Y-mobile.webp" }, { "id": "cmdsl2bx3000wvnvntshi4gnj", "name": "Desa Damai Berkeadilan", "jumlah": "78.65", - "imageId": "" + "imageName": "16sdgs.webp" }, { "id": "cmdsl2yz3000zvnvnmf60ok7q", "name": "Kemitraan Untuk Pembangunan Desa", "jumlah": "20.00", - "imageId": "" + "imageName": "L8jv3Wvj0GwsQgS7JSJQO-mobile.webp" }, { "id": "cmdsl492h0012vnvnmckm3n2x", "name": "Kelembagaan Desa Dinamis Dan Budaya Desa Adaptif", "jumlah": "47.22", - "imageId": "" + "imageName": "18sdgs.webp" } ] diff --git a/prisma/data/lingkungan/data-lingkungan-desa/data-lingkungan-desa.json b/prisma/data/lingkungan/data-lingkungan-desa/data-lingkungan-desa.json new file mode 100644 index 00000000..a96fcc1f --- /dev/null +++ b/prisma/data/lingkungan/data-lingkungan-desa/data-lingkungan-desa.json @@ -0,0 +1,44 @@ +[ + { + "id": "bnlneq3zg0000vn8issps1eg8", + "name": "Pelaksanaan Kebersihan Lingkungan Desa", + "jumlah": "1 paket kegiatan", + "deskripsi": "Kegiatan pelaksanaan kebersihan lingkungan desa Darmasaba untuk meningkatkan kualitas lingkungan dan kesehatan masyarakat.", + "icon": "lingkunganSehat" + }, + { + "id": "bnlneq3zg0000vn8issps2fh9", + "name": "Penataan Taman Telajakan", + "jumlah": "1 paket kegiatan", + "deskripsi": "Program penataan taman telajakan sebagai upaya memperindah lingkungan desa serta ruang terbuka publik di desa Darmasaba.", + "icon": "lingkunganSehat" + }, + { + "id": "bnlneq3zg0000vn8issps3gi0", + "name": "Pelatihan/Sosialisasi Peduli Lingkungan", + "jumlah": "1 paket kegiatan", + "deskripsi": "Pelatihan dan sosialisasi penyuluhan tentang kepedulian lingkungan hidup dan kehutanan bagi masyarakat.", + "icon": "pelatihan" + }, + { + "id": "bnlneq3zg0000vn8issps4hj2", + "name": "Pemanfaatan Pekarangan Rumah", + "jumlah": "1 paket kegiatan", + "deskripsi": "Program pemanfaatan lingkungan pekarangan rumah menjadi produktif untuk mendukung ketahanan pangan keluarga.", + "icon": "sumberOksigen" + }, + { + "id": "bnlneq3zg0000vn8issps5ik3", + "name": "Pengelolaan Daerah Aliran Sungai", + "jumlah": "1 paket kegiatan", + "deskripsi": "Kegiatan pengelolaan dan pemeliharaan daerah aliran sungai untuk menjaga kualitas sumber air dan mencegah erosi.", + "icon": "air" + }, + { + "id": "bnlneq3zg0000vn8issps6jl4", + "name": "Pengelolaan Sampah Desa (Inovasi CINtA)", + "jumlah": "1 paket kegiatan", + "deskripsi": "Program inovatif cara indah tangani sampah (CINtA) di desa Darmasaba termasuk TPS3R dan pemilahan sampah berbasis rumah tangga.", + "icon": "sampah" + } +] diff --git a/prisma/data/lingkungan/gotong-royong/gotong-royong.json b/prisma/data/lingkungan/gotong-royong/gotong-royong.json new file mode 100644 index 00000000..b03662dc --- /dev/null +++ b/prisma/data/lingkungan/gotong-royong/gotong-royong.json @@ -0,0 +1,35 @@ +[ + { + "id": "ee38b9ee-88ca-4a11-8fa0-7d407ea7e774", + "judul": "Gotong Royong PKK Mareresik Pura Desa dan Pura Dalem", + "deskripsiSingkat": "TP PKK Desa Darmasaba melaksanakan gotong royong mareresik di Pura Desa dan Pura Dalem Desa Adat Tegal.", + "deskripsiLengkap": "Pada tanggal 28 Januari 2024, TP PKK Desa Darmasaba bersama Ketua Kelompok PKK Banjar se-Desa Darmasaba melakukan kegiatan gotong royong membersihkan area Pura Desa dan Pura Dalem untuk mendukung kebersihan dan kesehatan lingkungan desa.", + "tanggal": "2024-01-28T00:00:00.000Z", + "lokasi": "Pura Desa dan Pura Dalem, Desa Adat Tegal, Desa Darmasaba, Badung", + "partisipan": 30, + "imageName": "YgOX5qAP3O1PHG5XmQXkr-mobile.webp", + "kategoriKegiatanId": "cmknan39v000004l8eiql149r" + }, + { + "id": "dead9da0-e2e2-494c-9f88-721f2c3aa62a", + "judul": "Mareresik (Gotong Royong)", + "deskripsiSingkat": "Kegiatan mareresik (gotong royong) yang dilaksanakan di lingkungan desa Darmasaba.", + "deskripsiLengkap": "Mareresik (gotong royong) dilakukan pada tanggal 17 November 2023 oleh warga Desa Darmasaba sebagai bagian dari tradisi membersihkan dan menjaga lingkungan desa secara kolektif.", + "tanggal": "2023-11-17T00:00:00.000Z", + "lokasi": "Desa Darmasaba, Badung", + "partisipan": 25, + "imageName": "qxqSDHe-akIRi1EkQFUbG-mobile.webp", + "kategoriKegiatanId": "cmknan39v000004l8eiql149r" + }, + { + "id": "5186e23b-28a6-4f64-89b8-43a35e5048a5", + "judul": "Gotong Royong dan Rapat Rutin TP PKK Desa Darmasaba", + "deskripsiSingkat": "Gotong royong bersama TP PKK Desa Darmasaba di Pura Dalem Kangin Desa Adat Tegal sekaligus rapat rutin.", + "deskripsiLengkap": "Pada tanggal 26 Mei 2022, TP PKK Desa Darmasaba bersama anggota se-Desa Darmasaba melaksanakan gotong royong mereresik Pura Dalem Kangin Desa Adat Tegal dan dilanjutkan dengan rapat rutin TP PKK sebagai wujud semangat kerja bersama dan kebersamaan sosial.", + "tanggal": "2022-05-26T00:00:00.000Z", + "lokasi": "Pura Dalem Kangin, Desa Adat Tegal, Desa Darmasaba, Badung", + "partisipan": 28, + "imageName": "iHTVkQZ1VdkMOXLt5qdAd-mobile.webp", + "kategoriKegiatanId": "cmknan39v000004l8eiql149r" + } +] diff --git a/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json index 874e2e32..39605ca1 100644 --- a/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json +++ b/prisma/data/lingkungan/gotong-royong/kategori-gotong-royong.json @@ -1,6 +1,6 @@ [ - { "nama": "Kebersihan" }, - { "nama": "Infrastruktur" }, - { "nama": "Sosial" }, - { "nama": "Lingkungan" } - ] \ No newline at end of file + { "id": "cmknan39v000004l8eiql816o", "nama": "Kebersihan" }, + { "id": "cmknan39v000004l8eiql927p", "nama": "Infrastruktur" }, + { "id": "cmknan39v000004l8eiql038q", "nama": "Sosial" }, + { "id": "cmknan39v000004l8eiql149r", "nama": "Lingkungan" } +] diff --git a/prisma/data/lingkungan/pengelolaan-sampah/keterangan-bank-sampah.json b/prisma/data/lingkungan/pengelolaan-sampah/keterangan-bank-sampah.json new file mode 100644 index 00000000..3275fce1 --- /dev/null +++ b/prisma/data/lingkungan/pengelolaan-sampah/keterangan-bank-sampah.json @@ -0,0 +1,20 @@ +[ + { + "id": "cmkm8z5v70000vnrmu60qyd7b", + "name": "TPS 3R Pudak Mesari", + "alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali 80352", + "namaTempatMaps": "TPS 3R Pudak Mesari", + "linkPetunjukArah": "https://www.google.com/maps/dir/?api=1&destination=-8.5680,115.2040", + "lat": -8.5680, + "lng": 115.2040 + }, + { + "id": "dummy0000000000000000000001", + "name": "Bank Sampah Darmasaba Mandiri", + "alamat": "Banjar Darmasaba Tengah, Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali", + "namaTempatMaps": "Bank Sampah Darmasaba Mandiri", + "linkPetunjukArah": "https://www.google.com/maps/dir/?api=1&destination=-8.5670,115.2000", + "lat": -8.5670, + "lng": 115.2000 + } +] diff --git a/prisma/data/lingkungan/pengelolaan-sampah/pengelolaan-sampah.json b/prisma/data/lingkungan/pengelolaan-sampah/pengelolaan-sampah.json new file mode 100644 index 00000000..7e3feab0 --- /dev/null +++ b/prisma/data/lingkungan/pengelolaan-sampah/pengelolaan-sampah.json @@ -0,0 +1,27 @@ +[ + { + "id": "cmkkshcox000504l88lp54coc", + "name": "Pilah sampah sesuai jenisnya", + "icon": "trash" + }, + { + "id": "cmkkshcox000504l99mq65dpd", + "name": "Bawa sampah ke Bank Sampah", + "icon": "truck" + }, + { + "id": "cmkkshcox000504l00nr76eqe", + "name": "Timbang sampah di Bank Sampah", + "icon": "scale" + }, + { + "id": "cmkkshcox000504l11os87frf", + "name": "Catat hasil timbangan di buku tabungan", + "icon": "clipboard" + }, + { + "id": "cmkkshcox000504l22pt98gsg", + "name": "Sampah didaur ulang oleh petugas Bank Sampah", + "icon": "sampah" + } +] diff --git a/prisma/data/lingkungan/program-penghijauan/program-penghijauan.json b/prisma/data/lingkungan/program-penghijauan/program-penghijauan.json new file mode 100644 index 00000000..9fdc655c --- /dev/null +++ b/prisma/data/lingkungan/program-penghijauan/program-penghijauan.json @@ -0,0 +1,23 @@ +[ + { + "id": "cmkmdp3zg0000vn8issps1eg8", + "name": "Penghijauan Desa Darmasaba", + "judul": "Program Penghijauan Lingkungan", + "deskripsi": "

Program penghijauan desa Darmasaba bertujuan menanam pohon di area publik dan pinggiran jalan desa sebagai upaya meningkatkan kualitas lingkungan, menyerap karbon, dan memperindah desa.

", + "icon": "pohon" + }, + { + "id": "cmkmdp3zg0000vn8isspt2fh9", + "name": "Komunitas Tanam Pohon", + "judul": "Gerakan Komunitas Tanam Pohon", + "deskripsi": "

Pelibatan warga desa dalam kegiatan rutin penanaman pohon di area perbukitan dan ruang terbuka hijau untuk meningkatkan kesadaran lingkungan dan mengurangi erosi.

", + "icon": "pohon" + }, + { + "id": "cmkmdp3zg0000vn8isspu3gi0", + "name": "Taman Hijau Bersama", + "judul": "Pembangunan Taman Hijau Desa", + "deskripsi": "

Pembangunan taman hijau di beberapa titik strategis desa, melibatkan banjar adat dan kelompok pemuda untuk menciptakan ruang publik yang nyaman dan ramah lingkungan.

", + "icon": "pohon" + } +] diff --git a/prisma/data/pendidikan/data-pendidikan/data-pendidikan.json b/prisma/data/pendidikan/data-pendidikan/data-pendidikan.json new file mode 100644 index 00000000..4e5b64ed --- /dev/null +++ b/prisma/data/pendidikan/data-pendidikan/data-pendidikan.json @@ -0,0 +1,22 @@ +[ + { + "id": "cmkqmqbv30000vn84kf0ogf61", + "name": "TK", + "jumlah": "120" + }, + { + "id": "cmkqmr20h0001vn84jxtuukfk", + "name": "SD", + "jumlah": "874" + }, + { + "id": "cmkqmshcx0002vn84ufnz9mue", + "name": "SMP", + "jumlah": "50" + }, + { + "id": "cmkqmsuoc0003vn84glk1d0rc", + "name": "SMA", + "jumlah": "862" + } +] diff --git a/prisma/data/pendidikan/info-sekolah/lembaga.json b/prisma/data/pendidikan/info-sekolah/lembaga.json new file mode 100644 index 00000000..7bae725d --- /dev/null +++ b/prisma/data/pendidikan/info-sekolah/lembaga.json @@ -0,0 +1,97 @@ +[ + { + "id": "cmghqwjs4000404l8c6vwd200", + "nama": "TK Widya Kumara", + "jenjangId": "cmghqwjs4000404l8c5uvc301" + }, + { + "id": "cmghqwjs4000404l8c6vwd202", + "nama": "TK Widya Sari", + "jenjangId": "cmghqwjs4000404l8c5uvc301" + }, + { + "id": "cmghqwjs4000404l8c6vwd203", + "nama": "TK Kuntala Dewi I", + "jenjangId": "cmghqwjs4000404l8c5uvc301" + }, + { + "id": "cmghqwjs4000404l8c6vwd204", + "nama": "TK Widya Kumarayasa", + "jenjangId": "cmghqwjs4000404l8c5uvc301" + }, + { + "id": "cmghqwjs4000404l8c6vwd205", + "nama": "TK Dewi Ganadwati", + "jenjangId": "cmghqwjs4000404l8c5uvc301" + }, + { + "id": "cmghqwjs4000404l8c6vwd400", + "nama": "SD No. 1 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd300", + "nama": "SD No. 2 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd401", + "nama": "SD No. 3 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd402", + "nama": "SD No. 4 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd403", + "nama": "SD No. 5 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd404", + "nama": "SD No. 6 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd405", + "nama": "SD No. 7 Darmasaba", + "jenjangId": "cmghqwjs4000404l8c5uvc302" + }, + { + "id": "cmghqwjs4000404l8c6vwd406", + "nama": "SMP Negeri 1 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc303" + }, + { + "id": "cmghqwjs4000404l8c6vwd407", + "nama": "SMP Negeri 2 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc303" + }, + { + "id": "cmghqwjs4000404l8c6vwd408", + "nama": "SMP Negeri 3 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc303" + }, + { + "id": "cmghqwjs4000404l8c6vwd409", + "nama": "SMP Negeri 4 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc303" + }, + { + "id": "cmghqwjs4000404l8c6vwd410", + "nama": "SMP Negeri 5 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc303" + }, + { + "id": "cmghqwjs4000404l8c6vwd411", + "nama": "SMA Negeri 1 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc304" + }, + { + "id": "cmghqwjs4000404l8c6vwd412", + "nama": "SMA Negeri 2 Abiansemal", + "jenjangId": "cmghqwjs4000404l8c5uvc304" + } +] diff --git a/prisma/data/pendidikan/info-sekolah/pengajar.json b/prisma/data/pendidikan/info-sekolah/pengajar.json new file mode 100644 index 00000000..2a82cdfc --- /dev/null +++ b/prisma/data/pendidikan/info-sekolah/pengajar.json @@ -0,0 +1,967 @@ +[ + { + "id": "dnkowzpeu000004l7exka3arm", + "nama": "I Kadek Ariyasa", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3brm", + "nama": "Ni Luh Sinta Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3crm", + "nama": "I Made Dharma Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3drm", + "nama": "Ni Kadek Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3erm", + "nama": "I Komang Aditya", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3frm", + "nama": "Ni Made Intan Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3grm", + "nama": "I Putu Bayu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3hrm", + "nama": "Ni Ketut Sri Wahyuni", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3irm", + "nama": "I Nyoman Yoga Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "dnkowzpeu000004l7exka3jrm", + "nama": "Ni Komang Ratna Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + + { + "id": "dnkox31m7000004lagpwk9z5r", + "nama": "I Made Satria", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z1r", + "nama": "Ni Luh Putri Ayu", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z2r", + "nama": "I Kadek Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z3r", + "nama": "Ni Made Rani Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z4r", + "nama": "I Komang Yuda Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z5r", + "nama": "Ni Kadek Ayu Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z6r", + "nama": "I Putu Ardi Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z7r", + "nama": "Ni Ketut Melati", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z8r", + "nama": "I Nyoman Agung", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "dnkox31m7000004lagpwk9z9r", + "nama": "Ni Komang Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + + { + "id": "dnkox64cu000104la7mge67yy", + "nama": "I Made Arya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67zz", + "nama": "Ni Luh Putu Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67aa", + "nama": "I Komang Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67bb", + "nama": "Ni Kadek Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67cc", + "nama": "I Putu Gede Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67dd", + "nama": "Ni Made Cahya Utami", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67ee", + "nama": "I Nyoman Aditya Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67ff", + "nama": "Ni Komang Ratna Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67gg", + "nama": "I Kadek Bima Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "dnkox64cu000104la7mge67hh", + "nama": "Ni Putu Ayu Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + + { + "id": "dnkox9520000204la1ans1pag", + "nama": "I Made Yoga Wirawan", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pbg", + "nama": "Ni Luh Desi Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pcg", + "nama": "I Komang Putra Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pdg", + "nama": "Ni Kadek Intan Paramitha", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1peg", + "nama": "I Putu Adi Wicaksana", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pfg", + "nama": "Ni Made Ayu Kirana", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1phg", + "nama": "I Nyoman Bayu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pih", + "nama": "Ni Komang Sri Laksmi", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pjh", + "nama": "I Kadek Rama Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "dnkox9520000204la1ans1pjk", + "nama": "Ni Putu Diah Anggraini", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + + { + "id": "dnkox9520000204la1ans1pzg", + "nama": "I Made Krisna Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzh", + "nama": "Ni Luh Ayu Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzi", + "nama": "I Komang Yuda Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzj", + "nama": "Ni Kadek Putri Anjani", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzk", + "nama": "I Putu Surya Adi", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzl", + "nama": "Ni Made Ayu Cahyani", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzm", + "nama": "I Nyoman Artha Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzn", + "nama": "Ni Komang Sinta Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzo", + "nama": "I Kadek Gede Pranaya", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "dnkox9520000204la1ans1pzp", + "nama": "Ni Putu Ratih Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + + { + "id": "dnkoxzl8u000704la0jla6y5a", + "nama": "I Made Arya Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5b", + "nama": "I Nyoman Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5c", + "nama": "I Kadek Surya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5d", + "nama": "I Komang Aditya Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5e", + "nama": "Ni Luh Putri Ayu", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5f", + "nama": "Ni Made Intan Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5g", + "nama": "I Gede Yoga Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5h", + "nama": "Ni Kadek Sri Wahyuni", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5i", + "nama": "I Putu Andika Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "dnkoxzl8u000704la0jla6y5j", + "nama": "Ni Komang Diah Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + + { + "id": "dnkoy1mvk000804ladmmq5qq1", + "nama": "I Made Bima Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq2", + "nama": "I Nyoman Rizky Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq3", + "nama": "I Kadek Wahyu Nugraha", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq4", + "nama": "I Komang Fajar Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq5", + "nama": "Ni Luh Sinta Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq6", + "nama": "Ni Made Putu Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq7", + "nama": "I Gede Arjuna Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq8", + "nama": "Ni Kadek Ayu Puspita", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq9", + "nama": "I Putu Danu Kresna", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "dnkoy1mvk000804ladmmq5qq0", + "nama": "Ni Komang Ratna Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + + { + "id": "dnkoy2m6j000904la2fbthtda", + "nama": "I Made Yoga Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdb", + "nama": "I Nyoman Bayu Adnyana", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdc", + "nama": "I Kadek Rama Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdd", + "nama": "I Komang Agus Prabawa", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtde", + "nama": "Ni Luh Ayu Citra Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdf", + "nama": "Ni Made Sari Indrayani", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdg", + "nama": "I Gede Mahesa Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdh", + "nama": "Ni Kadek Purnami Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdi", + "nama": "I Putu Arta Gunawan", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "dnkoy2m6j000904la2fbthtdj", + "nama": "Ni Komang Indah Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + + { + "id": "dnkoy5cs5000a04la9jlcbpya", + "nama": "I Made Arya Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyb", + "nama": "I Nyoman Dika Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyc", + "nama": "I Kadek Putra Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyd", + "nama": "I Komang Agus Setiawan", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpye", + "nama": "Ni Luh Putu Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyf", + "nama": "Ni Kadek Ayu Puspita", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyg", + "nama": "I Putu Yoga Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyh", + "nama": "Ni Komang Dwi Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyi", + "nama": "I Made Bayu Kurniawan", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "dnkoy5cs5000a04la9jlcbpyj", + "nama": "Ni Putu Ayu Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + + { + "id": "dnkoy6dtp000b04la1zlo60ua", + "nama": "I Made Dewa Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60ub", + "nama": "I Nyoman Surya Adnyana", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60uc", + "nama": "I Kadek Bima Santosa", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60ud", + "nama": "I Komang Arta Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60ue", + "nama": "Ni Luh Made Pertiwi", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60uf", + "nama": "Ni Kadek Citra Laksmi", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60ug", + "nama": "I Putu Rangga Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60uh", + "nama": "Ni Komang Rina Apriyani", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60ui", + "nama": "I Made Aditya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "dnkoy6dtp000b04la1zlo60uj", + "nama": "Ni Putu Ayu Cahyaningrum", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + + { + "id": "dnkoy76ux000c04lags2adcaa", + "nama": "I Made Krisna Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcab", + "nama": "I Nyoman Gede Putrawan", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcac", + "nama": "I Kadek Yoga Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcad", + "nama": "I Komang Danu Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcae", + "nama": "Ni Luh Ayu Sinta Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcaf", + "nama": "Ni Kadek Intan Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcag", + "nama": "I Putu Wira Gunawan", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcah", + "nama": "Ni Komang Ayu Ratnasari", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcai", + "nama": "I Made Fajar Nugraha", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy76ux000c04lags2adcaj", + "nama": "Ni Putu Desi Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "dnkoy85nz000d04lahr5hepn1", + "nama": "I Made Arya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn2", + "nama": "I Nyoman Surya Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn3", + "nama": "I Komang Aditya Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn4", + "nama": "I Putu Gede Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn5", + "nama": "Ni Luh Putri Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn6", + "nama": "Ni Kadek Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn7", + "nama": "I Made Yoga Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn8", + "nama": "I Nyoman Wira Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn9", + "nama": "Ni Komang Diah Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "dnkoy85nz000d04lahr5hepn0", + "nama": "I Putu Bayu Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + + { + "id": "dnkoy91wc000e04la966390na", + "nama": "I Made Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390nb", + "nama": "I Nyoman Agus Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390nc", + "nama": "I Komang Yuda Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390nd", + "nama": "I Putu Gede Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390ne", + "nama": "Ni Luh Desi Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390nf", + "nama": "Ni Kadek Ayu Prameswari", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390ng", + "nama": "I Made Rizky Ananta", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390nh", + "nama": "I Nyoman Dika Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390ni", + "nama": "Ni Komang Puspita Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "dnkoy91wc000e04la966390nj", + "nama": "I Putu Andika Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + + { + "id": "dnkoy9zzy000f04la7rze3fi1", + "nama": "I Made Wahyu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi2", + "nama": "I Nyoman Kevin Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi3", + "nama": "I Komang Arta Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi4", + "nama": "I Putu Gede Satya Wibawa", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi5", + "nama": "Ni Luh Ayu Citra Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi6", + "nama": "Ni Kadek Putri Anggraini", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi7", + "nama": "I Made Fajar Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi8", + "nama": "I Nyoman Rama Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi9", + "nama": "Ni Komang Sinta Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoy9zzy000f04la7rze3fi0", + "nama": "I Putu Dimas Pradipta", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "dnkoyapqt000g04laaob10wya", + "nama": "I Made Arya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyb", + "nama": "I Nyoman Adi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyc", + "nama": "I Wayan Putra Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyd", + "nama": "Ni Kadek Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyf", + "nama": "I Komang Yoga Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyg", + "nama": "Ni Luh Putu Anggreni", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyh", + "nama": "I Made Dewa Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyi", + "nama": "Ni Putu Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyj", + "nama": "I Nyoman Agus Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "dnkoyapqt000g04laaob10wyk", + "nama": "Ni Komang Ratih Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + + { + "id": "dnkoybh2p000h04lahbds4wc1", + "nama": "I Wayan Surya Dharma", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc2", + "nama": "Ni Kadek Ayu Prameswari", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc3", + "nama": "I Made Gede Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc4", + "nama": "I Nyoman Krisna Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc5", + "nama": "Ni Luh Desi Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc6", + "nama": "I Komang Bima Santosa", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc7", + "nama": "Ni Putu Cahya Utami", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc8", + "nama": "I Wayan Dimas Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc9", + "nama": "Ni Kadek Purnama Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "dnkoybh2p000h04lahbds4wc0", + "nama": "I Made Yoga Kencana", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + + { + "id": "dnkoyc6ij000i04la3r8i7f1a", + "nama": "I Nyoman Gede Sapta", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1b", + "nama": "Ni Luh Ayu Pertiwi", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1c", + "nama": "I Wayan Danu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1d", + "nama": "Ni Kadek Melati Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1e", + "nama": "I Made Raka Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1f", + "nama": "Ni Komang Sinta Laksmi", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1g", + "nama": "I Nyoman Arta Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1h", + "nama": "Ni Putu Indah Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1i", + "nama": "I Wayan Bagus Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoyc6ij000i04la3r8i7f1j", + "nama": "Ni Kadek Ayu Wulandari", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "dnkoycyfl000j04la757zg9ba", + "nama": "I Made Arya Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bb", + "nama": "I Komang Adi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bc", + "nama": "I Nyoman Dewa Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bd", + "nama": "I Ketut Agus Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9be", + "nama": "Ni Luh Putu Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bf", + "nama": "Ni Kadek Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bg", + "nama": "I Made Surya Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bh", + "nama": "I Komang Yoga Pradipta", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bi", + "nama": "Ni Putu Citra Wulandari", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "dnkoycyfl000j04la757zg9bj", + "nama": "I Nyoman Bayu Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + + { + "id": "dnkoydnnl000k04lae6jvhbfa", + "nama": "I Made Gede Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfb", + "nama": "I Komang Krisna Yudha", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfc", + "nama": "I Nyoman Putra Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfd", + "nama": "I Ketut Wira Adnyana", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfe", + "nama": "Ni Luh Kadek Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbff", + "nama": "Ni Made Ayu Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfg", + "nama": "I Putu Yoga Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfh", + "nama": "I Komang Dimas Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfi", + "nama": "Ni Kadek Sinta Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "dnkoydnnl000k04lae6jvhbfj", + "nama": "I Nyoman Arta Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + } +] diff --git a/prisma/data/pendidikan/info-sekolah/siswa.json b/prisma/data/pendidikan/info-sekolah/siswa.json new file mode 100644 index 00000000..9068ddf3 --- /dev/null +++ b/prisma/data/pendidikan/info-sekolah/siswa.json @@ -0,0 +1,967 @@ +[ + { + "id": "cmkowzpeu000004l7exka3arm", + "nama": "I Kadek Ariyasa", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3brm", + "nama": "Ni Luh Sinta Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3crm", + "nama": "I Made Dharma Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3drm", + "nama": "Ni Kadek Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3erm", + "nama": "I Komang Aditya", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3frm", + "nama": "Ni Made Intan Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3grm", + "nama": "I Putu Bayu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3hrm", + "nama": "Ni Ketut Sri Wahyuni", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3irm", + "nama": "I Nyoman Yoga Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + { + "id": "cmkowzpeu000004l7exka3jrm", + "nama": "Ni Komang Ratna Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd200" + }, + + { + "id": "cmkox31m7000004lagpwk9z5r", + "nama": "I Made Satria", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z1r", + "nama": "Ni Luh Putri Ayu", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z2r", + "nama": "I Kadek Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z3r", + "nama": "Ni Made Rani Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z4r", + "nama": "I Komang Yuda Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z5r", + "nama": "Ni Kadek Ayu Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z6r", + "nama": "I Putu Ardi Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z7r", + "nama": "Ni Ketut Melati", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z8r", + "nama": "I Nyoman Agung", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + { + "id": "cmkox31m7000004lagpwk9z9r", + "nama": "Ni Komang Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd202" + }, + + { + "id": "cmkox64cu000104la7mge67yy", + "nama": "I Made Arya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67zz", + "nama": "Ni Luh Putu Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67aa", + "nama": "I Komang Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67bb", + "nama": "Ni Kadek Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67cc", + "nama": "I Putu Gede Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67dd", + "nama": "Ni Made Cahya Utami", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67ee", + "nama": "I Nyoman Aditya Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67ff", + "nama": "Ni Komang Ratna Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67gg", + "nama": "I Kadek Bima Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + { + "id": "cmkox64cu000104la7mge67hh", + "nama": "Ni Putu Ayu Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd203" + }, + + { + "id": "cmkox9520000204la1ans1pag", + "nama": "I Made Yoga Wirawan", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pbg", + "nama": "Ni Luh Desi Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pcg", + "nama": "I Komang Putra Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pdg", + "nama": "Ni Kadek Intan Paramitha", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1peg", + "nama": "I Putu Adi Wicaksana", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pfg", + "nama": "Ni Made Ayu Kirana", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1phg", + "nama": "I Nyoman Bayu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pih", + "nama": "Ni Komang Sri Laksmi", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pjh", + "nama": "I Kadek Rama Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + { + "id": "cmkox9520000204la1ans1pjk", + "nama": "Ni Putu Diah Anggraini", + "lembagaId": "cmghqwjs4000404l8c6vwd204" + }, + + { + "id": "cmkox9520000204la1ans1pzg", + "nama": "I Made Krisna Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzh", + "nama": "Ni Luh Ayu Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzi", + "nama": "I Komang Yuda Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzj", + "nama": "Ni Kadek Putri Anjani", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzk", + "nama": "I Putu Surya Adi", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzl", + "nama": "Ni Made Ayu Cahyani", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzm", + "nama": "I Nyoman Artha Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzn", + "nama": "Ni Komang Sinta Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzo", + "nama": "I Kadek Gede Pranaya", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + { + "id": "cmkox9520000204la1ans1pzp", + "nama": "Ni Putu Ratih Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd205" + }, + + { + "id": "cmkoxzl8u000704la0jla6y5a", + "nama": "I Made Arya Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5b", + "nama": "I Nyoman Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5c", + "nama": "I Kadek Surya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5d", + "nama": "I Komang Aditya Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5e", + "nama": "Ni Luh Putri Ayu", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5f", + "nama": "Ni Made Intan Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5g", + "nama": "I Gede Yoga Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5h", + "nama": "Ni Kadek Sri Wahyuni", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5i", + "nama": "I Putu Andika Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + { + "id": "cmkoxzl8u000704la0jla6y5j", + "nama": "Ni Komang Diah Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd400" + }, + + { + "id": "cmkoy1mvk000804ladmmq5qq1", + "nama": "I Made Bima Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq2", + "nama": "I Nyoman Rizky Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq3", + "nama": "I Kadek Wahyu Nugraha", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq4", + "nama": "I Komang Fajar Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq5", + "nama": "Ni Luh Sinta Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq6", + "nama": "Ni Made Putu Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq7", + "nama": "I Gede Arjuna Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq8", + "nama": "Ni Kadek Ayu Puspita", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq9", + "nama": "I Putu Danu Kresna", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + { + "id": "cmkoy1mvk000804ladmmq5qq0", + "nama": "Ni Komang Ratna Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd300" + }, + + { + "id": "cmkoy2m6j000904la2fbthtda", + "nama": "I Made Yoga Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdb", + "nama": "I Nyoman Bayu Adnyana", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdc", + "nama": "I Kadek Rama Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdd", + "nama": "I Komang Agus Prabawa", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtde", + "nama": "Ni Luh Ayu Citra Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdf", + "nama": "Ni Made Sari Indrayani", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdg", + "nama": "I Gede Mahesa Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdh", + "nama": "Ni Kadek Purnami Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdi", + "nama": "I Putu Arta Gunawan", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + { + "id": "cmkoy2m6j000904la2fbthtdj", + "nama": "Ni Komang Indah Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd401" + }, + + { + "id": "cmkoy5cs5000a04la9jlcbpya", + "nama": "I Made Arya Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyb", + "nama": "I Nyoman Dika Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyc", + "nama": "I Kadek Putra Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyd", + "nama": "I Komang Agus Setiawan", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpye", + "nama": "Ni Luh Putu Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyf", + "nama": "Ni Kadek Ayu Puspita", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyg", + "nama": "I Putu Yoga Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyh", + "nama": "Ni Komang Dwi Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyi", + "nama": "I Made Bayu Kurniawan", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + { + "id": "cmkoy5cs5000a04la9jlcbpyj", + "nama": "Ni Putu Ayu Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd402" + }, + + { + "id": "cmkoy6dtp000b04la1zlo60ua", + "nama": "I Made Dewa Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60ub", + "nama": "I Nyoman Surya Adnyana", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60uc", + "nama": "I Kadek Bima Santosa", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60ud", + "nama": "I Komang Arta Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60ue", + "nama": "Ni Luh Made Pertiwi", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60uf", + "nama": "Ni Kadek Citra Laksmi", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60ug", + "nama": "I Putu Rangga Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60uh", + "nama": "Ni Komang Rina Apriyani", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60ui", + "nama": "I Made Aditya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + { + "id": "cmkoy6dtp000b04la1zlo60uj", + "nama": "Ni Putu Ayu Cahyaningrum", + "lembagaId": "cmghqwjs4000404l8c6vwd403" + }, + + { + "id": "cmkoy76ux000c04lags2adcaa", + "nama": "I Made Krisna Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcab", + "nama": "I Nyoman Gede Putrawan", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcac", + "nama": "I Kadek Yoga Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcad", + "nama": "I Komang Danu Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcae", + "nama": "Ni Luh Ayu Sinta Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcaf", + "nama": "Ni Kadek Intan Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcag", + "nama": "I Putu Wira Gunawan", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcah", + "nama": "Ni Komang Ayu Ratnasari", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcai", + "nama": "I Made Fajar Nugraha", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy76ux000c04lags2adcaj", + "nama": "Ni Putu Desi Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd404" + }, + { + "id": "cmkoy85nz000d04lahr5hepn1", + "nama": "I Made Arya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn2", + "nama": "I Nyoman Surya Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn3", + "nama": "I Komang Aditya Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn4", + "nama": "I Putu Gede Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn5", + "nama": "Ni Luh Putri Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn6", + "nama": "Ni Kadek Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn7", + "nama": "I Made Yoga Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn8", + "nama": "I Nyoman Wira Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn9", + "nama": "Ni Komang Diah Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + { + "id": "cmkoy85nz000d04lahr5hepn0", + "nama": "I Putu Bayu Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd405" + }, + + { + "id": "cmkoy91wc000e04la966390na", + "nama": "I Made Dwi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390nb", + "nama": "I Nyoman Agus Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390nc", + "nama": "I Komang Yuda Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390nd", + "nama": "I Putu Gede Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390ne", + "nama": "Ni Luh Desi Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390nf", + "nama": "Ni Kadek Ayu Prameswari", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390ng", + "nama": "I Made Rizky Ananta", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390nh", + "nama": "I Nyoman Dika Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390ni", + "nama": "Ni Komang Puspita Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + { + "id": "cmkoy91wc000e04la966390nj", + "nama": "I Putu Andika Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd406" + }, + + { + "id": "cmkoy9zzy000f04la7rze3fi1", + "nama": "I Made Wahyu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi2", + "nama": "I Nyoman Kevin Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi3", + "nama": "I Komang Arta Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi4", + "nama": "I Putu Gede Satya Wibawa", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi5", + "nama": "Ni Luh Ayu Citra Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi6", + "nama": "Ni Kadek Putri Anggraini", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi7", + "nama": "I Made Fajar Pranata", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi8", + "nama": "I Nyoman Rama Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi9", + "nama": "Ni Komang Sinta Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoy9zzy000f04la7rze3fi0", + "nama": "I Putu Dimas Pradipta", + "lembagaId": "cmghqwjs4000404l8c6vwd407" + }, + { + "id": "cmkoyapqt000g04laaob10wya", + "nama": "I Made Arya Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyb", + "nama": "I Nyoman Adi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyc", + "nama": "I Wayan Putra Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyd", + "nama": "Ni Kadek Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyf", + "nama": "I Komang Yoga Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyg", + "nama": "Ni Luh Putu Anggreni", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyh", + "nama": "I Made Dewa Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyi", + "nama": "Ni Putu Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyj", + "nama": "I Nyoman Agus Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + { + "id": "cmkoyapqt000g04laaob10wyk", + "nama": "Ni Komang Ratih Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd408" + }, + + { + "id": "cmkoybh2p000h04lahbds4wc1", + "nama": "I Wayan Surya Dharma", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc2", + "nama": "Ni Kadek Ayu Prameswari", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc3", + "nama": "I Made Gede Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc4", + "nama": "I Nyoman Krisna Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc5", + "nama": "Ni Luh Desi Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc6", + "nama": "I Komang Bima Santosa", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc7", + "nama": "Ni Putu Cahya Utami", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc8", + "nama": "I Wayan Dimas Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc9", + "nama": "Ni Kadek Purnama Sari", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + { + "id": "cmkoybh2p000h04lahbds4wc0", + "nama": "I Made Yoga Kencana", + "lembagaId": "cmghqwjs4000404l8c6vwd409" + }, + + { + "id": "cmkoyc6ij000i04la3r8i7f1a", + "nama": "I Nyoman Gede Sapta", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1b", + "nama": "Ni Luh Ayu Pertiwi", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1c", + "nama": "I Wayan Danu Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1d", + "nama": "Ni Kadek Melati Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1e", + "nama": "I Made Raka Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1f", + "nama": "Ni Komang Sinta Laksmi", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1g", + "nama": "I Nyoman Arta Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1h", + "nama": "Ni Putu Indah Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1i", + "nama": "I Wayan Bagus Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoyc6ij000i04la3r8i7f1j", + "nama": "Ni Kadek Ayu Wulandari", + "lembagaId": "cmghqwjs4000404l8c6vwd410" + }, + { + "id": "cmkoycyfl000j04la757zg9ba", + "nama": "I Made Arya Putra", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bb", + "nama": "I Komang Adi Saputra", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bc", + "nama": "I Nyoman Dewa Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bd", + "nama": "I Ketut Agus Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9be", + "nama": "Ni Luh Putu Sari Dewi", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bf", + "nama": "Ni Kadek Ayu Lestari", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bg", + "nama": "I Made Surya Mahendra", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bh", + "nama": "I Komang Yoga Pradipta", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bi", + "nama": "Ni Putu Citra Wulandari", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + { + "id": "cmkoycyfl000j04la757zg9bj", + "nama": "I Nyoman Bayu Pramana", + "lembagaId": "cmghqwjs4000404l8c6vwd411" + }, + + { + "id": "cmkoydnnl000k04lae6jvhbfa", + "nama": "I Made Gede Pratama", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfb", + "nama": "I Komang Krisna Yudha", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfc", + "nama": "I Nyoman Putra Santika", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfd", + "nama": "I Ketut Wira Adnyana", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfe", + "nama": "Ni Luh Kadek Maharani", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbff", + "nama": "Ni Made Ayu Purnami", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfg", + "nama": "I Putu Yoga Mahardika", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfh", + "nama": "I Komang Dimas Prasetya", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfi", + "nama": "Ni Kadek Sinta Permata", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + }, + { + "id": "cmkoydnnl000k04lae6jvhbfj", + "nama": "I Nyoman Arta Wijaya", + "lembagaId": "cmghqwjs4000404l8c6vwd412" + } +] diff --git a/prisma/data/pendidikan/perpustakaan-digital/kategori-buku.json b/prisma/data/pendidikan/perpustakaan-digital/kategori-buku.json new file mode 100644 index 00000000..71e58601 --- /dev/null +++ b/prisma/data/pendidikan/perpustakaan-digital/kategori-buku.json @@ -0,0 +1,47 @@ +[ + { + "id": "cmkqb11mc000104jibq76bdzu", + "name": "Fiksi" + }, + { + "id": "cmkqb11mc000104jibq87bdzu", + "name": "Non Fiksi" + }, + { + "id": "cmkqb11mc000104jibq97bdzu", + "name": "Pendidikan" + }, + { + "id": "cmkqb11mc000104jibqa7bdzu", + "name": "Ilmiah" + }, + { + "id": "cmkqb11mc000104jibqb7bdzu", + "name": "Drama" + }, + { + "id": "cmkqb11mc000104jibqc7bdzu", + "name": "Sejarah" + }, + { + "id": "cmkqb11mc000104jibqd7bdzu", + "name": "Teknologi" + }, + { + "id": "cmkqb11mc000104jibqe7bdzu", + "name": "Agama" + }, + { + "id": "cmkqb11mc000104jibqf7bdzu", + "name": "Pengembangan Diri" + }, + { + "id": "cmkqb11mc000104jibqg7bdzu", + "name": "Kesehatan" + }, + { + "id": "cmkqb11mc000104jibqh7bdzu", + "name": "Anak Dan Remaja" + } + +] \ No newline at end of file diff --git a/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json b/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json new file mode 100644 index 00000000..abfe70cd --- /dev/null +++ b/prisma/data/pendidikan/perpustakaan-digital/perpustakaan-digital.json @@ -0,0 +1,198 @@ +[ + { + "id": "cmkqhbi6f0002vneao4my49k9", + "judul": "Laskar Pelangi", + "deskripsi": "

Novel inspiratif tentang perjuangan anak-anak di Belitung dalam meraih pendidikan dan mimpi mereka

", + "kategoriId": "cmkqb11mc000104jibq76bdzu", + "imageName": "RnAdv7O0QAFrxkFLAXJSa-mobile.webp" + }, + { + "id": "cmkqhedff0005vneas3rtbumi", + "judul": "Bumi Manusia", + "deskripsi": "

Kisah kehidupan Minke di masa kolonial yang menggambarkan perjuangan, pendidikan, dan identitas bangsa

", + "kategoriId": "cmkqb11mc000104jibqc7bdzu", + "imageName": "71eZShq4FYAFLxpLfZB0W-mobile.webp" + }, + { + "id": "cmkqhg1g70008vneajbpz8phh", + "judul": "Atomic Habits", + "deskripsi": "

Panduan membangun kebiasaan kecil yang konsisten untuk menghasilkan perubahan besar dalam hidup

", + "kategoriId": "cmkqb11mc000104jibqf7bdzu", + "imageName": "Uxq3GXPqh7HN9fHmRkr3r-mobile.webp" + }, + { + "id": "cmkqhl6sr000bvneampx0svus", + "judul": "Clean Code", + "deskripsi": "

Buku wajib programmer tentang cara menulis kode yang bersih, mudah dibaca, dan mudah dirawat

", + "kategoriId": "cmkqb11mc000104jibqd7bdzu", + "imageName": "W5Fc0uRADNkIY3nZicvQA-mobile.webp" + }, + { + "id": "cmkqhoaa1000evnearppgpyxo", + "judul": "Sejarah Indonesia Modern", + "deskripsi": "

Membahas perjalanan sejarah Indonesia dari masa kolonial hingga era modern

", + "kategoriId": "cmkqb11mc000104jibqc7bdzu", + "imageName": "mp77Op-MwtPQZnH3so4JY-mobile.webp" + }, + { + "id": "cmkqhr9oc000hvnea677ad3kb", + "judul": "Ensiklopedia Anak Pintar", + "deskripsi": "

Buku referensi bergambar yang membantu anak mengenal ilmu pengetahuan secara menyenangkan

", + "kategoriId": "cmkqb11mc000104jibqh7bdzu", + "imageName": "V09ZxN1wOwbSFLQiDK0VQ-mobile.webp" + }, + { + "id": "cmkqi5ksf000kvnea9c04n2hy", + "judul": "Filosofi Teras", + "deskripsi": "

Pengenalan filsafat Stoikisme untuk menghadapi kehidupan modern dengan lebih tenang

", + "kategoriId": "cmkqb11mc000104jibq87bdzu", + "imageName": "Wqp4AyVkGjqRMED9Q5XAs-mobile.webp" + }, + { + "id": "cmkqi97hq000nvneaparjbcrm", + "judul": "Pemrograman JavaScript Dasar", + "deskripsi": "

Panduan dasar belajar JavaScript untuk pemula dalam dunia pengembangan web

", + "kategoriId": "cmkqb11mc000104jibqd7bdzu", + "imageName": "NH4aLc7cVuutdQBCofTC0-mobile.webp" + }, + { + "id": "cmkqibjt9000qvnea13ox7fmv", + "judul": "Pendidikan Karakter", + "deskripsi": "

Buku yang membahas pentingnya pendidikan karakter dalam membentuk generasi bangsa

", + "kategoriId": "cmkqb11mc000104jibqf7bdzu", + "imageName": "MLrsPrD6oiHsrNP4Lc8J7-mobile.webp" + }, + { + "id": "cmkqidnar000tvneaohk5v8k6", + "judul": "Psikologi Kepribadian", + "deskripsi": "

Mengenal teori-teori kepribadian manusia dalam perspektif psikologi

", + "kategoriId": "cmkqb11mc000104jibq87bdzu", + "imageName": "iaIeNdhuxqltqKP7aZncQ-mobile.webp" + }, + { + "id": "cmkqifdiu000wvnea7xd0yi4f", + "judul": "Ayat-Ayat Cinta", + "deskripsi": "

Novel religi yang mengangkat kisah cinta, iman, dan perjuangan hidup

", + "kategoriId": "cmkqb11mc000104jibqe7bdzu", + "imageName": "WUDssJ59pTKE_3IuTiZ2s-mobile.webp" + }, + { + "id": "cmkqik7vi000zvneae7d5cq9i", + "judul": "Negeri 5 Menara", + "deskripsi": "

Cerita persahabatan dan perjuangan santri dalam mengejar mimpi hingga ke mancanegara

", + "kategoriId": "cmkqb11mc000104jibq76bdzu", + "imageName": "RJH_-4_R_nlP7GVEQeD1M-mobile.webp" + }, + { + "id": "cmkqinno30012vneac1sgsvis", + "judul": "Belajar UI/UX Design", + "deskripsi": "

Panduan praktis memahami desain antarmuka dan pengalaman pengguna

", + "kategoriId": "cmkqb11mc000104jibqd7bdzu", + "imageName": "9MA-Jx_36uoho2Tg40_G9-mobile.webp" + }, + { + "id": "cmkqiqegd0015vneawv5u5tpm", + "judul": "Manajemen Waktu Efektif", + "deskripsi": "

Teknik mengatur waktu agar lebih produktif dan fokus pada hal penting

", + "kategoriId": "cmkqb11mc000104jibqf7bdzu", + "imageName": "dkb7ZWFl28TREVcvH8sWd-mobile.webp" + }, + { + "id": "cmkqiurc60018vneavyd3pj9q", + "judul": "Dongeng Nusantara", + "deskripsi": "

Kumpulan dongeng tradisional Indonesia yang sarat pesan moral

", + "kategoriId": "cmkqb11mc000104jibq76bdzu", + "imageName": "nVj3one6CLuWRd04QnsWo-mobile.webp" + }, + { + "id": "cmkqix2kb001bvnea5v81cw7p", + "judul": "Ekonomi Makro", + "deskripsi": "

Pembahasan konsep ekonomi makro secara sistematis dan mudah dipahami

", + "kategoriId": "cmkqb11mc000104jibq87bdzu", + "imageName": "AnB7JO4_6tlPTX3ypOVLi-mobile.webp" + }, + { + "id": "cmkqiyts2001evneahnk45ry5", + "judul": "Seni Berpikir Kritis", + "deskripsi": "

Buku yang membantu pembaca menghindari kesalahan berpikir dalam pengambilan keputusan

", + "kategoriId": "cmkqb11mc000104jibq87bdzu", + "imageName": "sAyoMERxL6JgFfiO22KPb-mobile.webp" + }, + { + "id": "cmkqj0nq0001hvnea06r8m3kj", + "judul": "Seni Berpikir Kritis", + "deskripsi": "

Buku yang membantu pembaca menghindari kesalahan berpikir dalam pengambilan keputusan

", + "kategoriId": "cmkqb11mc000104jibq87bdzu", + "imageName": "WeA-JP2Ks_32fv1k529vj-mobile.webp" + }, + { + "id": "cmkqj37w4001kvnea04n9w2bx", + "judul": "Panduan Shalat Lengkap", + "deskripsi": "

Panduan praktis dan lengkap tentang tata cara shalat sesuai tuntunan

", + "kategoriId": "cmkqb11mc000104jibqe7bdzu", + "imageName": "pxlHu2kDmIprQqC2PuXaL-mobile.webp" + }, + { + "id": "cmkqj5qp6001nvnea4xhvluz3", + "judul": "Cerita Sains untuk Anak", + "deskripsi": "

Cerita edukatif yang mengenalkan sains kepada anak dengan bahasa sederhana

", + "kategoriId": "cmkqb11mc000104jibqh7bdzu", + "imageName": "G0iELZb2DhQDCCP5OdzJR-desktop.webp" + }, + { + "id": "cml7fq776000104jscnj58sgm", + "judul": "Pedagogy of the Oppressed", + "deskripsi": "

Klasik pemikiran pendidikan kritis; menggali hubungan guru-murid dan peran pendidikan dalam pembebasan sosial

", + "kategoriId": "cmkqb11mc000104jibq97bdzu", + "imageName": "pendidikan-1.webp" + }, + { + "id": "cml7fqurm000204js5p60hkym", + "judul": "The Courage to Teach", + "deskripsi": "

Tentang refleksi diri seorang pendidik; cocok untuk pengajar yang ingin lebih dari sekedar “metode mengajar”

", + "kategoriId": "cmkqb11mc000104jibq97bdzu", + "imageName": "pendidikan-2.webp" + }, + { + "id": "cml7fqurm000204js5p60hkzn", + "judul": "A Brief History of Time", + "deskripsi": "

Penjelasan kosmologi yang terkenal dunia; sains kompleks dibahas dengan bahasa yang bisa dinikmati pembaca umum

", + "kategoriId": "cmkqb11mc000104jibqa7bdzu", + "imageName": "ilmiah-1.webp" + }, + { + "id": "cml7fqurm000204js5p60hkao", + "judul": "The Selfish Gene", + "deskripsi": "

Membawa perspektif baru tentang evolusi melalui “gen” sebagai unit seleksi

", + "kategoriId": "cmkqb11mc000104jibqa7bdzu", + "imageName": "ilmiah-2.webp" + }, + { + "id": "cml7fx09c000304jshams3xbg", + "judul": "A Little Life", + "deskripsi": "

Novel yang menggambarkan hidup seorang remaja yang mengalami kehidupan yang sangat sulit

", + "kategoriId": "cmkqb11mc000104jibqb7bdzu", + "imageName": "drama-1.webp" + }, + { + "id": "cml7fx09c000304jshams3xch", + "judul": "Death of a Salesman", + "deskripsi": "

Drama teater klasik Amerika tentang harapan, keluarga, dan realitas hidup.

", + "kategoriId": "cmkqb11mc000104jibqb7bdzu", + "imageName": "drama-2.webp" + }, + { + "id": "cml7fx09c000304jshams3xdi", + "judul": "How Not to Die", + "deskripsi": "

Panduan berbasis penelitian tentang pola makan untuk mencegah dan menangani penyakit.

", + "kategoriId": "cmkqb11mc000104jibqg7bdzu", + "imageName": "kesehatan-1.webp" + }, + { + "id": "cml7fx09c000304jshams3xej", + "judul": "The Body Keeps the Score", + "deskripsi": "

Fokus pada trauma, otak & tubuh; penting untuk memahami kesehatan mental secara mendalam.

", + "kategoriId": "cmkqb11mc000104jibqg7bdzu", + "imageName": "kesehatan-2.webp" + } +] diff --git a/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json b/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json index 731ccdb9..795e82cb 100644 --- a/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json +++ b/prisma/data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json @@ -1,14 +1,14 @@ [ { "id": "cmeppcwzk0000vn5exmudcipd", - "jenisInformasi": "Potensi Desa", - "deskripsi": "

“Potensi desa adalah segenap sumber daya alam dan sumber daya manusia yang dimiliki desa sebagai modal dasar yang perlu dikelola dan dikembangkan bagi kelangsungan dan perkembangan desa. Adapun potensi yang dimiliki Desa Darmasaba yaitu:

  1. TPS3R Pudak Mesari

  2. Bumdes Pudak Mesari

  3. Pertanian

  4. Jogging Track Tegeh Aban, Karang Gadon dan Munduk Uma Desa

  5. Taman Beji Cengana

  6. Dam Tanah Putih

  7. Gumuh Sari Water Park

  8. UMKM

  9. Kawasan Kuliner

  10. IKM berbasis Pengolahan Pangan

  11. Genteng

  12. Peternakan Ikan Lele

  13. Pemotongan Daging”

", + "jenisInformasi": "Penyelenggaraan Informasi Publik Desa", + "deskripsi": "

Kegiatan pengadaan/pembuatan aplikasi dan pelatihan untuk kemudahan akses informasi publik desa Darmasaba melalui sarana digital sebagai bagian dari transparansi layanan masyarakat.

", "tanggal": "2021-05-25" }, { "id": "cmeppieay0001vn5e8qe658ub", - "jenisInformasi": "Layanan Surat Keterangan Desa", - "deskripsi": "

“Desa Darmasaba menyediakan berbagai jenis layanan surat keterangan untuk kebutuhan administratif, antara lain:

", + "jenisInformasi": "Kemudahan Akses Internet dan Informasi Desa", + "deskripsi": "

Penyediaan jaringan/ instalasi komunikasi dan informasi desa untuk mempermudah akses internet dan informasi kepada masyarakat desa Darmasaba, bagian dari penyelenggaraan informasi desa.

", "tanggal": "2025-02-21" } ] \ No newline at end of file diff --git a/prisma/data/ppid/profile-ppid/profilePPid.json b/prisma/data/ppid/profile-ppid/profilePPid.json index 0c6828f7..26fed553 100644 --- a/prisma/data/ppid/profile-ppid/profilePPid.json +++ b/prisma/data/ppid/profile-ppid/profilePPid.json @@ -5,6 +5,7 @@ "biodata": "

I.B Surya Prabhawa Manuaba, S.H., M.H., adalah Perbekel Darmasaba periode 2021-2027, seorang advokat, pendiri Mantra Legal Consultants & Advocates, serta aktif di bidang musik dan akademis. Dia menempuh pendidikan hukum di Universitas Udayana dan Universitas Mahasaraswati Denpasar, serta memiliki pengalaman luas di berbagai organisasi dan kepemimpinan.

", "riwayat": "", "pengalaman": "", - "unggulan": "

Pemberdayaan Ekonomi dan UMKM

" + "unggulan": "

Pemberdayaan Ekonomi dan UMKM

", + "imageName" : "t9x6bSW8WnbdPS9cx9PLC-mobile.webp" } ] diff --git a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json index 713cb799..8602e313 100644 --- a/prisma/data/ppid/struktur-ppid/pegawai-PPID.json +++ b/prisma/data/ppid/struktur-ppid/pegawai-PPID.json @@ -8,7 +8,8 @@ "telepon": "081234567891", "alamat": "Jl. Raya Desa No. 1", "posisiId": "kepala_desa", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewxfvw000004ibee5013f4", @@ -19,7 +20,8 @@ "telepon": "081234567892", "alamat": "Jl. Raya Desa No. 2", "posisiId": "sekretaris_desa", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewxvqw000104ibgm5l8fzs", @@ -30,7 +32,8 @@ "telepon": "081234567892", "alamat": "Jl. Raya Desa No. 2", "posisiId": "kaur_keuangan", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewy1g9000204ib2n7hbx0i", @@ -41,7 +44,8 @@ "telepon": "081234567892", "alamat": "Jl. Raya Desa No. 2", "posisiId": "kadus_banjar_dinas_menesa", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewybah000304ibgqhn1gm2", @@ -52,7 +56,8 @@ "telepon": "081234567893", "alamat": "Jl. Raya Desa No. 2", "posisiId": "kadus_banjar_dinas_darmasaba", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewygqz000404ib20sv8nvg", @@ -63,7 +68,8 @@ "telepon": "081234567893", "alamat": "Jl. Raya Desa No. 2", "posisiId": "kadus_banjar_dinas_bucu", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewyos1000504ibcu8o2gyk", @@ -74,7 +80,8 @@ "telepon": "081234567893", "alamat": "Jl. Raya Desa No. 2", "posisiId": "kadus_banjar_dinas_gulingan", - "isActive": true + "isActive": true, + "imageName": "" }, { "id": "cmgewyxk7000604ib8djs3i6c", @@ -85,7 +92,8 @@ "telepon": "081234567893", "alamat": "Jl. Raya Desa No. 2", "posisiId": "kadus_banjar_dinas_taman", - "isActive": true + "isActive": true, + "imageName": "" } ] \ No newline at end of file diff --git a/prisma/data/resolveImageId.ts b/prisma/data/resolveImageId.ts new file mode 100644 index 00000000..58227739 --- /dev/null +++ b/prisma/data/resolveImageId.ts @@ -0,0 +1,11 @@ +import safeImageId from "./safeImageId"; + +export default async function resolveImageIdForSeed( + existingImageId: string | null | undefined, + seedImageId: string | null | undefined +) { + if (existingImageId) return existingImageId; + + // ✅ Skip validasi saat seed + return await safeImageId(seedImageId, true); +} \ No newline at end of file diff --git a/prisma/data/safeImageId.ts b/prisma/data/safeImageId.ts new file mode 100644 index 00000000..ab01a8e6 --- /dev/null +++ b/prisma/data/safeImageId.ts @@ -0,0 +1,24 @@ +import prisma from "@/lib/prisma"; + +export default async function safeImageId( + imageId?: string | null, + skipValidation = false // ✅ tambah param +) { + if (!imageId) return null; + + if (skipValidation) { + console.log(`⚠️ Skipping validation for ${imageId} (seed mode)`); + return imageId; // langsung return tanpa cek DB + } + + const exists = await prisma.fileStorage.findUnique({ + where: { id: imageId }, + }); + + if (!exists) { + console.warn(`⚠️ imageId ${imageId} not found in FileStorage`); + return null; + } + + return imageId; +} \ No newline at end of file diff --git a/prisma/data/user/roles.json b/prisma/data/user/roles.json index b79f3928..4d2a1046 100644 --- a/prisma/data/user/roles.json +++ b/prisma/data/user/roles.json @@ -1,23 +1,32 @@ [ - { - "id": "role-1", - "name": "ADMIN DESA", - "description": "Administrator Desa", - "permissions": ["manage_users", "manage_content", "view_reports"], - "isActive": true - }, - { - "id": "role-2", - "name": "ADMIN KESEHATAN", - "description": "Administrator Bidang Kesehatan", - "permissions": ["manage_health_data", "view_reports"], - "isActive": true - }, - { - "id": "role-3", - "name": "ADMIN SEKOLAH", - "description": "Administrator Sekolah", - "permissions": ["manage_school_data", "view_reports"], - "isActive": true - } - ] \ No newline at end of file + { + "id": "0", + "name": "DEVELOPER", + "description": "Developer", + "isActive": true + }, + { + "id": "1", + "name": "SUPER ADMIN", + "description": "Administrator", + "isActive": true + }, + { + "id": "2", + "name": "ADMIN DESA", + "description": "Administrator Desa", + "isActive": true + }, + { + "id": "3", + "name": "ADMIN KESEHATAN", + "description": "Administrator Bidang Kesehatan", + "isActive": true + }, + { + "id": "4", + "name": "ADMIN PENDIDIKAN", + "description": "Administrator Bidang Pendidikan", + "isActive": true + } +] diff --git a/prisma/data/user/users.json b/prisma/data/user/users.json index eea2a98a..733aeba4 100644 --- a/prisma/data/user/users.json +++ b/prisma/data/user/users.json @@ -1,23 +1,10 @@ [ { - "id": "user-1", - "nama": "Admin Desa", - "nomor": "089647037426", - "roleId": "role-1", - "isActive": true - }, - { - "id": "user-2", - "nama": "Admin Kesehatan", - "nomor": "082339004198", - "roleId": "role-2", - "isActive": true - }, - { - "id": "user-3", - "nama": "Admin Sekolah", - "nomor": "085237157222", - "roleId": "role-3", - "isActive": true + "id": "cmie1o0zh0002vn132vtzg7hh", + "username": "SuperAdmin-Nico", + "nomor": "6289647037426", + "roleId": 0, + "isActive": true, + "sessionInvalid": false } ] diff --git a/prisma/lib/create_file_share_folder.ts b/prisma/lib/create_file_share_folder.ts new file mode 100644 index 00000000..025d0490 --- /dev/null +++ b/prisma/lib/create_file_share_folder.ts @@ -0,0 +1,246 @@ +// import { getValidAuthToken } from "../../src/lib/seafile-auth-service"; + +// type CdnItem = { +// name: string; +// path: string; +// cdnUrl: string; +// }; + +// type DirItem = { +// type: "file" | "dir"; +// name: string; +// }; + +// const BASE_URL = process.env.SEAFILE_BASE_URL!; +// const REPO_ID = process.env.SEAFILE_REPO_ID!; + +// // folder yang dishare (RELATIVE, tanpa slash depan) +// const DIR_TARGET = "asset-web"; + +// // 🔑 TOKEN DIRECTORY SHARE (/d/{token}) +// const PUBLIC_SHARE_TOKEN = process.env.SEAFILE_PUBLIC_SHARE_TOKEN!; + +// /** +// * Ambil list file dari repo (butuh token sekali) +// */ +// async function getDirItems(): Promise { +// const token = await getValidAuthToken(); + +// // Validasi bahwa semua variabel lingkungan telah diatur +// if (!BASE_URL) { +// throw new Error('SEAFILE_BASE_URL environment variable is not set'); +// } + +// if (!REPO_ID) { +// throw new Error('SEAFILE_REPO_ID environment variable is not set'); +// } + +// // Bangun URL dan pastikan valid +// const url = `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`; + +// try { +// new URL(url); // Ini akan melempar error jika URL tidak valid +// } catch (error) { +// throw new Error(`Invalid URL constructed: ${url}. Error: ${error}`); +// } + +// const res = await fetch(url, { +// headers: { +// Authorization: `Token ${token}`, +// }, +// }); + +// if (!res.ok) { +// const text = await res.text(); +// throw new Error(`Failed get dir items: ${text}`); +// } + +// return res.json(); +// } + +// /** +// * Build PUBLIC CDN URL +// */ +// function buildPublicCdnUrl(fileName: string) { +// return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent( +// fileName, +// )}&raw=1`; +// } + +// /** +// * Ambil semua PUBLIC CDN URL +// */ +// export async function getAllPublicCdnUrls(): Promise { +// const items = await getDirItems(); + +// return items +// .filter((item) => item.type === "file") +// .map((file) => { +// const path = `${DIR_TARGET}/${file.name}`; +// return { +// name: file.name, +// path, +// cdnUrl: buildPublicCdnUrl(file.name), +// }; +// }); +// } + +// /** +// * Run langsung (optional) +// */ +// if (import.meta.main) { +// const data = await getAllPublicCdnUrls(); +// console.log(data); +// } + + + +// import { getValidAuthToken } from "../../src/lib/seafile-auth-service"; + +// type CdnItem = { +// name: string; +// path: string; +// cdnUrl: string; +// }; + +// type DirItem = { +// type: "file" | "dir"; +// name: string; +// }; + +// // ✅ PAKAI ENV YANG BENAR +// const BASE_URL = process.env.SEAFILE_URL!; +// const REPO_ID = process.env.SEAFILE_REPO_ID!; +// const PUBLIC_SHARE_TOKEN = process.env.SEAFILE_PUBLIC_SHARE_TOKEN!; + +// // folder yang dishare (RELATIVE, TANPA slash depan) +// const DIR_TARGET = "asset-web"; + +// /** +// * Ambil list file dari repo (token dipakai SEKALI) +// */ +// async function getDirItems(): Promise { +// if (!BASE_URL || !REPO_ID) { +// throw new Error("SEAFILE env not configured correctly"); +// } + +// const token = await getValidAuthToken(); + +// const url = `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`; + +// const res = await fetch(url, { +// headers: { +// Authorization: `Token ${token}`, +// }, +// }); + +// if (!res.ok) { +// const text = await res.text(); +// throw new Error(`Failed get dir items: ${text}`); +// } + +// return res.json(); +// } + +// /** +// * Build PUBLIC CDN URL (DIRECTORY SHARE) +// */ +// function buildPublicCdnUrl(fileName: string) { +// const fullPath = `/${DIR_TARGET}/${fileName}`; +// return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent( +// fullPath, +// )}&raw=1`; +// } + +// /** +// * Ambil semua PUBLIC CDN URL +// */ +// export async function getAllPublicCdnUrls(): Promise { +// const items = await getDirItems(); + +// return items +// .filter((item) => item.type === "file") +// .map((file) => ({ +// name: file.name, +// path: `${DIR_TARGET}/${file.name}`, +// cdnUrl: buildPublicCdnUrl(file.name), +// })); +// } + +// /** +// * Run langsung +// */ +// if (import.meta.main) { +// const data = await getAllPublicCdnUrls(); +// console.log(data); +// } + + +import { getValidAuthToken } from "../../src/lib/seafile-auth-service"; +type CdnItem = { + name: string; + path: string; + cdnUrl: string; +}; +type DirItem = { + type: "file" | "dir"; + name: string; +}; +const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com"; +const REPO_ID = process.env.SEAFILE_REPO_ID!; +// folder yang dishare (RELATIVE, tanpa slash depan) +const DIR_TARGET = "asset-web"; +// 🔑 TOKEN DIRECTORY SHARE (/d/{token}) +const PUBLIC_SHARE_TOKEN = "3a9a9ecb5e244f4da8ae"; +/** + * Ambil list file dari repo (butuh token sekali) + */ +async function getDirItems(): Promise { + const token = await getValidAuthToken(); + const res = await fetch( + `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`, + { + headers: { + Authorization: `Token ${token}`, + }, + }, + ); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed get dir items: ${text}`); + } + return res.json(); +} +/** + * Build PUBLIC CDN URL + */ +function buildPublicCdnUrl(fileName: string) { + return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent( + fileName, + )}&raw=1`; +} +/** + * Ambil semua PUBLIC CDN URL + */ +export async function getAllPublicCdnUrls(): Promise { + const items = await getDirItems(); + return items + .filter((item) => item.type === "file") + .map((file) => { + // const path = `${DIR_TARGET}/${file.name}`; + const path = `/${file.name}`; + return { + name: file.name, + path, + cdnUrl: buildPublicCdnUrl(file.name), + }; + }); +} +/** + * Run langsung (optional) + */ +if (import.meta.main) { + const data = await getAllPublicCdnUrls(); + console.log(data); +} + diff --git a/prisma/lib/get_images.ts b/prisma/lib/get_images.ts new file mode 100644 index 00000000..dc5421ae --- /dev/null +++ b/prisma/lib/get_images.ts @@ -0,0 +1,81 @@ +import { getValidAuthToken } from "../../src/lib/seafile-auth-service"; + +type DirItem = { + type: "file" | "dir"; + name: string; + path: string; + size?: number; +}; + +const REPO_ID = process.env.SEAFILE_REPO_ID!; + +// ⛔ PENTING: RELATIVE PATH (tanpa slash depan) +const DIR_TARGET = "asset-web"; + +const BASE_URL = process.env.SEAFILE_URL; + +async function getDirItems(): Promise { + const token = await getValidAuthToken(); + const headers = { + Authorization: `Token ${token}`, + }; + + 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(); +} + +async function getDownloadUrl(filePath: string): Promise { + const token = await getValidAuthToken(); + const headers = { + Authorization: `Token ${token}`, + }; + + const res = await fetch( + `${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}&reuse=1`, + { headers }, + ); + + if (!res.ok) { + const text = await res.text(); + console.error("Seafile error:", { + status: res.status, + body: text, + url: res.url, + }); + throw new Error(`Failed get file url`); + } + + const data = await res.json(); + + return data; +} + +/** + * Ambil semua download URL dari target dir + */ +export 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; +} diff --git a/prisma/lib/get_shared_images.ts b/prisma/lib/get_shared_images.ts new file mode 100644 index 00000000..c76b9064 --- /dev/null +++ b/prisma/lib/get_shared_images.ts @@ -0,0 +1,71 @@ +//ini code awal cari image by folder di seafile + + +type CdnItem = { + name: string; + path: string; + cdnUrl: string; +}; + +const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com"; +const SHARE_ID = "3325e9db2c504ebf9584"; + +// https://cld-dkr-makuro-seafile.wibudev.com/d/3a9a9ecb5e244f4da8ae/ +// https://cld-dkr-makuro-seafile.wibudev.com/d/3a9a9ecb5e244f4da8ae/files/?p=-M_tICRVz6ZxOfvkuHQgU-mobile.webp&raw=1 + + +/** + * Build CDN URL langsung (tanpa API, tanpa token) + */ +export function buildCdnUrl(filePath: string) { + // filePath contoh: "banner/home.jpg" + return `${BASE_URL}/f/${SHARE_ID}/${filePath}?raw=1`; +} + +/** + * Ambil daftar file dari PUBLIC SHARE (optional) + * Tidak pakai token + */ +async function getPublicDirItems(path = "/"): Promise { + const res = await fetch( + `${BASE_URL}/api/v2.1/share-links/${SHARE_ID}/dir/?p=${encodeURIComponent( + path, + )}`, + ); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed get public dir items: ${text}`); + } + + return res.json(); +} + +/** + * Ambil semua CDN URL dari folder public share + */ +export async function getAllCdnUrls( + dirPath = "/", +): Promise { + const items = await getPublicDirItems(dirPath); + + return items + .filter((item: any) => item.type === "file") + .map((file: any) => { + const filePath = + dirPath === "/" + ? file.name + : `${dirPath.replace(/\/$/, "")}/${file.name}`; + + return { + name: file.name, + path: filePath, + cdnUrl: buildCdnUrl(filePath), + }; + }); +} + +if(import.meta.main) { + const allCdnUrls = await getAllCdnUrls(); + console.log(allCdnUrls); +} diff --git a/prisma/lib/get_sharef.ts b/prisma/lib/get_sharef.ts new file mode 100644 index 00000000..13c2dc45 --- /dev/null +++ b/prisma/lib/get_sharef.ts @@ -0,0 +1,33 @@ +const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com"; +const ADMIN_TOKEN = process.env.SEAFILE_TOKEN!; +const REPO_ID = process.env.SEAFILE_REPO_ID!; + +export async function createFileShareForFolder() { + const res = await fetch(`${BASE_URL}/api/v2.1/share-links/`, { + method: "POST", + headers: { + Authorization: `Token ${ADMIN_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + repo_id: REPO_ID, + path: "/asset-web", // FOLDER + permission: "r", + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text); + } + + const data = await res.json(); + console.log("FILE SHARE LINK:", data); + + // data.link -> https://domain/f/XXXX/ + // data.token / data.id (tergantung versi) +} + +if (import.meta.main) { + await createFileShareForFolder(); +} diff --git a/prisma/migrations/20251119062255_add_unique_username/migration.sql b/prisma/migrations/20251119062255_add_unique_username/migration.sql new file mode 100644 index 00000000..d8d9e087 --- /dev/null +++ b/prisma/migrations/20251119062255_add_unique_username/migration.sql @@ -0,0 +1,1127 @@ +/* + Warnings: + + - You are about to drop the column `kelahiranKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `kematianBayi` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `kematianKasar` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `tahun` on the `DataKematian_Kelahiran` table. All the data in the column will be lost. + - You are about to drop the column `jumlah` on the `GrafikKepuasan` table. All the data in the column will be lost. + - You are about to drop the column `label` on the `GrafikKepuasan` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KolaborasiInovasi` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KontakDaruratKeamanan` table. All the data in the column will be lost. + - You are about to drop the column `imageId` on the `KontakItem` table. All the data in the column will be lost. + - You are about to drop the column `kategoriId` on the `KontakItem` table. All the data in the column will be lost. + - You are about to drop the column `programKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `tipsKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `videoKeamananId` on the `PencegahanKriminalitas` table. All the data in the column will be lost. + - You are about to drop the column `kategori` on the `PotensiDesa` table. All the data in the column will be lost. + - You are about to drop the column `ikonUrl` on the `ProgramKemiskinan` table. All the data in the column will be lost. + - You are about to drop the `ProgramKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `TipsKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `VideoKeamanan` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `hubungan_organisasi` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `pegawai` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `posisi_organisasi` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `struktur_organisasi` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `kelahiranId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty. + - Added the required column `kematianId` to the `DataKematian_Kelahiran` table without a default value. This is not possible if the table is not empty. + - Added the required column `category` to the `FileStorage` table without a default value. This is not possible if the table is not empty. + - Added the required column `alamat` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `jenisKelamin` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `nama` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `penyakit` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `tanggal` to the `GrafikKepuasan` table without a default value. This is not possible if the table is not empty. + - Added the required column `whatsapp` to the `KontakDarurat` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty. + - Added the required column `kategoriId` to the `KontakDaruratKeamanan` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `KontakItem` table without a default value. This is not possible if the table is not empty. + - Added the required column `notelp` to the `LowonganPekerjaan` table without a default value. This is not possible if the table is not empty. + - Added the required column `name` to the `MediaSosial` table without a default value. This is not possible if the table is not empty. + - Added the required column `kontak` to the `PasarDesa` table without a default value. This is not possible if the table is not empty. + - Added the required column `deskripsi` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `deskripsiSingkat` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `judul` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `linkVideo` to the `PencegahanKriminalitas` table without a default value. This is not possible if the table is not empty. + - Added the required column `jadwalPelayanan` to the `Posyandu` table without a default value. This is not possible if the table is not empty. + - Added the required column `icon` to the `ProgramKemiskinan` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "JenisKelamin" AS ENUM ('LAKI_LAKI', 'PEREMPUAN'); + +-- CreateEnum +CREATE TYPE "Agama" AS ENUM ('ISLAM', 'KRISTEN_PROTESTAN', 'KRISTEN_KATOLIK', 'HINDU', 'BUDDHA', 'KONGHUCU', 'LAINNYA'); + +-- CreateEnum +CREATE TYPE "StatusPernikahan" AS ENUM ('BELUM_MENIKAH', 'MENIKAH', 'JANDA_DUDA'); + +-- CreateEnum +CREATE TYPE "UkuranBaju" AS ENUM ('S', 'M', 'L', 'XL', 'XXL', 'LAINNYA'); + +-- CreateEnum +CREATE TYPE "StatusPeminjaman" AS ENUM ('Dipinjam', 'Dikembalikan', 'Terlambat', 'Dibatalkan'); + +-- DropForeignKey +ALTER TABLE "JadwalKegiatan" DROP CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey"; + +-- DropForeignKey +ALTER TABLE "KolaborasiInovasi" DROP CONSTRAINT "KolaborasiInovasi_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakDaruratKeamanan" DROP CONSTRAINT "KontakDaruratKeamanan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakItem" DROP CONSTRAINT "KontakItem_kategoriId_fkey"; + +-- DropForeignKey +ALTER TABLE "MediaSosial" DROP CONSTRAINT "MediaSosial_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PelayananSuratKeterangan" DROP CONSTRAINT "PelayananSuratKeterangan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_programKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_tipsKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "PencegahanKriminalitas" DROP CONSTRAINT "PencegahanKriminalitas_videoKeamananId_fkey"; + +-- DropForeignKey +ALTER TABLE "Penghargaan" DROP CONSTRAINT "Penghargaan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_atasanId_fkey"; + +-- DropForeignKey +ALTER TABLE "hubungan_organisasi" DROP CONSTRAINT "hubungan_organisasi_bawahanId_fkey"; + +-- DropForeignKey +ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "pegawai" DROP CONSTRAINT "pegawai_posisiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_pegawaiId_fkey"; + +-- DropForeignKey +ALTER TABLE "struktur_organisasi" DROP CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey"; + +-- AlterTable +ALTER TABLE "ArtikelKesehatan" ADD COLUMN "imageId" TEXT; + +-- AlterTable +ALTER TABLE "DataKematian_Kelahiran" DROP COLUMN "kelahiranKasar", +DROP COLUMN "kematianBayi", +DROP COLUMN "kematianKasar", +DROP COLUMN "tahun", +ADD COLUMN "kelahiranId" TEXT NOT NULL, +ADD COLUMN "kematianId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "FileStorage" ADD COLUMN "category" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "GrafikKepuasan" DROP COLUMN "jumlah", +DROP COLUMN "label", +ADD COLUMN "alamat" TEXT NOT NULL, +ADD COLUMN "jenisKelamin" TEXT NOT NULL, +ADD COLUMN "nama" TEXT NOT NULL, +ADD COLUMN "penyakit" TEXT NOT NULL, +ADD COLUMN "tanggal" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "JadwalKegiatan" ALTER COLUMN "pendaftaranJadwalKegiatanId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "KolaborasiInovasi" DROP COLUMN "imageId"; + +-- AlterTable +ALTER TABLE "KontakDarurat" ADD COLUMN "whatsapp" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "KontakDaruratKeamanan" DROP COLUMN "imageId", +ADD COLUMN "deletedAt" TIMESTAMP(3), +ADD COLUMN "icon" TEXT NOT NULL, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "kategoriId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "KontakItem" DROP COLUMN "imageId", +DROP COLUMN "kategoriId", +ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "icon" TEXT NOT NULL, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE "LaporanPublik" ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ALTER COLUMN "status" SET DEFAULT 'Proses'; + +-- AlterTable +ALTER TABLE "LowonganPekerjaan" ADD COLUMN "notelp" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "MediaSosial" ADD COLUMN "name" TEXT NOT NULL, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PasarDesa" ADD COLUMN "kontak" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "PelayananSuratKeterangan" ADD COLUMN "image2Id" TEXT, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PencegahanKriminalitas" DROP COLUMN "programKeamananId", +DROP COLUMN "tipsKeamananId", +DROP COLUMN "videoKeamananId", +ADD COLUMN "deskripsi" TEXT NOT NULL, +ADD COLUMN "deskripsiSingkat" TEXT NOT NULL, +ADD COLUMN "judul" TEXT NOT NULL, +ADD COLUMN "linkVideo" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Penghargaan" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Posyandu" ADD COLUMN "jadwalPelayanan" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "PotensiDesa" DROP COLUMN "kategori", +ADD COLUMN "kategoriId" TEXT, +ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "ProgramKemiskinan" DROP COLUMN "ikonUrl", +ADD COLUMN "icon" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "StrukturPPID" ADD COLUMN "pegawaiPPIDId" TEXT, +ADD COLUMN "posisiOrganisasiPPIDId" TEXT; + +-- DropTable +DROP TABLE "ProgramKeamanan"; + +-- DropTable +DROP TABLE "TipsKeamanan"; + +-- DropTable +DROP TABLE "VideoKeamanan"; + +-- DropTable +DROP TABLE "hubungan_organisasi"; + +-- DropTable +DROP TABLE "pegawai"; + +-- DropTable +DROP TABLE "posisi_organisasi"; + +-- DropTable +DROP TABLE "struktur_organisasi"; + +-- CreateTable +CREATE TABLE "DesaAntiKorupsi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "fileId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DesaAntiKorupsi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriDesaAntiKorupsi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriDesaAntiKorupsi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SdgsDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "jumlah" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "SdgsDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APBDes" ( + "id" TEXT NOT NULL, + "tahun" INTEGER, + "name" TEXT, + "deskripsi" TEXT, + "jumlah" TEXT, + "imageId" TEXT, + "fileId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "APBDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "APBDesItem" ( + "id" TEXT NOT NULL, + "kode" TEXT NOT NULL, + "uraian" TEXT NOT NULL, + "anggaran" DOUBLE PRECISION NOT NULL, + "realisasi" DOUBLE PRECISION NOT NULL, + "selisih" DOUBLE PRECISION NOT NULL, + "persentase" DOUBLE PRECISION NOT NULL, + "tipe" TEXT, + "level" INTEGER NOT NULL, + "parentId" TEXT, + "apbdesId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "APBDesItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PrestasiDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PrestasiDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriPrestasiDesa" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriPrestasiDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Responden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "tanggal" DATE NOT NULL, + "jenisKelaminId" TEXT NOT NULL, + "ratingId" TEXT NOT NULL, + "kelompokUmurId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Responden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenisKelaminResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenisKelaminResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PilihanRatingResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PilihanRatingResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UmurResponden" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "UmurResponden_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PosisiOrganisasiPPID" ( + "id" TEXT NOT NULL, + "nama" VARCHAR(100) NOT NULL, + "deskripsi" TEXT, + "hierarki" INTEGER NOT NULL, + "parentId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PosisiOrganisasiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PegawaiPPID" ( + "id" TEXT NOT NULL, + "namaLengkap" VARCHAR(255) NOT NULL, + "gelarAkademik" VARCHAR(100), + "imageId" TEXT, + "tanggalMasuk" DATE, + "email" VARCHAR(255), + "telepon" VARCHAR(20), + "alamat" TEXT, + "posisiId" VARCHAR(50) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PegawaiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturOrganisasiPPID" ( + "id" TEXT NOT NULL, + "posisiOrganisasiId" VARCHAR(50) NOT NULL, + "pegawaiId" TEXT NOT NULL, + "hubunganOrganisasiId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "StrukturOrganisasiPPID_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PerbekelDariMasaKeMasa" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "periode" TEXT NOT NULL, + "imageId" TEXT, + "daerah" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PerbekelDariMasaKeMasa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriPotensi" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriPotensi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AjukanPermohonan" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "nik" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "nomorKk" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "AjukanPermohonan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kelahiran" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "tanggal" TIMESTAMP(3) NOT NULL, + "jenisKelamin" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Kelahiran_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Kematian" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "tanggal" TIMESTAMP(3) NOT NULL, + "jenisKelamin" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "penyebab" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Kematian_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KontakDaruratToItem" ( + "id" TEXT NOT NULL, + "kontakDaruratId" TEXT NOT NULL, + "kontakItemId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "KontakDaruratToItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturBumDes" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "posisiOrganisasiBumDesId" TEXT, + "pegawaiBumDesId" TEXT, + + CONSTRAINT "StrukturBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PosisiOrganisasiBumDes" ( + "id" TEXT NOT NULL, + "nama" VARCHAR(100) NOT NULL, + "deskripsi" TEXT, + "hierarki" INTEGER NOT NULL, + "parentId" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PosisiOrganisasiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PegawaiBumDes" ( + "id" TEXT NOT NULL, + "namaLengkap" VARCHAR(255) NOT NULL, + "gelarAkademik" VARCHAR(100), + "imageId" TEXT, + "tanggalMasuk" DATE, + "email" VARCHAR(255), + "telepon" VARCHAR(20), + "alamat" TEXT, + "posisiId" VARCHAR(50) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PegawaiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StrukturOrganisasiBumDes" ( + "id" TEXT NOT NULL, + "posisiOrganisasiId" VARCHAR(50) NOT NULL, + "pegawaiId" TEXT NOT NULL, + "hubunganOrganisasiId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "StrukturOrganisasiBumDes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MitraKolaborasi" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "imageId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "MitraKolaborasi_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenjangPendidikan" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenjangPendidikan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Lembaga" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "jenjangId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Lembaga_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Siswa" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "lembagaId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Siswa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Pengajar" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "lembagaId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Pengajar_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KeunggulanProgram" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KeunggulanProgram_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BeasiswaPendaftar" ( + "id" TEXT NOT NULL, + "namaLengkap" TEXT NOT NULL, + "nis" TEXT, + "kelas" TEXT, + "jenisKelamin" "JenisKelamin" NOT NULL, + "alamatDomisili" TEXT, + "tempatLahir" TEXT NOT NULL, + "tanggalLahir" TIMESTAMP(3) NOT NULL, + "namaOrtu" TEXT, + "nik" TEXT NOT NULL, + "pekerjaanOrtu" TEXT, + "penghasilan" TEXT, + "noHp" TEXT NOT NULL, + "kewarganegaraan" TEXT, + "agama" "Agama", + "alamatKTP" TEXT, + "email" TEXT, + "statusPernikahan" "StatusPernikahan", + "ukuranBaju" "UkuranBaju", + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BeasiswaPendaftar_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanProgram" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanProgram_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProgramUnggulan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "ProgramUnggulan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LokasiJadwalBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "LokasiJadwalBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FasilitasBimbinganBelajarDesa" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "FasilitasBimbinganBelajarDesa_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TujuanPendidikanNonFormal" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TujuanPendidikanNonFormal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TempatKegiatan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "TempatKegiatan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JenisProgramYangDiselenggarakan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "JenisProgramYangDiselenggarakan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DataPerpustakaan" ( + "id" TEXT NOT NULL, + "judul" TEXT NOT NULL, + "deskripsi" TEXT NOT NULL, + "kategoriId" TEXT NOT NULL, + "imageId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DataPerpustakaan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KategoriBuku" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "KategoriBuku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PeminjamanBuku" ( + "id" TEXT NOT NULL, + "nama" TEXT NOT NULL, + "noTelp" TEXT NOT NULL, + "alamat" TEXT NOT NULL, + "bukuId" TEXT NOT NULL, + "tanggalPinjam" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "batasKembali" TIMESTAMP(3) NOT NULL, + "tanggalKembali" TIMESTAMP(3), + "status" "StatusPeminjaman" NOT NULL DEFAULT 'Dipinjam', + "catatan" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "PeminjamanBuku_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "nomor" TEXT NOT NULL, + "roleId" TEXT NOT NULL DEFAULT '1', + "instansi" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "lastLogin" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "roles" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "permissions" JSONB NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "roles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KodeOtp" ( + "id" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "nomor" TEXT NOT NULL, + "otp" INTEGER NOT NULL, + + CONSTRAINT "KodeOtp_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "permissions" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "permissions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserSession" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3), + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DataPendidikan" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "jumlah" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "DataPendidikan_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DesaAntiKorupsi_name_key" ON "DesaAntiKorupsi"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriDesaAntiKorupsi_name_key" ON "KategoriDesaAntiKorupsi"("name"); + +-- CreateIndex +CREATE INDEX "APBDesItem_kode_idx" ON "APBDesItem"("kode"); + +-- CreateIndex +CREATE INDEX "APBDesItem_level_idx" ON "APBDesItem"("level"); + +-- CreateIndex +CREATE INDEX "APBDesItem_apbdesId_idx" ON "APBDesItem"("apbdesId"); + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriPrestasiDesa_name_key" ON "KategoriPrestasiDesa"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Responden_name_key" ON "Responden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "JenisKelaminResponden_name_key" ON "JenisKelaminResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PilihanRatingResponden_name_key" ON "PilihanRatingResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "UmurResponden_name_key" ON "UmurResponden"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "PegawaiPPID_email_key" ON "PegawaiPPID"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "PegawaiBumDes_email_key" ON "PegawaiBumDes"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "BeasiswaPendaftar_nik_key" ON "BeasiswaPendaftar"("nik"); + +-- CreateIndex +CREATE UNIQUE INDEX "BeasiswaPendaftar_email_key" ON "BeasiswaPendaftar"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_nomor_key" ON "User"("nomor"); + +-- CreateIndex +CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "permissions_name_key" ON "permissions"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSession_userId_key" ON "UserSession"("userId"); + +-- AddForeignKey +ALTER TABLE "MediaSosial" ADD CONSTRAINT "MediaSosial_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriDesaAntiKorupsi"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaAntiKorupsi" ADD CONSTRAINT "DesaAntiKorupsi_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SdgsDesa" ADD CONSTRAINT "SdgsDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDes" ADD CONSTRAINT "APBDes_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "APBDesItem"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APBDesItem" ADD CONSTRAINT "APBDesItem_apbdesId_fkey" FOREIGN KEY ("apbdesId") REFERENCES "APBDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPrestasiDesa"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PrestasiDesa" ADD CONSTRAINT "PrestasiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_jenisKelaminId_fkey" FOREIGN KEY ("jenisKelaminId") REFERENCES "JenisKelaminResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_ratingId_fkey" FOREIGN KEY ("ratingId") REFERENCES "PilihanRatingResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Responden" ADD CONSTRAINT "Responden_kelompokUmurId_fkey" FOREIGN KEY ("kelompokUmurId") REFERENCES "UmurResponden"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_posisiOrganisasiPPIDId_fkey" FOREIGN KEY ("posisiOrganisasiPPIDId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturPPID" ADD CONSTRAINT "StrukturPPID_pegawaiPPIDId_fkey" FOREIGN KEY ("pegawaiPPIDId") REFERENCES "PegawaiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PosisiOrganisasiPPID" ADD CONSTRAINT "PosisiOrganisasiPPID_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiPPID" ADD CONSTRAINT "PegawaiPPID_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiPPID" ADD CONSTRAINT "StrukturOrganisasiPPID_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiPPID"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PerbekelDariMasaKeMasa" ADD CONSTRAINT "PerbekelDariMasaKeMasa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PelayananSuratKeterangan" ADD CONSTRAINT "PelayananSuratKeterangan_image2Id_fkey" FOREIGN KEY ("image2Id") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AjukanPermohonan" ADD CONSTRAINT "AjukanPermohonan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "PelayananSuratKeterangan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Penghargaan" ADD CONSTRAINT "Penghargaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JadwalKegiatan" ADD CONSTRAINT "JadwalKegiatan_pendaftaranJadwalKegiatanId_fkey" FOREIGN KEY ("pendaftaranJadwalKegiatanId") REFERENCES "PendaftaranJadwalKegiatan"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kematianId_fkey" FOREIGN KEY ("kematianId") REFERENCES "Kematian"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataKematian_Kelahiran" ADD CONSTRAINT "DataKematian_Kelahiran_kelahiranId_fkey" FOREIGN KEY ("kelahiranId") REFERENCES "Kelahiran"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ArtikelKesehatan" ADD CONSTRAINT "ArtikelKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratKeamanan" ADD CONSTRAINT "KontakDaruratKeamanan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakDaruratId_fkey" FOREIGN KEY ("kontakDaruratId") REFERENCES "KontakDaruratKeamanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDaruratToItem" ADD CONSTRAINT "KontakDaruratToItem_kontakItemId_fkey" FOREIGN KEY ("kontakItemId") REFERENCES "KontakItem"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_posisiOrganisasiBumDesId_fkey" FOREIGN KEY ("posisiOrganisasiBumDesId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturBumDes" ADD CONSTRAINT "StrukturBumDes_pegawaiBumDesId_fkey" FOREIGN KEY ("pegawaiBumDesId") REFERENCES "PegawaiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PosisiOrganisasiBumDes" ADD CONSTRAINT "PosisiOrganisasiBumDes_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PegawaiBumDes" ADD CONSTRAINT "PegawaiBumDes_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "PosisiOrganisasiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StrukturOrganisasiBumDes" ADD CONSTRAINT "StrukturOrganisasiBumDes_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "PegawaiBumDes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MitraKolaborasi" ADD CONSTRAINT "MitraKolaborasi_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Lembaga" ADD CONSTRAINT "Lembaga_jenjangId_fkey" FOREIGN KEY ("jenjangId") REFERENCES "JenjangPendidikan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Siswa" ADD CONSTRAINT "Siswa_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Pengajar" ADD CONSTRAINT "Pengajar_lembagaId_fkey" FOREIGN KEY ("lembagaId") REFERENCES "Lembaga"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriBuku"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PeminjamanBuku" ADD CONSTRAINT "PeminjamanBuku_bukuId_fkey" FOREIGN KEY ("bukuId") REFERENCES "DataPerpustakaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260106072549_nico_6_jan2025/migration.sql b/prisma/migrations/20260106072549_nico_6_jan2025/migration.sql new file mode 100644 index 00000000..6952c280 --- /dev/null +++ b/prisma/migrations/20260106072549_nico_6_jan2025/migration.sql @@ -0,0 +1,142 @@ +/* + Warnings: + + - You are about to drop the column `dokterdanTenagaMedisId` on the `FasilitasKesehatan` table. All the data in the column will be lost. + - You are about to drop the column `tarifDanLayananId` on the `FasilitasKesehatan` table. All the data in the column will be lost. + - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `UserSession` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `permissions` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "FasilitasKesehatan" DROP CONSTRAINT "FasilitasKesehatan_dokterdanTenagaMedisId_fkey"; + +-- DropForeignKey +ALTER TABLE "FasilitasKesehatan" DROP CONSTRAINT "FasilitasKesehatan_tarifDanLayananId_fkey"; + +-- DropForeignKey +ALTER TABLE "User" DROP CONSTRAINT "User_roleId_fkey"; + +-- DropForeignKey +ALTER TABLE "UserSession" DROP CONSTRAINT "UserSession_userId_fkey"; + +-- AlterTable +ALTER TABLE "DokterdanTenagaMedis" ADD COLUMN "jadwalLibur" TEXT, +ADD COLUMN "jamBukaLibur" TEXT, +ADD COLUMN "jamBukaOperasional" TEXT, +ADD COLUMN "jamTutupLibur" TEXT, +ADD COLUMN "jamTutupOperasional" TEXT; + +-- AlterTable +ALTER TABLE "FasilitasKesehatan" DROP COLUMN "dokterdanTenagaMedisId", +DROP COLUMN "tarifDanLayananId"; + +-- AlterTable +ALTER TABLE "MediaSosial" ADD COLUMN "icon" TEXT; + +-- AlterTable +ALTER TABLE "roles" ALTER COLUMN "permissions" DROP NOT NULL; + +-- DropTable +DROP TABLE "User"; + +-- DropTable +DROP TABLE "UserSession"; + +-- DropTable +DROP TABLE "permissions"; + +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "nomor" TEXT NOT NULL, + "roleId" TEXT NOT NULL DEFAULT '2', + "isActive" BOOLEAN NOT NULL DEFAULT false, + "sessionInvalid" BOOLEAN NOT NULL DEFAULT false, + "lastLogin" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "permissions" JSONB, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_sessions" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + + CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserMenuAccess" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "menuId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UserMenuAccess_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_Tarif" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_Tarif_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_Dokter" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_Dokter_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_nomor_key" ON "users"("nomor"); + +-- CreateIndex +CREATE INDEX "user_sessions_userId_idx" ON "user_sessions"("userId"); + +-- CreateIndex +CREATE INDEX "user_sessions_token_idx" ON "user_sessions"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserMenuAccess_userId_menuId_key" ON "UserMenuAccess"("userId", "menuId"); + +-- CreateIndex +CREATE INDEX "_Tarif_B_index" ON "_Tarif"("B"); + +-- CreateIndex +CREATE INDEX "_Dokter_B_index" ON "_Dokter"("B"); + +-- AddForeignKey +ALTER TABLE "users" ADD CONSTRAINT "users_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserMenuAccess" ADD CONSTRAINT "UserMenuAccess_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Tarif" ADD CONSTRAINT "_Tarif_A_fkey" FOREIGN KEY ("A") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Tarif" ADD CONSTRAINT "_Tarif_B_fkey" FOREIGN KEY ("B") REFERENCES "TarifDanLayanan"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Dokter" ADD CONSTRAINT "_Dokter_A_fkey" FOREIGN KEY ("A") REFERENCES "DokterdanTenagaMedis"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Dokter" ADD CONSTRAINT "_Dokter_B_fkey" FOREIGN KEY ("B") REFERENCES "FasilitasKesehatan"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260122074939_21_jan_26/migration.sql b/prisma/migrations/20260122074939_21_jan_26/migration.sql new file mode 100644 index 00000000..3363be3d --- /dev/null +++ b/prisma/migrations/20260122074939_21_jan_26/migration.sql @@ -0,0 +1,84 @@ +-- DropForeignKey +ALTER TABLE "Berita" DROP CONSTRAINT "Berita_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "InfoWabahPenyakit" DROP CONSTRAINT "InfoWabahPenyakit_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KontakDarurat" DROP CONSTRAINT "KontakDarurat_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PenangananDarurat" DROP CONSTRAINT "PenangananDarurat_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "Posyandu" DROP CONSTRAINT "Posyandu_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProgramKesehatan" DROP CONSTRAINT "ProgramKesehatan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "Puskesmas" DROP CONSTRAINT "Puskesmas_imageId_fkey"; + +-- AlterTable +ALTER TABLE "Berita" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "InfoWabahPenyakit" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "KontakDarurat" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PasarDesa" ADD COLUMN "deskripsi" TEXT; + +-- AlterTable +ALTER TABLE "PenangananDarurat" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Posyandu" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "ProgramKesehatan" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Puskesmas" ALTER COLUMN "imageId" DROP NOT NULL; + +-- CreateTable +CREATE TABLE "LayananToPolsek" ( + "id" TEXT NOT NULL, + "layananId" TEXT NOT NULL, + "polsekTerdekatId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "isActive" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "LayananToPolsek_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Berita" ADD CONSTRAINT "Berita_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Posyandu" ADD CONSTRAINT "Posyandu_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Puskesmas" ADD CONSTRAINT "Puskesmas_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProgramKesehatan" ADD CONSTRAINT "ProgramKesehatan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PenangananDarurat" ADD CONSTRAINT "PenangananDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KontakDarurat" ADD CONSTRAINT "KontakDarurat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "InfoWabahPenyakit" ADD CONSTRAINT "InfoWabahPenyakit_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LayananToPolsek" ADD CONSTRAINT "LayananToPolsek_layananId_fkey" FOREIGN KEY ("layananId") REFERENCES "LayananPolsek"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LayananToPolsek" ADD CONSTRAINT "LayananToPolsek_polsekTerdekatId_fkey" FOREIGN KEY ("polsekTerdekatId") REFERENCES "PolsekTerdekat"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260225082505_deploy/migration.sql b/prisma/migrations/20260225082505_deploy/migration.sql new file mode 100644 index 00000000..ac4024a7 --- /dev/null +++ b/prisma/migrations/20260225082505_deploy/migration.sql @@ -0,0 +1,170 @@ +/* + Warnings: + + - You are about to alter the column `nama` on the `KategoriPotensi` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`. + - You are about to alter the column `name` on the `PotensiDesa` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`. + - You are about to alter the column `kategoriId` on the `PotensiDesa` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(36)`. + - A unique constraint covering the columns `[nama]` on the table `KategoriPotensi` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[name]` on the table `PotensiDesa` will be added. If there are existing duplicate values, this will fail. + - Made the column `kategoriId` on table `PotensiDesa` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropForeignKey +ALTER TABLE "DataPerpustakaan" DROP CONSTRAINT "DataPerpustakaan_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "DesaDigital" DROP CONSTRAINT "DesaDigital_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "InfoTekno" DROP CONSTRAINT "InfoTekno_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "KegiatanDesa" DROP CONSTRAINT "KegiatanDesa_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PengaduanMasyarakat" DROP CONSTRAINT "PengaduanMasyarakat_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_kategoriId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProfileDesaImage" DROP CONSTRAINT "ProfileDesaImage_imageId_fkey"; + +-- AlterTable +ALTER TABLE "CaraMemperolehInformasi" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "CaraMemperolehSalinanInformasi" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "DaftarInformasiPublik" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "DasarHukumPPID" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "DataPerpustakaan" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "DesaDigital" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "FormulirPermohonanKeberatan" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "InfoTekno" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "JenisInformasiDiminta" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "JenisKelaminResponden" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "KategoriPotensi" ALTER COLUMN "nama" SET DATA TYPE VARCHAR(100), +ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "KategoriPrestasiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "KegiatanDesa" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "LambangDesa" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "MaskotDesa" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "PegawaiPPID" ADD COLUMN "deletedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "PengaduanMasyarakat" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "PermohonanInformasiPublik" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "PilihanRatingResponden" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "PosisiOrganisasiPPID" ADD COLUMN "deletedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "PotensiDesa" ALTER COLUMN "name" SET DATA TYPE VARCHAR(255), +ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT, +ALTER COLUMN "kategoriId" SET NOT NULL, +ALTER COLUMN "kategoriId" SET DATA TYPE VARCHAR(36); + +-- AlterTable +ALTER TABLE "PrestasiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "ProfileDesaImage" ALTER COLUMN "imageId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "ProfilePPID" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "Responden" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "SejarahDesa" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "UmurResponden" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "VisiMisiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "VisiMisiPPID" ALTER COLUMN "deletedAt" DROP NOT NULL, +ALTER COLUMN "deletedAt" DROP DEFAULT; + +-- CreateIndex +CREATE UNIQUE INDEX "KategoriPotensi_nama_key" ON "KategoriPotensi"("nama"); + +-- CreateIndex +CREATE UNIQUE INDEX "PotensiDesa_name_key" ON "PotensiDesa"("name"); + +-- AddForeignKey +ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DesaDigital" ADD CONSTRAINT "DesaDigital_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "InfoTekno" ADD CONSTRAINT "InfoTekno_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 648c57fd..044d57cd 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "postgresql" \ No newline at end of file +provider = "postgresql" diff --git a/prisma/newseed-asset.ts b/prisma/newseed-asset.ts new file mode 100644 index 00000000..fabdbafc --- /dev/null +++ b/prisma/newseed-asset.ts @@ -0,0 +1,82 @@ +type DirItem = { + type: "file" | "dir"; + name: string; + path: string; + size?: number; +}; + +type FileDownload = { + name: string; + path: string; + link: string; +}; + +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 item di directory + */ +async function getDirItems(): Promise { + const res = await fetch( + `${BASE_URL}/repos/${REPO_ID}/dir/?p=${encodeURIComponent(DIR_TARGET)}`, + { headers }, + ); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`Failed get dir items: ${body}`); + } + + return res.json(); +} + + +/** + * Ambil download URL file + */ +async function getDownloadUrl(filePath: string): Promise { + const res = await fetch( + `${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}`, + { headers }, + ); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`Failed get file url: ${body}`); + } + + const data = await res.json(); + return data.url; +} + +/** + * Ambil semua download URL dari folder image + */ +export async function getImageDownloadList(): Promise { + const items = await getDirItems(); + + const files = items.filter((item) => item.type === "file"); + + return Promise.all( + files.map(async (file) => { + const filePath = `${DIR_TARGET}/${file.name}`; // → /image/xxx.webp + + return { + name: file.name, + path: filePath, + link: await getDownloadUrl(filePath), + }; + }), + ); +} + diff --git a/prisma/resolveImageByName.ts b/prisma/resolveImageByName.ts new file mode 100644 index 00000000..e105eef3 --- /dev/null +++ b/prisma/resolveImageByName.ts @@ -0,0 +1,78 @@ +// import prisma from "@/lib/prisma"; + +// // Ganti nama fungsi dan logikanya +// export default async function resolveImageById( +// imageId?: string | null +// ): Promise { +// if (!imageId) return null; + +// const image = await prisma.fileStorage.findFirst({ +// where: { +// id: imageId, // ← cari berdasarkan ID +// category: "image", +// isActive: true, +// deletedAt: null, +// }, +// select: { id: true }, +// }); + +// if (!image) { +// console.warn(`⚠️ Image with ID ${imageId} not found`); +// return null; +// } + +// return image.id; +// } + +import prisma from "@/lib/prisma"; + +/** + * Resolve image ID by checking multiple possible names + * @param imageId - The ID from JSON (could be filename or actual ID) + * @returns The actual database ID or null + */ +export default async function resolveImageById(imageId: string | null): Promise { + if (!imageId) return null; + + try { + // 1. Coba cari berdasarkan ID langsung + const byId = await prisma.fileStorage.findUnique({ + where: { id: imageId }, + select: { id: true }, + }); + if (byId) return byId.id; + + // 2. Coba cari berdasarkan name (exact match) + const byName = await prisma.fileStorage.findUnique({ + where: { name: imageId }, + select: { id: true }, + }); + if (byName) return byName.id; + + // 3. Coba cari berdasarkan realName + const byRealName = await prisma.fileStorage.findFirst({ + where: { realName: imageId }, + select: { id: true }, + }); + if (byRealName) return byRealName.id; + + // 4. Coba dengan menambahkan ekstensi .webp + const withWebp = `${imageId.replace(/\.(jpg|jpeg|png)$/i, '')}.webp`; + const byWebp = await prisma.fileStorage.findFirst({ + where: { + OR: [ + { name: withWebp }, + { name: { contains: imageId.split('.')[0] } }, + ], + }, + select: { id: true }, + }); + if (byWebp) return byWebp.id; + + console.warn(`⚠️ Image not found for: ${imageId}`); + return null; + } catch (error) { + console.error(`❌ Error resolving image ${imageId}:`, error); + return null; + } +} \ No newline at end of file diff --git a/prisma/safeSeedMany.ts b/prisma/safeSeedMany.ts new file mode 100644 index 00000000..d909bb2c --- /dev/null +++ b/prisma/safeSeedMany.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { PrismaClient } from "@prisma/client"; +import { safeSeedUnique } from "./safeseedUnique"; +import cliProgress from 'cli-progress'; + +type SafeSeedOptions = { + skipUpdate?: boolean; + silent?: boolean; // Opsional: untuk suppress log +}; + +/** + * Batch upsert with progress logging + */ +export async function safeSeedMany( + model: T, + items: Array<{ where: Record; data: Record }>, + options: SafeSeedOptions = {} +) { + const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); + bar.start(items.length, 0); + + let success = 0; + let failed = 0; + let skipped = 0; + + for (const [index, item] of items.entries()) { + try { + const result = await safeSeedUnique(model, item.where, item.data, { + ...options, + silent: true, + }); + if (result) success++; + else skipped++; + } catch (err) { + failed++; + } + bar.update(index + 1); + } + + bar.stop(); + console.log(`✅ ${String(model)}: ${success} seeded, ${skipped} skipped, ${failed} failed`); + + return { success, skipped, failed }; +} \ No newline at end of file diff --git a/prisma/safeseedUnique.ts b/prisma/safeseedUnique.ts index 92d16071..e0ea0d52 100644 --- a/prisma/safeseedUnique.ts +++ b/prisma/safeseedUnique.ts @@ -1,30 +1,134 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// helpers/safeSeedUnique.ts +import prisma from "@/lib/prisma"; import { PrismaClient } from "@prisma/client"; -const prisma = new PrismaClient(); +type SafeSeedOptions = { + skipUpdate?: boolean; + silent?: boolean; // Opsional: untuk suppress log +}; /** - * Helper generic buat seed dengan upsert aman + * Safely upsert data with error handling + * @param model - Prisma model name + * @param where - Unique identifier(s) + * @param data - Full data object (will be used for create) + * @param options - Additional options */ export async function safeSeedUnique( model: T, where: Record, - data: Record + data: Record, + options: SafeSeedOptions = {} ) { - const m = prisma[model]; - - if (!m) throw new Error(`Model ${String(model)} tidak ditemukan di PrismaClient`); + const m = prisma[model] as any; + + if (!m) { + throw new Error(`❌ Model ${String(model)} tidak ditemukan di Prisma Client`); + } try { - // @ts-expect-error upsert dynamic - await m.upsert({ + const result = await m.upsert({ where, - update: data, - create: { ...where, ...data }, + update: options.skipUpdate ? {} : data, + create: data, }); - console.log(`✅ Seeded ${String(model)} -> ${JSON.stringify(where)}`); - } catch (err) { - console.error(`❌ Gagal seed ${String(model)} -> ${JSON.stringify(where)}`, err); + + if (!options.silent) { + console.log(`✅ Seeded ${String(model)}:`, where); + } + + return result; + } catch (err: any) { + // Handle specific Prisma errors + if (err.code === "P2002") { + console.warn(`⚠️ Duplicate ${String(model)} (skipped):`, where); + return null; + } + + if (err.code === "P2003") { + console.error(`❌ Foreign key constraint failed for ${String(model)}:`, where); + console.error(" Missing relation:", err.meta?.field_name); + throw err; + } + + if (err.code === "P2025") { + console.error(`❌ Record not found for ${String(model)}:`, where); + throw err; + } + + // Log unexpected errors with full details + console.error(`❌ Failed to seed ${String(model)}:`, where); + console.error(" Error:", err.message); + console.error(" Code:", err.code); + + throw err; } } + + + +//ini yang bener pertama + +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// import prisma from "@/lib/prisma"; +// import { PrismaClient } from "@prisma/client"; + +// type SafeSeedOptions = { +// skipUpdate?: boolean; +// }; + +// // prisma/safeseedUnique.ts +// export async function safeSeedUnique( +// model: T, +// where: Record, +// data: Record, +// options: SafeSeedOptions = {} +// ) { +// const m = prisma[model] as any; +// if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`); + +// try { +// // Pastikan `where` berisi field yang benar-benar unique (misal: `id`) +// const result = await m.upsert({ +// where, +// update: options.skipUpdate ? {} : data, +// create: data, // ✅ Jangan duplikasi `where` ke `create` +// }); +// console.log(`✅ Seed ${String(model)}:`, where); +// return result; +// } catch (err) { +// console.error(`❌ Gagal seed ${String(model)}:`, where, err); +// throw err; // ✅ Rethrow agar seeding berhenti jika kritis +// } +// } + +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// import { PrismaClient } from "@prisma/client"; + +// const prisma = new PrismaClient(); + +// type SafeSeedOptions = { +// skipUpdate?: boolean; +// }; + +// export async function safeSeedUnique( +// model: T, +// where: Record, +// data: Record, +// options: SafeSeedOptions = {} +// ) { +// const m = prisma[model] as any; +// if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`); + +// try { +// await m.upsert({ +// where, +// update: options.skipUpdate ? {} : data, +// create: { ...where, ...data }, +// }); + +// console.log(`✅ Seed ${String(model)}:`, where); +// } catch (err) { +// console.error(`❌ Gagal seed ${String(model)}:`, where, err); +// } +// } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a52a0cb1..5a9168cd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -136,6 +136,7 @@ model MediaSosial { name String image FileStorage? @relation(fields: [imageId], references: [id]) imageId String? + icon String? iconUrl String? @db.VarChar(255) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -184,18 +185,46 @@ model SdgsDesa { //========================================= APBDes ========================================= // model APBDes { id String @id @default(cuid()) - name String - jumlah String + tahun Int? + name String? // misalnya: "APBDes Tahun 2025" + deskripsi String? + jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items) + items APBDesItem[] image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id]) imageId String? file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id]) fileId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? // opsional, tidak perlu default now() isActive Boolean @default(true) } +model APBDesItem { + id String @id @default(cuid()) + kode String // contoh: "4", "4.1", "4.1.2" + uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha" + anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS) + realisasi Float + selisih Float // realisasi - anggaran + persentase Float + tipe String? // (realisasi / anggaran) * 100 + level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail + parentId String? // untuk relasi hierarki + parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id]) + children APBDesItem[] @relation("APBDesItemParent") + apbdesId String + apbdes APBDes @relation(fields: [apbdesId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) + + @@index([kode]) + @@index([level]) + @@index([apbdesId]) +} + //========================================= PRESTASI DESA ========================================= // model PrestasiDesa { id String @id @default(cuid()) @@ -207,7 +236,7 @@ model PrestasiDesa { imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -216,7 +245,7 @@ model KategoriPrestasiDesa { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) PrestasiDesa PrestasiDesa[] } @@ -234,7 +263,7 @@ model Responden { kelompokUmurId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -243,7 +272,7 @@ model JenisKelaminResponden { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) Responden Responden[] } @@ -253,7 +282,7 @@ model PilihanRatingResponden { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) Responden Responden[] } @@ -263,7 +292,7 @@ model UmurResponden { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) Responden Responden[] } @@ -297,6 +326,7 @@ model PosisiOrganisasiPPID { isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + deletedAt DateTime? parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id]) children PosisiOrganisasiPPID[] @relation("Parent") StrukturOrganisasiPPID StrukturOrganisasiPPID[] @@ -316,6 +346,7 @@ model PegawaiPPID { isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + deletedAt DateTime? posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id]) strukturOrganisasi StrukturPPID[] // Relasi balik StrukturOrganisasiPPID StrukturOrganisasiPPID[] @@ -341,7 +372,7 @@ model VisiMisiPPID { misi String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -352,7 +383,7 @@ model DasarHukumPPID { content String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -369,7 +400,7 @@ model ProfilePPID { imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -381,7 +412,7 @@ model DaftarInformasiPublik { tanggal DateTime @db.Date createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -402,7 +433,7 @@ model PermohonanInformasiPublik { caraMemperolehSalinanInformasiId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -411,7 +442,7 @@ model JenisInformasiDiminta { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) PermohonanInformasiPublik PermohonanInformasiPublik[] } @@ -421,7 +452,7 @@ model CaraMemperolehInformasi { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) PermohonanInformasiPublik PermohonanInformasiPublik[] } @@ -431,7 +462,7 @@ model CaraMemperolehSalinanInformasi { name String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) PermohonanInformasiPublik PermohonanInformasiPublik[] } @@ -445,7 +476,7 @@ model FormulirPermohonanKeberatan { alasan String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -502,7 +533,7 @@ model SejarahDesa { deskripsi String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -512,7 +543,7 @@ model VisiMisiDesa { misi String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -522,7 +553,7 @@ model LambangDesa { deskripsi String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -533,15 +564,15 @@ model MaskotDesa { images ProfileDesaImage[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } model ProfileDesaImage { id String @id @default(cuid()) label String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? MaskotDesa MaskotDesa @relation(fields: [maskotDesaId], references: [id]) maskotDesaId String } @@ -578,8 +609,8 @@ model Berita { id String @id @default(cuid()) judul String deskripsi String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? content String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -602,25 +633,25 @@ model KategoriBerita { // ========================================= POTENSI DESA ========================================= // model PotensiDesa { id String @id @default(cuid()) - name String - deskripsi String + name String @unique @db.VarChar(255) + deskripsi String @db.Text kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id]) - kategoriId String? + kategoriId String @db.VarChar(36) image FileStorage? @relation(fields: [imageId], references: [id]) imageId String? content String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } model KategoriPotensi { id String @id @default(cuid()) - nama String + nama String @unique @db.VarChar(100) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) PotensiDesa PotensiDesa[] } @@ -754,24 +785,22 @@ model Penghargaan { // ========================================= FASILITAS KESEHATAN ========================================= // model FasilitasKesehatan { - id String @id @default(cuid()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) - informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id]) - informasiUmumId String - layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id]) - layananUnggulanId String - dokterdantenagamedis DokterdanTenagaMedis @relation(fields: [dokterdanTenagaMedisId], references: [id]) - dokterdanTenagaMedisId String - fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id]) - fasilitasPendukungId String - prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id]) - prosedurPendaftaranId String - tarifdanlayanan TarifDanLayanan @relation(fields: [tarifDanLayananId], references: [id]) - tarifDanLayananId String + id String @id @default(cuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + informasiumum InformasiUmum @relation(fields: [informasiUmumId], references: [id]) + informasiUmumId String + layananunggulan LayananUnggulan @relation(fields: [layananUnggulanId], references: [id]) + layananUnggulanId String + dokterdantenagamedis DokterdanTenagaMedis[] @relation("Dokter") + fasilitaspendukung FasilitasPendukung @relation(fields: [fasilitasPendukungId], references: [id]) + fasilitasPendukungId String + prosedurpendaftaran ProsedurPendaftaran @relation(fields: [prosedurPendaftaranId], references: [id]) + prosedurPendaftaranId String + tarifdanlayanan TarifDanLayanan[] @relation("Tarif") } model InformasiUmum { @@ -797,15 +826,20 @@ model LayananUnggulan { } model DokterdanTenagaMedis { - id String @id @default(cuid()) - name String - specialist String - jadwal String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) - FasilitasKesehatan FasilitasKesehatan[] + id String @id @default(cuid()) + name String + specialist String + jadwal String + jadwalLibur String? + jamBukaOperasional String? + jamTutupOperasional String? + jamBukaLibur String? + jamTutupLibur String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + FasilitasKesehatan FasilitasKesehatan[] @relation("Dokter") } model FasilitasPendukung { @@ -836,7 +870,7 @@ model TarifDanLayanan { updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) isActive Boolean @default(true) - FasilitasKesehatan FasilitasKesehatan[] + FasilitasKesehatan FasilitasKesehatan[] @relation("Tarif") } // ========================================= JADWAL KEGIATAN ========================================= // @@ -1081,17 +1115,17 @@ model DoctorSign { // ========================================= POSYANDU ========================================= // model Posyandu { - id String @id @default(cuid()) + id String @id @default(cuid()) name String nomor String deskripsi String jadwalPelayanan String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= PUSKESMAS ========================================= // @@ -1101,8 +1135,8 @@ model Puskesmas { alamat String jam JamOperasional @relation(fields: [jamId], references: [id]) jamId String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? kontak KontakPuskesmas @relation(fields: [kontakId], references: [id]) kontakId String createdAt DateTime @default(now()) @@ -1138,57 +1172,57 @@ model KontakPuskesmas { // ========================================= PROGRAM KESSEHATAN ========================================= // model ProgramKesehatan { - id String @id @default(cuid()) + id String @id @default(cuid()) name String deskripsiSingkat String deskripsi String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= PENANGANAN DARURAT ========================================= // model PenangananDarurat { - id String @id @default(cuid()) + id String @id @default(cuid()) name String deskripsi String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= KONTAK DARURAT ========================================= // model KontakDarurat { - id String @id @default(cuid()) + id String @id @default(cuid()) name String deskripsi String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? whatsapp String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= INFO WABAH PENYAKIT ========================================= // model InfoWabahPenyakit { - id String @id @default(cuid()) + id String @id @default(cuid()) name String deskripsiSingkat String deskripsiLengkap String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= MENU KEAMANAN ========================================= // @@ -1207,7 +1241,7 @@ model KeamananLingkungan { // ========================================= POLSEK TERDEKAT ========================================= // model PolsekTerdekat { - id String @id @default(uuid()) + id String @id @default(uuid()) nama String jarakKeDesa String alamat String @@ -1217,22 +1251,36 @@ model PolsekTerdekat { namaTempatMaps String alamatMaps String linkPetunjukArah String - layananPolsek LayananPolsek @relation(fields: [layananPolsekId], references: [id]) + layananPolsek LayananPolsek @relation(fields: [layananPolsekId], references: [id]) layananPolsekId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) + LayananToPolsek LayananToPolsek[] } model LayananPolsek { - id String @id @default(uuid()) - nama String // contoh: "Pelayanan SKCK", "Laporan Kriminal" - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - isActive Boolean @default(true) - PolsekTerdekat PolsekTerdekat[] + id String @id @default(uuid()) + nama String // contoh: "Pelayanan SKCK", "Laporan Kriminal" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + isActive Boolean @default(true) + PolsekTerdekat PolsekTerdekat[] + LayananToPolsek LayananToPolsek[] +} + +model LayananToPolsek { + id String @id @default(uuid()) + layanan LayananPolsek @relation(fields: [layananId], references: [id]) + layananId String + polsekTerdekat PolsekTerdekat @relation(fields: [polsekTerdekatId], references: [id]) + polsekTerdekatId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } // ========================================= KONTAK DARURAT ========================================= // @@ -1345,6 +1393,7 @@ model PasarDesa { rating Float alamatUsaha String kontak String + deskripsi String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1612,8 +1661,8 @@ model DesaDigital { id String @id @default(cuid()) name String deskripsi String @db.Text - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1663,8 +1712,8 @@ model InfoTekno { id String @id @default(cuid()) name String deskripsi String @db.Text - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1719,8 +1768,8 @@ model PengaduanMasyarakat { nik String judulPengaduan String lokasiKejadian String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? deskripsiPengaduan String @db.Text jenisPengaduan JenisPengaduan @relation(fields: [jenisPengaduanId], references: [id]) jenisPengaduanId String @@ -1801,8 +1850,8 @@ model KegiatanDesa { tanggal DateTime lokasi String partisipan Int - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1887,7 +1936,7 @@ model NilaiKonservasiAdat { // ========================================= INFO SEKOLAH & PAUD ========================================= // model JenjangPendidikan { id String @id @default(cuid()) - nama String // TK/PAUD, SD, SMP, SMA/SMK + nama String // TK/PAUD, SD, SMP, SMA/SMK lembagas Lembaga[] // Relasi ke lembaga createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -1942,23 +1991,28 @@ model KeunggulanProgram { } model BeasiswaPendaftar { - id String @id @default(cuid()) + id String @id @default(cuid()) namaLengkap String - nik String @unique + nis String? + kelas String? + jenisKelamin JenisKelamin + alamatDomisili String? tempatLahir String tanggalLahir DateTime - jenisKelamin JenisKelamin - kewarganegaraan String - agama Agama - alamatKTP String - alamatDomisili String? + namaOrtu String? + nik String @unique + pekerjaanOrtu String? + penghasilan String? noHp String - email String @unique - statusPernikahan StatusPernikahan + kewarganegaraan String? + agama Agama? + alamatKTP String? + email String? @unique + statusPernikahan StatusPernikahan? ukuranBaju UkuranBaju? - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } enum JenisKelamin { @@ -2081,8 +2135,8 @@ model DataPerpustakaan { deskripsi String @db.Text kategori KategoriBuku @relation(fields: [kategoriId], references: [id]) kategoriId String - image FileStorage @relation(fields: [imageId], references: [id]) - imageId String + image FileStorage? @relation(fields: [imageId], references: [id]) + imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -2127,28 +2181,43 @@ enum StatusPeminjaman { Dibatalkan } + +// ========================================= DATA PENDIDIKAN ========================================= // +model DataPendidikan { + id String @id @default(cuid()) + name String + jumlah String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) +} + // ========================================= USER ========================================= // model User { - id String @id @default(cuid()) - username String - nomor String @unique - role Role @relation(fields: [roleId], references: [id]) - roleId String @default("1") - instansi String? - UserSession UserSession? // Nama instansi (Puskesmas, Sekolah, dll) - isActive Boolean @default(true) - lastLogin DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? + id String @id @default(cuid()) + username String + nomor String @unique + roleId String @default("2") + isActive Boolean @default(false) + sessionInvalid Boolean @default(false) + lastLogin DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + permissions Json? + sessions UserSession[] // ✅ Relasi one-to-many + role Role @relation(fields: [roleId], references: [id]) + menuAccesses UserMenuAccess[] + + @@map("users") } model Role { id String @id @default(cuid()) name String @unique // ADMIN_DESA, ADMIN_KESEHATAN, ADMIN_SEKOLAH description String? - permissions Json // Menyimpan permission dalam format JSON + permissions Json? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -2167,35 +2236,30 @@ model KodeOtp { otp Int } -// Tabel untuk menyimpan permission -model Permission { - id String @id @default(cuid()) - name String @unique - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@map("permissions") -} - model UserSession { - id String @id @default(cuid()) - token String - expires DateTime? - active Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - User User @relation(fields: [userId], references: [id]) - userId String @unique + id String @id @default(cuid()) + token String @db.Text // ✅ JWT bisa panjang + expiresAt DateTime // ✅ Ubah jadi expiresAt (konsisten) + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String // ✅ HAPUS @unique - user bisa punya multiple sessions + + @@index([userId]) // ✅ Index untuk query cepat + @@index([token]) // ✅ Index untuk verify cepat + @@map("user_sessions") } -// ========================================= DATA PENDIDIKAN ========================================= // -model DataPendidikan { +model UserMenuAccess { id String @id @default(cuid()) - name String - jumlah String + userId String + menuId String // ID menu (misal: "Landing Page", "Kesehatan") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + + user User @relation(fields: [userId], references: [id]) + + @@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali } diff --git a/prisma/seed.ts b/prisma/seed.ts index 52de4324..3c645b5b 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,1189 +1,349 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import prisma from "@/lib/prisma"; -import profilePejabatDesa from "./data/landing-page/profile/profile.json"; -import programInovasi from "./data/landing-page/profile/programInovasi.json"; -import mediaSosial from "./data/landing-page/profile/mediaSosial.json"; -import desaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/desaantiKorpusi.json"; -import kategoriDesaAntiKorupsi from "./data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json"; -import sdgsDesa from "./data/landing-page/sdgs-desa/sdgs-desa.json"; -import apbdes from "./data/landing-page/apbdes/apbdes.json"; -import kategoriPrestasiDesa from "./data/landing-page/prestasi-desa/kategori-prestasi.json"; -import prestasiDesa from "./data/landing-page/prestasi-desa/prestasi-desa.json"; -import penghargaan from "./data/landing-page/penghargaan/penghargaan.json"; -import profilePPID from "./data/ppid/profile-ppid/profilePPid.json"; -import pegawaiPPID from "./data/ppid/struktur-ppid/pegawai-PPID.json"; -import posisiOrganisasiPPID from "./data/ppid/struktur-ppid/posisi-organisasi-PPID.json"; -import visiMisiPPID from "./data/ppid/visi-misi-ppid/visimisiPPID.json"; -import dasarHukumPPID from "./data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"; -import jenisKelamin from "./data/ppid/ikm/jenis-kelamin/jenis-kelamin.json"; -import daftarInformasiPublik from "./data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json"; -import pilihanRatingResponden from "./data/ppid/ikm/pilihan-rating-responden/rating-responden.json"; -import umurResponden from "./data/ppid/ikm/umur-responden/umur-responden.json"; -import categoryPengumuman from "./data/category-pengumuman.json"; -import pelayananPerizinanBerusaha from "./data/desa/layanan/pelayananPerizinanBerusaha.json"; -import pelayananSuratKeterangan from "./data/desa/layanan/pelayananSuratKeterangan.json"; -import pelayananTelunjukSaktiDesa from "./data/desa/layanan/pelayananTelunjukSaktiDesa.json"; -import pelayananPendudukNonPermanen from "./data/desa/layanan/pelayanaPendudukNonPermanen.json"; -import lambangDesa from "./data/desa/profile/lambang_desa.json"; -import maskotDesa from "./data/desa/profile/maskot_desa.json"; -import profilPerbekel from "./data/desa/profile/profil_perbekel.json"; -import sejarahDesa from "./data/desa/profile/sejarah_desa.json"; -import visiMisiDesa from "./data/desa/profile/visi_misi_desa.json"; -import detailDataPengangguran from "./data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json"; -import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json"; -import pegawai from "./data/ekonomi/struktur-organisasi/pegawai-bumdes.json"; -import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json"; -import kategoriBerita from "./data/desa/berita/kategori-berita.json"; -import contohEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json"; -import materiEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json"; -import tujuanEdukasiLingkungan from "./data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json"; -import bentukKonservasiBerdasarkanAdat from "./data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json"; -import kategoriKegiatanData from "./data/lingkungan/gotong-royong/kategori-gotong-royong.json"; -import filosofiTriHita from "./data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json"; -import nilaiKonservasiAdat from "./data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json"; -import caraMemperolehInformasi from "./data/list-caraMemperolehInformasi.json"; -import caraMemperolehSalinanInformasi from "./data/list-caraMemperolehSalinanInformasi.json"; -import jenisInformasiDiminta from "./data/list-jenisInfromasi.json"; -import potensi from "./data/list-potensi.json"; -import fasilitasBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json"; -import lokasiJadwalBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json"; -import tujuanBimbinganBelajarDesa from "./data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json"; -import jenisProgramYangDiselenggarakan from "./data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json"; -import tempatKegiatan from "./data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json"; -import tujuanProgram2 from "./data/pendidikan/pendidikan-non-formal/tujuan-program2.json"; -import programUnggulan from "./data/pendidikan/program-pendidikan-anak/program-unggulan.json"; -import tujuanProgram from "./data/pendidikan/program-pendidikan-anak/tujuan-program.json"; +import { seedVideo } from "./_seeder_list/desa/gallery/video/seed_video"; +import { seedLayanan } from "./_seeder_list/desa/layanan/seed_layanan"; +import { seedPenghargaan } from "./_seeder_list/desa/penghargaan/penghargaan"; +import { seedPengumuman } from "./_seeder_list/desa/pengumuman/seed_pengumuman"; +import { seedPotensi } from "./_seeder_list/desa/potensi/seed_potensi"; +import { seedProfileDesa } from "./_seeder_list/desa/profile-desa/seed_profile_desa"; +import { seedProfilePerbekel } from "./_seeder_list/desa/profile-desa/seed_profile_perbekel"; +import { seedDesaAntiKorupsi } from "./_seeder_list/landing-page/desa-anti-korupsi/seed_desa_anti_korupsi"; +import { seedPrestasiDesa } from "./_seeder_list/landing-page/prestasi-desa/seed_prestasi_desa"; +import { seedMediaSosial } from "./_seeder_list/landing-page/profil_landing_page/seed_media_sosial"; +import { seedProfileLP } from "./_seeder_list/landing-page/profil_landing_page/seed_profile_lp"; +import { seedProgramInovasi } from "./_seeder_list/landing-page/profil_landing_page/seed_program_inovasi"; +import { seedSDGSDesa } from "./_seeder_list/landing-page/sdgs/seed_sdgs"; +import { seedDaftarInformasiPublikPpid } from "./_seeder_list/ppid/daftar-informasi-publik-ppid/seed_daftar_informasi_publik_ppid"; +import { seedDasarHukumPpid } from "./_seeder_list/ppid/dasar-hukum-ppid/seed_dasar_hukum_ppid"; +import { seedIkmPpid } from "./_seeder_list/ppid/ikm/seed_ikm"; +import { seedPegawaiPpid } from "./_seeder_list/ppid/struktur-ppid/seed_struktur_ppid"; +import { seedVisiMisiPpid } from "./_seeder_list/ppid/visi-misi-ppid/seed_visi_misi_ppid"; +import dataPendidikan from "./data/pendidikan/data-pendidikan/data-pendidikan.json"; import roles from "./data/user/roles.json"; import users from "./data/user/users.json"; -import fileStorage from "./data/file-storage.json"; -import jenjangPendidikan from "./data/pendidikan/info-sekolah/jenjang-pendidikan.json"; -import seedAssets from "./seed_assets"; import { safeSeedUnique } from "./safeseedUnique"; +import seedAssets from "./seed_assets"; +import { seedBerita } from "./_seeder_list/desa/berita/seed_berita"; +import { seedFoto } from "./_seeder_list/desa/gallery/foto/seed_foto"; +import { seedPosyandu } from "./_seeder_list/kesehatan/posyandu/seed_posyandu"; +import { seedPuskesmas } from "./_seeder_list/kesehatan/puskesmas/seed_puskesmas"; +import { seedProgramKesehatan } from "./_seeder_list/kesehatan/program-kesehatan/seed_program_kesehatan"; +import { seedPenangananDarurat } from "./_seeder_list/kesehatan/penanganan-darurat/seed_penanganan_darurat"; +import { seedKontakDarurat } from "./_seeder_list/kesehatan/kontak-darurat/seed_kontak_darurat"; +import { seedInfoWabahPenyakit } from "./_seeder_list/kesehatan/info-wabah-penyakit/seed_info_wabah_penyakit"; +import { seedKeamananLingkungan } from "./_seeder_list/keamanan/seed_keamanan_lingkungan"; +import { seedPolsekTerdekat } from "./_seeder_list/keamanan/seed_polsek_terdekat"; +import { seedKontakDaruratKeamanan } from "./_seeder_list/keamanan/seed_kontak_darurat"; +import { seedPencegahanKriminalitas } from "./_seeder_list/keamanan/seed_pencegahan_kriminalitas"; +import { seedLaporanPublik } from "./_seeder_list/keamanan/seed_laporan_publik"; +import { seedPasarDesa } from "./_seeder_list/ekonomi/seed_pasar_desa"; +import { seedLowonganKerjaLokal } from "./_seeder_list/ekonomi/seed_lowongan_kerja_lokal"; +import { seedDemografiPekerjaan } from "./_seeder_list/ekonomi/seed_demografi_pekerjaan"; +import { seedSektorUnggulanDesa } from "./_seeder_list/ekonomi/seed_sektor_unggulan_desa"; +import { seedProgramKemiskinan } from "./_seeder_list/ekonomi/seed_program_kemiskinan"; +import { seedJumlahPendudukMiskin } from "./_seeder_list/ekonomi/seed_jumlah_penduduk_miskin"; +import { seedPendudukUsiaKerjaYangMenganggur } from "./_seeder_list/ekonomi/seed_penduduk_usia_kerja_yang_menganggur"; +import { seedJumlahPengangguran } from "./_seeder_list/ekonomi/seed_jumlah_pengangguran"; +import { seedPendapatanAsli } from "./_seeder_list/ekonomi/seed_pendapatan_asli"; +import { seedStrukturBumdes } from "./_seeder_list/ekonomi/seed_struktur_bumdes"; +import { seedAjukan } from "./_seeder_list/inovasi/seed_ajukan"; +import { seedDesaDigital } from "./_seeder_list/inovasi/seed_desa_digital"; +import { seedLayananOnlineDesa } from "./_seeder_list/inovasi/seed_layanan_online_desa"; +import { seedProgramKreatifDesa } from "./_seeder_list/inovasi/seed_program_kreatif_desa"; +import { seedKolaborasiInovasi } from "./_seeder_list/inovasi/seed_kolaborasi_inovasi"; +import { seedInfoTeknologi } from "./_seeder_list/inovasi/seed_info_teknologi"; +import { seedPengelolaanSampah } from "./_seeder_list/lingkungan/seed_pengelolaan_sampah"; +import { seedProgramPenghijauan } from "./_seeder_list/lingkungan/seed_program_penghijauan"; +import { seedDataLingkunganDesa } from "./_seeder_list/lingkungan/seed_data_lingkungan_desa"; +import { seedDataGotongRoyong } from "./_seeder_list/lingkungan/seed_data_gotong_royong"; +import { seedEdukasiLingkungan } from "./_seeder_list/lingkungan/seed_edukasi_lingkungan"; +import { seedKonservasiAdatBali } from "./_seeder_list/lingkungan/seed_konservasi_adat_bali"; +import { seedInfoSekolah } from "./_seeder_list/pendidikan/seed_info_sekolah"; +import { seedInfoProgramPendidikan } from "./_seeder_list/pendidikan/seed_info_program_pendidikan"; +import { seedBimbinganBelajar } from "./_seeder_list/pendidikan/seed_bimbingan_belajar"; +import { seedDataPendidikan } from "./_seeder_list/pendidikan/seed_data_pendidikan"; +import { seedPendidikanNonFormal } from "./_seeder_list/pendidikan/seed_pendidikan_non_formal"; +import { seedDataPerpustakaan } from "./_seeder_list/pendidikan/seed_data_perpustakaan"; +import { seedProfilPpd } from "./_seeder_list/ppid/profil-ppid/seed_profil_ppd"; (async () => { - // =========== USER & ROLE =========== - // In your seed.ts - // =========== ROLES =========== - console.log("🔄 Seeding roles..."); - for (const r of roles) { - await safeSeedUnique("role", { id: r.id }, { - name: r.name, - description: r.description, - permissions: r.permissions, - isActive: r.isActive, - }); - } - - console.log("✅ Roles seeded"); - - // =========== USERS =========== - console.log("🔄 Seeding users..."); - for (const u of users) { - // First verify the role exists - const roleExists = await prisma.role.findUnique({ - where: { id: u.roleId }, - }); - - if (!roleExists) { - console.error(`❌ Role with id ${u.roleId} not found for user ${u.nama}`); - continue; - } - - await safeSeedUnique("user", { id: u.id }, { - username: u.nama, - nomor: u.nomor, - roleId: u.roleId, - isActive: u.isActive, - }); - } - console.log("✅ Users seeded"); - - // =========== FILE STORAGE =========== - console.log("🔄 Seeding file storage..."); - for (const f of fileStorage) { - await prisma.fileStorage.upsert({ - where: { id: f.id }, - update: { - name: f.name, - realName: f.realName, - path: f.path, - mimeType: f.mimeType, - link: f.link, - category: f.category, - }, - create: { - id: f.id, - name: f.name, - realName: f.realName, - path: f.path, - mimeType: f.mimeType, - link: f.link, - category: f.category, - }, - }); - } - console.log("✅ File storage seeded"); - // =========== LANDING PAGE =========== - // =========== SUBMENU PROFILE =========== - // =========== PROFILE PEJABAT DESA =========== - for (const p of profilePejabatDesa) { - await prisma.pejabatDesa.upsert({ - where: { id: p.id }, - update: { - name: p.name, - position: p.position, - imageId: p.imageId, - }, - create: { - id: p.id, - name: p.name, - position: p.position, - imageId: p.imageId, - }, - }); - } - console.log( - "✅ profilePejabatDesa seeded without imageId (editable later via UI)" - ); - - // =========== PROGRAM INOVASI =========== - for (const p of programInovasi) { - let imageId: string | null = null; - - if (p.imageId) { - const imageExists = await prisma.fileStorage.findUnique({ - where: { id: p.imageId }, - }); - - if (imageExists) { - imageId = p.imageId; - } else { - console.warn( - `⚠️ imageId ${p.imageId} tidak ditemukan untuk ProgramInovasi ${p.name}` - ); - } - } - await prisma.programInovasi.upsert({ - where: { id: p.id }, - update: { - name: p.name, - description: p.description, - link: p.link, - imageId: p.imageId, - }, - create: { - id: p.id, - name: p.name, - description: p.description, - link: p.link, - imageId: p.imageId, - }, - }); - } - console.log("program inovasi success ..."); - - // =========== MEDIA SOSIAL =========== - for (const p of mediaSosial) { - await prisma.mediaSosial.upsert({ - where: { id: p.id }, - update: { - name: p.name, - iconUrl: p.iconUrl, - imageId: p.imageId, - }, - create: { - id: p.id, - name: p.name, - iconUrl: p.iconUrl, - imageId: p.imageId, - }, - }); - } - console.log("media sosial success ..."); - - // =========== SUBMENU DESA ANTI KORUPSI =========== - // =========== KATEGORI DESA ANTI KORUPSI =========== - 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 ..."); - - // =========== KATEGORI DESA ANTI KORUPSI =========== - for (const p of kategoriDesaAntiKorupsi) { - await prisma.kategoriDesaAntiKorupsi.upsert({ - where: { id: p.id }, - update: { - name: p.name, - }, - create: { - id: p.id, - name: p.name, - }, - }); - } - console.log("desa anti korupsi success ..."); - - // =========== 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 ..."); - - // =========== PRESTASI DESA=========== - for (const p of prestasiDesa) { - await prisma.prestasiDesa.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("prestasi desa success ..."); - - // =========== PENGHARGAAN =========== - for (const p of penghargaan) { - await prisma.penghargaan.upsert({ - where: { id: p.id }, - update: { - name: p.name, - juara: p.juara, - deskripsi: p.deskripsi, - }, - create: { - id: p.id, - name: p.name, - juara: p.juara, - deskripsi: p.deskripsi, - }, - }); - } - console.log("penghargaan success ..."); - - // =========== LAYANAN DESA =========== - for (const p of pelayananSuratKeterangan) { - await prisma.pelayananSuratKeterangan.upsert({ - where: { id: p.id }, - update: { - name: p.name, - deskripsi: p.deskripsi, - }, - create: { - id: p.id, - name: p.name, - deskripsi: p.deskripsi, - }, - }); - } - 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 surat keterangan success ..."); - - // =========== SDGSDesa =========== - for (const l of sdgsDesa) { - await prisma.sdgsDesa.upsert({ - where: { id: l.id }, - update: { - name: l.name, - jumlah: l.jumlah, - }, - create: { - id: l.id, - name: l.name, - jumlah: l.jumlah, - }, - }); - } - - console.log("sdgs desa success ..."); - - // =========== APBDes =========== - for (const l of apbdes) { - await prisma.aPBDes.upsert({ - where: { - id: l.id, - }, - update: { - name: l.name, - jumlah: l.jumlah, - }, - create: { - name: l.name, - jumlah: l.jumlah, - }, - }); - } - - console.log("sdgs desa success ..."); - - // =========== MENU DESA =========== - // =========== SUBMENU PROFILE =========== - // =========== 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 ..."); - - // =========== 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 ..."); - - // =========== 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 ..."); - - // =========== PROFIL PERBEKEL =========== - for (const c of profilPerbekel) { - await prisma.profilPerbekel.upsert({ - where: { id: c.id }, - update: { - biodata: c.biodata, - pengalaman: c.pengalaman, - pengalamanOrganisasi: c.pengalamanOrganisasi, - programUnggulan: c.programUnggulan, - // imageId tidak di-update - }, - create: { - id: c.id, - biodata: c.biodata, - pengalaman: c.pengalaman, - pengalamanOrganisasi: c.pengalamanOrganisasi, - programUnggulan: c.programUnggulan, - // imageId tidak di-create - }, - }); - } - console.log( - "✅ profilePerbekel seeded without imageId (editable later via UI)" - ); - - // =========== 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 ..."); - - // =========== MENU PPID =========== - // =========== SUBMENU PROFILE PPID =========== - for (const c of profilePPID) { - await prisma.profilePPID.upsert({ - where: { id: c.id }, - update: { - name: c.name, - biodata: c.biodata, - riwayat: c.riwayat, - pengalaman: c.pengalaman, - unggulan: c.unggulan, - // imageId tidak di-update - }, - create: { - id: c.id, - name: c.name, - biodata: c.biodata, - riwayat: c.riwayat, - pengalaman: c.pengalaman, - unggulan: c.unggulan, - // imageId tidak di-create - }, - }); - } - console.log("✅ profilePPID seeded without imageId (editable later via UI)"); - - // =========== SUBMENU STRUKTUR PPID =========== - // =========== POSISI ORGANISASI PPID =========== - - 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"); - - // =========== PEGAWAI PPID =========== - const flattenedPegawai = pegawaiPPID.flat(); - for (const p of flattenedPegawai) { - await prisma.pegawaiPPID.upsert({ - where: { id: p.id }, - update: p, - create: p, - }); - } - console.log("pegawai berhasil"); - - // =========== SUBMENU VISI MISI PPID =========== - - 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 ..."); - - // =========== SUBMENU DASAR HUKUM PPID =========== - 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 ..."); - - // =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID =========== - 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 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 ..."); - - for (const p of potensi) { - await prisma.potensi.upsert({ - where: { - name: p.name, - }, - update: { - name: p.name, - }, - create: { - name: p.name, - }, - }); - } - - console.log("potensi success ..."); - - for (const k of kategoriBerita) { - await prisma.kategoriBerita.upsert({ - where: { - name: k.name, - }, - update: { - name: k.name, - }, - create: { - name: k.name, - }, - }); - } - - console.log("kategori berita success ..."); - - for (const c of categoryPengumuman) { - await prisma.categoryPengumuman.upsert({ - where: { - name: c.name, - }, - update: { - name: c.name, - }, - create: { - name: c.name, - }, - }); - } - - console.log("category pengumuman 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 ..."); - - 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 ..."); - - 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 success ..."); - - const flattenedPosisiBumdes = posisiOrganisasi.flat(); - - // ✅ Urutkan berdasarkan hierarki - const sortedPosisiBumdes = flattenedPosisiBumdes.sort((a, b) => a.hierarki - b.hierarki); - - for (const p of sortedPosisiBumdes) { - 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 ..."); - - for (const d of detailDataPengangguran) { - 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 ..."); - - // =========== KATEGORI GOTONG ROYONG =========== - // Add IDs to the kategoriKegiatan data - const kategoriKegiatan = kategoriKegiatanData.map((k, index) => ({ - ...k, - id: `kategori-${index + 1}` - })); - - for (const k of kategoriKegiatan) { - await prisma.kategoriKegiatan.upsert({ - where: { - id: k.id, - }, - update: { - nama: k.nama, - }, - create: { - id: k.id, - nama: k.nama, - }, - }); - } - - console.log("kategori kegiatan success ..."); - - 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 ..."); - - 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 ..."); - - 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)"); - - 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)" - ); - - for (const t of tujuanProgram2) { - 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)" - ); - - for (const j of jenjangPendidikan) { - await prisma.jenjangPendidikan.upsert({ - where: { - id: j.id || undefined, - }, - update: { - nama: j.nama, - }, - create: { - nama: j.nama, - }, - }); - } - - console.log("✅ Jenjang Pendidikan seeded successfully"); - - // seed assets + // Always run seedAssets to handle new images without duplication + console.log("📂 Checking for new assets to seed..."); await seedAssets(); + // // =========== FILE STORAGE =========== + + console.log("🔄 Seeding roles..."); + + for (const r of roles) { + try { + // ✅ Destructure to remove permissions if exists + const { permissions, ...roleData } = r as any; + + await safeSeedUnique( + "role", + { name: roleData.name }, + { + id: roleData.id, + name: roleData.name, + description: roleData.description, + permissions: roleData.permissions || {}, // ✅ Include permissions + isActive: roleData.isActive, + }, + ); + console.log(`✅ Seeded role -> ${roleData.name}`); + } catch (error: any) { + if (error.code === "P2002") { + console.warn(`⚠️ Role already exists (skipping): ${r.name}`); + } else { + console.error(`❌ Failed to seed role ${r.name}:`, error.message); + } + } + } + console.log("✅ Roles seeding completed"); + + // =========== USER =========== + console.log("🔄 Seeding users..."); + for (const u of users) { + try { + // Verify role exists first + const roleExists = await prisma.role.findUnique({ + where: { id: u.roleId.toString() }, + select: { id: true }, // Only select id to minimize query + }); + + if (!roleExists) { + console.error( + `❌ Role with id ${u.roleId} not found for user ${u.username}`, + ); + continue; + } + + await safeSeedUnique( + "user", + { nomor: u.nomor }, + { + id: u.id, + username: u.username, + nomor: u.nomor, + roleId: u.roleId.toString(), + isActive: u.isActive, + sessionInvalid: false, + }, + ); + console.log(`✅ Seeded user -> ${u.username}`); + } catch (error: any) { + if (error.code === "P2003") { + console.error( + `❌ Foreign key constraint failed for user ${u.username}: Role ${u.roleId} does not exist`, + ); + } else { + console.error(`❌ Failed to seed user ${u.username}:`, error.message); + } + } + } + console.log("✅ Users seeding completed"); + // =========== LANDING PAGE =========== + // =========== SUBMENU PROFILE =========== + await seedProgramInovasi(); + await seedProfileLP(); + await seedMediaSosial(); + + // // =========== SUBMENU DESA ANTI KORUPSI =========== + await seedDesaAntiKorupsi(); + + // // =========== SDGSDesa =========== + await seedSDGSDesa(); + + // // =========== APBDes =========== + // for (const l of apbdes) { + // await prisma.aPBDes.upsert({ + // where: { + // id: l.id, + // }, + // update: { + // name: l.name, + // jumlah: l.jumlah, + // }, + // create: { + // name: l.name, + // jumlah: l.jumlah, + // }, + // }); + // } + + // console.log("apbdes success ..."); + + // // =========== PRESTASI DESA=========== + await seedPrestasiDesa(); + + // // =========== MENU PPID =========== + + // // =========== SUBMENU PROFIL PPID =========== + await seedProfilPpd(); + + // // =========== SUBMENU STRUKTUR PPID =========== + await seedPegawaiPpid(); + + // // =========== SUBMENU VISI MISI PPID =========== + await seedVisiMisiPpid(); + + // // =========== SUBMENU DASAR HUKUM PPID =========== + await seedDasarHukumPpid(); + + // // =========== SUBMENU DAFTAR INFORMASI PUBLIK PPID =========== + await seedDaftarInformasiPublikPpid(); + + // // =========== SUBMENU INDEKS KEPUASAN MASYARAKAT =========== + await seedIkmPpid(); + + // // =========== MENU DESA =========== + // // =========== SUBMENU PROFILE =========== + await seedProfileDesa(); + await seedProfilePerbekel(); + + // // =========== SUBMENU POTENSI DESA =========== + await seedPotensi(); + + // // =========== SUBMENU BERITA =========== + await seedBerita(); + + // // ================== SUBMENU PENGUMUMAN ======================== + await seedPengumuman(); + + // // ================== SUBMENU GALLERY ======================== + await seedVideo(); + await seedFoto(); + + // // =========== SUBMENU LAYANAN =========== + await seedLayanan(); + + // // =========== PENGHARGAAN =========== + await seedPenghargaan(); + + // // ====================== MENU KESEHATAN ======================== + // // ==================== SUBMENU POSYANDU ========================= + await seedPosyandu(); + + // // ==================== SUBMENU PUSKESMAS ========================= + await seedPuskesmas(); + + // // ==================== SUBMENU PROGRAM KESEHATAN ========================= + await seedProgramKesehatan(); + + // // ==================== SUBMENU PENANGANAN DARURAT ========================= + await seedPenangananDarurat(); + + // // ==================== SUBMENU KONTAK DARURAT ========================= + await seedKontakDarurat(); + + // // ==================== SUBMENU INFO WABAH PENYAKIT ========================= + await seedInfoWabahPenyakit(); + + // // ====================== MENU KEAMANAN ======================== + // // ==================== SUBMENU KEAMANAN LINGKUNGAN ============ + await seedKeamananLingkungan(); + + // // ==================== SUBMENU POLSEK TERDEKAT ================ + await seedPolsekTerdekat(); + + // // ==================== SUBMENU KONTAK DARURAT ================= + await seedKontakDaruratKeamanan(); + + // // ==================== SUBMENU PENCEGAHAN KRIMINALITAS ======== + await seedPencegahanKriminalitas(); + // // ==================== SUBMENU LAPORAN PUBLIK ================= + await seedLaporanPublik(); + + // // ==================== SUBMENU TIPS KEAMANAN ================== + await seedKeamananLingkungan(); + + // // ====================== MENU EKONOMI ======================== + // // ==================== SUBMENU PASAR DESA ==================== + await seedPasarDesa(); + + // // ==================== SUBMENU LOWONGAN KERJA LOKAL ========== + await seedLowonganKerjaLokal(); + + // // ==================== SUBMENU STRUKTUR ORGANISASI DAN SK PENGURUS BUMDES ========== + await seedStrukturBumdes(); + + // // ==================== SUBMENU PENDAPATAN ASLI DESA ========== + await seedPendapatanAsli(); + + // // ==================== SUBMENU JUMLAH PENGANGGURAN ========== + await seedJumlahPengangguran(); + + // // ==================== SUBMENU PENDUDUK USIA KERJA ========== + await seedPendudukUsiaKerjaYangMenganggur(); + + // // ==================== SUBMENU PENDUDUK MISKIN ============= + await seedJumlahPendudukMiskin(); + + // // ==================== SUBMENU PROGRAM KEMISKINAN ============= + await seedProgramKemiskinan(); + + // // ==================== SUBMENU SEKTOR UNGGULAN DESA ============= + await seedSektorUnggulanDesa(); + + // // ==================== SUBMENU DEMOGRAFI PEKERJAAN ============= + await seedDemografiPekerjaan(); + + // // ====================== MENU INOVASI ========================== + // // ====================== SUBMENU AJUKAN IDE INOVATIF =========== + await seedAjukan(); + // // ==================== SUBMENU DESA DIGITAL ==================== + await seedDesaDigital(); + + // // ==================== SUBMENU LAYANAN ONLINE DESA ========== + await seedLayananOnlineDesa(); + + // // ==================== SUBMENU PROGRAM KREATIF ========== + await seedProgramKreatifDesa(); + // // ==================== SUBMENU KOLABORASI INOVASI ========== + await seedKolaborasiInovasi(); + + // // ==================== SUBMENU INFO TEKNOLOGI TEPAT GUNA ========== + await seedInfoTeknologi(); + + // // ====================== MENU LINGKUNGAN ========================== + // // ==================== SUBMENU PENGELOLAAN SAMPAH ========== + await seedPengelolaanSampah(); + + // // ==================== SUBMENU PROGRAM PENGHIJAUAN ========== + await seedProgramPenghijauan(); + + // // ==================== SUBMENU DATA LINGKUNGAN DESA ========== + await seedDataLingkunganDesa(); + + // // =========== SUBMENU GOTONG ROYONG =========== + await seedDataGotongRoyong(); + + // // =========== SUBMENU EDUKASI LINGKUNGAN =========== + await seedEdukasiLingkungan(); + + // // =========== SUBMENU KONSERVASI ADAT BALI =========== + await seedKonservasiAdatBali(); + + // // ====================== MENU PENDIDIKAN ========================== + // // =========== SUBMENU INFO SEKOLAH ===================== + await seedInfoSekolah(); + + // // =========== SUBMENU PROGRAM PENDIDIKAN ANAK ===================== + + await seedInfoProgramPendidikan(); + + // // =========== SUBMENU BIMBINGAN BELAJAR DESA ===================== + await seedBimbinganBelajar(); + + // // =========== SUBMENU PENDIDIKAN NON FORMAL ===================== + await seedPendidikanNonFormal(); + // // =========== SUBMENU PERPUSTAKAAN DIGITAL ===================== + await seedDataPerpustakaan(); + + // =========== SUBMENU DATA PENDIDIKAN ===================== + await seedDataPendidikan(); })() .then(() => prisma.$disconnect()) .catch((e) => { diff --git a/prisma/seed_assets.ts b/prisma/seed_assets.ts index f92c0d36..11f1ede2 100644 --- a/prisma/seed_assets.ts +++ b/prisma/seed_assets.ts @@ -1,118 +1,49 @@ -// prisma/seedAssets.ts -import fs from "fs/promises"; -import path from "path"; -import sharp from "sharp"; -import fetch from "node-fetch"; -import AdmZip from "adm-zip"; +/* eslint-disable @typescript-eslint/no-explicit-any */ import prisma from "@/lib/prisma"; -const UPLOADS_DIR = - process.env.WIBU_UPLOAD_DIR || path.join(process.cwd(), "uploads"); - -// --- Helper: deteksi kategori file --- -function detectCategory(filename: string): "image" | "document" | "other" { - const ext = path.extname(filename).toLowerCase(); - if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image"; - if ([".pdf", ".doc", ".docx"].includes(ext)) return "document"; - return "other"; -} - -// --- Helper: recursive walk dir --- -async function walkDir(dir: string, fileList: string[] = []): Promise { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - if (entry.name === "__MACOSX") continue; // skip folder sampah - await walkDir(fullPath, fileList); - } else { - if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah - fileList.push(fullPath); - } - } - - return fileList; -} +import { getAllPublicCdnUrls } from "./lib/create_file_share_folder"; export default async function seedAssets() { - console.log("🚀 Seeding assets..."); + const images = await getAllPublicCdnUrls(); - // 1. Download zip - const url = - "https://cld-dkr-makuro-seafile.wibudev.com/f/ffd5a548a04f47939474/?dl=1"; - const res = await fetch(url); - if (!res.ok) throw new Error(`Gagal download assets: ${res.statusText}`); - const buffer = Buffer.from(await res.arrayBuffer()); + for (const img of images) { + try { + // Check if the image already exists by name + const existingImage = await prisma.fileStorage.findUnique({ + where: { name: img.name }, + }); - // 2. Extract zip ke folder tmp - const extractDir = path.join(process.cwd(), "tmp_assets"); - await fs.rm(extractDir, { recursive: true, force: true }); - await fs.mkdir(extractDir, { recursive: true }); - - const zip = new AdmZip(buffer); - zip.extractAllTo(extractDir, true); - - // 3. Cari semua file valid (recursive) - const files = await walkDir(extractDir); - - // 4. Loop tiap file & simpan - for (const filePath of files) { - const entryName = path.basename(filePath); - const category = detectCategory(entryName); - - let finalName = entryName; - let mimeType = "application/octet-stream"; - let targetPath = ""; - - if (category === "image") { - const fileBaseName = path.parse(entryName).name; - finalName = `${fileBaseName}.webp`; - targetPath = path.join(UPLOADS_DIR, "images", finalName); - await fs.mkdir(path.dirname(targetPath), { recursive: true }); - await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); - mimeType = "image/webp"; - } else if (category === "document") { - targetPath = path.join(UPLOADS_DIR, "documents", entryName); - await fs.mkdir(path.dirname(targetPath), { recursive: true }); - await fs.copyFile(filePath, targetPath); - mimeType = "application/pdf"; - } else { - targetPath = path.join(UPLOADS_DIR, "other", entryName); - await fs.mkdir(path.dirname(targetPath), { recursive: true }); - await fs.copyFile(filePath, targetPath); + if (!existingImage) { + // Only create if it doesn't exist + await prisma.fileStorage.create({ + data: { + name: img.name, + category: "image", + mimeType: "image/webp", + link: img.cdnUrl, + path: "images", + realName: img.name, + isActive: true, + }, + }); + console.log(`✅ Created new image: ${img.name}`); + } else { + console.log(`ℹ️ Image already exists, skipping: ${img.name}`); + } + } catch (err) { + console.log(`❌ Failed to seed asset ${img.name}:`, JSON.stringify(err)); } - - // 5. Simpan ke DB - await prisma.fileStorage.create({ - data: { - name: finalName, - realName: entryName, - path: targetPath, - mimeType, - link: `/uploads/${category}/${finalName}`, - category, - }, - }); - - console.log(`📂 saved: ${category}/${finalName}`); } - // 6. Cleanup - await fs.rm(extractDir, { recursive: true, force: true }); - - console.log("✅ Selesai seed assets!"); + console.log("🎉 Image seeding completed"); } -// --- Auto run kalau dipanggil langsung --- if (import.meta.main) { seedAssets() - .catch((err) => { - console.error("❌ Error seeding assets:", err); - process.exit(1); + .then(() => { + console.log("seed assets success"); }) - .finally(async () => { - await prisma.$disconnect(); + .catch((err) => { + console.log("gagal seed assets", JSON.stringify(err)); }); } diff --git a/public/mangupuraaward.jpeg b/public/mangupuraaward.jpeg new file mode 100644 index 00000000..22606461 Binary files /dev/null and b/public/mangupuraaward.jpeg differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..11c993b7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Desa Darmasaba", + "short_name": "Darmasaba", + "description": "Website resmi Desa Darmasaba, Kabupaten Badung, Bali", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#1e40af", + "icons": [ + { + "src": "/darmasaba-icon.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/darmasaba-icon.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/_com/createEditor.tsx b/src/app/admin/(dashboard)/_com/createEditor.tsx index 7878e59a..86cf608c 100644 --- a/src/app/admin/(dashboard)/_com/createEditor.tsx +++ b/src/app/admin/(dashboard)/_com/createEditor.tsx @@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline'; import TextAlign from '@tiptap/extension-text-align'; import Superscript from '@tiptap/extension-superscript'; import SubScript from '@tiptap/extension-subscript'; +import { useEffect } from 'react'; type CreateEditorProps = { value: string; @@ -32,6 +33,13 @@ export default function CreateEditor({ value, onChange }: CreateEditorProps) { }, }); + // 👇 Tambahkan efek untuk sinkronisasi value dari luar (resetForm) + useEffect(() => { + if (editor && value !== editor.getHTML()) { + editor.commands.setContent(value || ''); + } + }, [value, editor]); + return ( diff --git a/src/app/admin/(dashboard)/_com/editEditor.tsx b/src/app/admin/(dashboard)/_com/editEditor.tsx index 66317cc2..004a0dc0 100644 --- a/src/app/admin/(dashboard)/_com/editEditor.tsx +++ b/src/app/admin/(dashboard)/_com/editEditor.tsx @@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) { editor.off('update', updateHandler); }; }, [editor, onChange]); + return ( diff --git a/src/app/admin/(dashboard)/_com/header.tsx b/src/app/admin/(dashboard)/_com/header.tsx index 39735f4d..0b7c345b 100644 --- a/src/app/admin/(dashboard)/_com/header.tsx +++ b/src/app/admin/(dashboard)/_com/header.tsx @@ -1,7 +1,11 @@ +'use client'; + import React from 'react'; -import { Grid, GridCol, Paper, TextInput, Title } from '@mantine/core'; +import { Grid, GridCol, Paper, TextInput } from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; -import colors from '@/con/colors'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedTitle } from '@/components/admin/UnifiedTypography'; type HeaderSearchProps = { title: string; @@ -18,13 +22,16 @@ const HeaderSearch = ({ value, onChange, }: HeaderSearchProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); + return ( - {title} + {title} - + diff --git a/src/app/admin/(dashboard)/_com/iconMap.tsx b/src/app/admin/(dashboard)/_com/iconMap.tsx index de88a3dc..f6da2248 100644 --- a/src/app/admin/(dashboard)/_com/iconMap.tsx +++ b/src/app/admin/(dashboard)/_com/iconMap.tsx @@ -27,8 +27,26 @@ import { IconFiretruck, IconBuilding, IconAlertTriangle, + + // ===== Tambahan ===== + IconLifebuoy, + IconRun, + IconShield, + IconPhoneCall, + IconFirstAidKit, + IconStethoscope, + IconBuildingCommunity, + IconFileText, + IconInfoCircle, + IconMessageReport, + IconUsers, + IconQuestionMark, + IconBook, } from '@tabler/icons-react' +/* ======================= + Icon Keys (DB Safe) +======================= */ export type IconKey = | 'ekowisata' | 'kompetisi' @@ -50,14 +68,34 @@ export type IconKey = | 'pelatihan' | 'subsidi' | 'layananKesehatan' + + // ===== Keamanan & Darurat ===== | 'polisi' | 'ambulans' | 'pemadam' - | 'rumahSakit' - | 'bangunan' | 'darurat' + | 'sar' + | 'evakuasi' + | 'keamanan' + | 'teleponDarurat' + // ===== Kesehatan ===== + | 'rumahSakit' + | 'puskesmas' + | 'klinik' + // ===== Pemerintahan ===== + | 'bangunan' + | 'kantorDesa' + | 'administrasi' + | 'informasi' + | 'pengaduan' + | 'layananPublik' + | 'book' + +/* ======================= + Icon Map +======================= */ const iconMap: Record> = { ekowisata: IconLeaf, kompetisi: IconTrophy, @@ -79,22 +117,46 @@ const iconMap: Record> = { pelatihan: IconSchool, subsidi: IconShoppingCart, layananKesehatan: IconHospital, + + // ===== Keamanan & Darurat ===== polisi: IconShieldFilled, ambulans: IconAmbulance, pemadam: IconFiretruck, + darurat: IconAlertTriangle, + sar: IconLifebuoy, + evakuasi: IconRun, + keamanan: IconShield, + teleponDarurat: IconPhoneCall, + + // ===== Kesehatan ===== rumahSakit: IconHospital, + puskesmas: IconFirstAidKit, + klinik: IconStethoscope, + + // ===== Pemerintahan ===== bangunan: IconBuilding, - darurat: IconAlertTriangle + kantorDesa: IconBuildingCommunity, + administrasi: IconFileText, + informasi: IconInfoCircle, + pengaduan: IconMessageReport, + layananPublik: IconUsers, + book: IconBook } +/* ======================= + Icon Mapper Component +======================= */ type Props = { name: IconKey size?: number color?: string } -export const IconMapper: React.FC = ({ name, size = 24, color }) => { - const IconComponent = iconMap[name] - if (!IconComponent) return null +export const IconMapper: React.FC = ({ + name, + size = 24, + color, +}) => { + const IconComponent = iconMap[name] ?? IconQuestionMark return } diff --git a/src/app/admin/(dashboard)/_com/judulList.tsx b/src/app/admin/(dashboard)/_com/judulList.tsx index 4eaa5731..7f376d11 100644 --- a/src/app/admin/(dashboard)/_com/judulList.tsx +++ b/src/app/admin/(dashboard)/_com/judulList.tsx @@ -1,12 +1,16 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text } from '@mantine/core'; +import { Grid, GridCol, Button } from '@mantine/core'; import { IconCircleDashedPlus } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; const JudulList = ({ title = "", href = "#" }) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -16,10 +20,18 @@ const JudulList = ({ title = "", href = "#" }) => { return ( - {title} + {title} - diff --git a/src/app/admin/(dashboard)/_com/judulListTab.tsx b/src/app/admin/(dashboard)/_com/judulListTab.tsx index 21037671..dda1fe69 100644 --- a/src/app/admin/(dashboard)/_com/judulListTab.tsx +++ b/src/app/admin/(dashboard)/_com/judulListTab.tsx @@ -1,9 +1,11 @@ 'use client' -import colors from '@/con/colors'; -import { Grid, GridCol, Button, Text, Paper, TextInput } from '@mantine/core'; +import { Grid, GridCol, Button, Paper, TextInput } from '@mantine/core'; import { IconCircleDashedPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React from 'react'; +import { useDarkMode } from '@/state/darkModeStore'; +import { themeTokens } from '@/utils/themeTokens'; +import { UnifiedText } from '@/components/admin/UnifiedTypography'; type JudulListTabProps = { title: string; @@ -14,17 +16,16 @@ type JudulListTabProps = { onChange?: (e: React.ChangeEvent) => void; } - - - const JudulListTab = ({ title = "", href = "#", placeholder = "pencarian", searchIcon = , value, - onChange + onChange }: JudulListTabProps) => { + const { isDark } = useDarkMode(); + const tokens = themeTokens(isDark); const router = useRouter(); const handleNavigate = () => { @@ -34,10 +35,17 @@ const JudulListTab = ({ return ( - {title} + + {title} + - + - diff --git a/src/app/admin/(dashboard)/_com/modalNonaktif.tsx b/src/app/admin/(dashboard)/_com/modalNonaktif.tsx new file mode 100644 index 00000000..9b3c3826 --- /dev/null +++ b/src/app/admin/(dashboard)/_com/modalNonaktif.tsx @@ -0,0 +1,36 @@ +// components/modal/ModalKonfirmasiHapus.tsx +import colors from "@/con/colors" +import { Modal, Text, Button, Flex } from "@mantine/core" + +interface ModalKonfirmasiNonAktifProps { + opened: boolean + loading?: boolean + onClose: () => void + onConfirm: () => void + text: string +} + +export function ModalKonfirmasiNonAktif({ + opened, + loading = false, + onClose, + onConfirm, + text, +}: ModalKonfirmasiNonAktifProps) { + return ( + Konfirmasi Non Aktif} + centered + > + {text} + + + + + + ) +} diff --git a/src/app/admin/(dashboard)/_com/selectIcon.tsx b/src/app/admin/(dashboard)/_com/selectIcon.tsx index 88e17af8..421ba3ca 100644 --- a/src/app/admin/(dashboard)/_com/selectIcon.tsx +++ b/src/app/admin/(dashboard)/_com/selectIcon.tsx @@ -5,28 +5,40 @@ import { Box, rem, Select } from '@mantine/core'; import { IconAlertTriangle, IconAmbulance, + IconBook, IconBuilding, + IconBuildingCommunity, IconCash, IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDroplet, + IconFileText, IconFiretruck, + IconFirstAidKit, IconHome, IconHomeEco, IconHospital, + IconInfoCircle, IconLeaf, + IconLifebuoy, + IconMessageReport, + IconPhoneCall, IconRecycle, + IconRun, IconScale, IconSchool, + IconShield, IconShieldFilled, IconShoppingCart, + IconStethoscope, IconTent, IconTrashFilled, IconTree, IconTrendingUp, IconTrophy, IconTruckFilled, + IconUsers, } from '@tabler/icons-react'; import { useEffect, useState } from 'react'; @@ -51,15 +63,33 @@ const iconMap = { pelatihan: { label: 'Pelatihan', icon: IconSchool }, subsidi: { label: 'Subsidi', icon: IconShoppingCart }, layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital }, + + // ===== Keamanan & Darurat ===== polisi: { label: 'Polisi', icon: IconShieldFilled }, ambulans: { label: 'Ambulans', icon: IconAmbulance }, - pemadam: { label: 'Pemadam', icon: IconFiretruck }, - rumahSakit: { label: 'Rumah Sakit', icon: IconHospital }, - bangunan: { label: 'Bangunan', icon: IconBuilding }, + pemadam: { label: 'Pemadam Kebakaran', icon: IconFiretruck }, darurat: { label: 'Darurat', icon: IconAlertTriangle }, + sar: { label: 'SAR / Basarnas', icon: IconLifebuoy }, + evakuasi: { label: 'Evakuasi', icon: IconRun }, + keamanan: { label: 'Keamanan', icon: IconShield }, + teleponDarurat: { label: 'Telepon Darurat', icon: IconPhoneCall }, + // ===== Kesehatan ===== + rumahSakit: { label: 'Rumah Sakit', icon: IconHospital }, + puskesmas: { label: 'Puskesmas', icon: IconFirstAidKit }, + klinik: { label: 'Klinik', icon: IconStethoscope }, + + // ===== Pemerintahan & Fasilitas ===== + bangunan: { label: 'Bangunan', icon: IconBuilding }, + kantorDesa: { label: 'Kantor Desa', icon: IconBuildingCommunity }, + administrasi: { label: 'Administrasi', icon: IconFileText }, + informasi: { label: 'Informasi', icon: IconInfoCircle }, + pengaduan: { label: 'Pengaduan', icon: IconMessageReport }, + layananPublik: { label: 'Layanan Publik', icon: IconUsers }, + book: { label: 'Buku', icon: IconBook } }; + type IconKey = keyof typeof iconMap; const iconList = Object.entries(iconMap).map(([value, data]) => ({ diff --git a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx index 093a41df..2f87177e 100644 --- a/src/app/admin/(dashboard)/_com/selectIconEdit.tsx +++ b/src/app/admin/(dashboard)/_com/selectIconEdit.tsx @@ -1,6 +1,6 @@ -'use client' +'use client'; -import { Box, rem, Select } from '@mantine/core'; +import { Box, Group, rem, Select, SelectProps } from '@mantine/core'; import { IconAmbulance, IconCash, @@ -25,7 +25,19 @@ import { IconTrophy, IconTruckFilled, IconBuilding, - IconAlertTriangle + IconAlertTriangle, + IconBuildingCommunity, + IconFileText, + IconFirstAidKit, + IconInfoCircle, + IconLifebuoy, + IconMessageReport, + IconPhoneCall, + IconRun, + IconShield, + IconStethoscope, + IconUsers, + IconBook, } from '@tabler/icons-react'; const iconMap = { @@ -38,26 +50,44 @@ const iconMap = { scale: { label: 'Scale', icon: IconScale }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, trash: { label: 'Trash', icon: IconTrashFilled }, - lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, - sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, - ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, - mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, - rumah: {label: 'Rumah', icon: IconHome}, - pohon: {label: 'Pohon', icon: IconTree}, - air: {label: 'Air', icon: IconDroplet}, - bantuan: {label: 'Bantuan', icon: IconCash}, - pelatihan: {label: 'Pelatihan', icon: IconSchool}, - subsidi: {label: 'Subsidi', icon: IconShoppingCart}, - layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital}, - polisi: {label: 'Polisi', icon: IconShieldFilled}, - ambulans: {label: 'Ambulans', icon: IconAmbulance}, - pemadam: {label: 'Pemadam', icon: IconFiretruck}, - rumahSakit: {label: 'Rumah Sakit', icon: IconHospital}, - bangunan: {label: 'Bangunan', icon: IconBuilding}, - darurat: {label: 'Darurat', icon: IconAlertTriangle}, + lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco }, + sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled }, + ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp }, + mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled }, + rumah: { label: 'Rumah', icon: IconHome }, + pohon: { label: 'Pohon', icon: IconTree }, + air: { label: 'Air', icon: IconDroplet }, + bantuan: { label: 'Bantuan', icon: IconCash }, + pelatihan: { label: 'Pelatihan', icon: IconSchool }, + subsidi: { label: 'Subsidi', icon: IconShoppingCart }, + layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital }, + + // ===== Keamanan & Darurat ===== + polisi: { label: 'Polisi', icon: IconShieldFilled }, + ambulans: { label: 'Ambulans', icon: IconAmbulance }, + pemadam: { label: 'Pemadam Kebakaran', icon: IconFiretruck }, + darurat: { label: 'Darurat', icon: IconAlertTriangle }, + sar: { label: 'SAR / Basarnas', icon: IconLifebuoy }, + evakuasi: { label: 'Evakuasi', icon: IconRun }, + keamanan: { label: 'Keamanan', icon: IconShield }, + teleponDarurat: { label: 'Telepon Darurat', icon: IconPhoneCall }, + + // ===== Kesehatan ===== + rumahSakit: { label: 'Rumah Sakit', icon: IconHospital }, + puskesmas: { label: 'Puskesmas', icon: IconFirstAidKit }, + klinik: { label: 'Klinik', icon: IconStethoscope }, + + // ===== Pemerintahan & Fasilitas ===== + bangunan: { label: 'Bangunan', icon: IconBuilding }, + kantorDesa: { label: 'Kantor Desa', icon: IconBuildingCommunity }, + administrasi: { label: 'Administrasi', icon: IconFileText }, + informasi: { label: 'Informasi', icon: IconInfoCircle }, + pengaduan: { label: 'Pengaduan', icon: IconMessageReport }, + layananPublik: { label: 'Layanan Publik', icon: IconUsers }, + book: { label: 'Buku', icon: IconBook } }; -type IconKey = keyof typeof iconMap; +export type IconKey = keyof typeof iconMap; const iconList = Object.entries(iconMap).map(([value, data]) => ({ value, @@ -67,44 +97,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({ export default function SelectIconProgramEdit({ onChange, value, + ...props }: { - onChange: (value: IconKey) => void; - value: IconKey; -}) { - const IconComponent = iconMap[value]?.icon || null; - + onChange: (value: IconKey | '') => void; + value: IconKey | ''; +} & Omit) { return ( val && onChange(val as SosmedKey)} + styles={{ + input: { + textAlign: 'left', + fontSize: rem(16), + paddingLeft: 36, + }, + section: { + left: 10, + right: 'auto', + }, + }} + /> + + {/* 🔥 PREVIEW DIPISAH DI LUAR SELECT */} + {selectedImage && ( + + + + )} + + ); +} diff --git a/src/app/admin/(dashboard)/_com/selectSocialMediaEdit.tsx b/src/app/admin/(dashboard)/_com/selectSocialMediaEdit.tsx new file mode 100644 index 00000000..185037fd --- /dev/null +++ b/src/app/admin/(dashboard)/_com/selectSocialMediaEdit.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { Box, Select } from '@mantine/core'; +import { useEffect, useState } from 'react'; + +export const sosmedMap = { + facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' }, + instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' }, + tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' }, + youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' }, + whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' }, + gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' }, + telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' }, + x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' }, + telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' }, + custom: { label: 'Custom Icon', src: null }, +}; + +type SosmedKey = keyof typeof sosmedMap; + +const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({ + value, + label: item.label, +})); + +export default function SelectSocialMediaEdit({ + value, + onChange, +}: { + value: string; + onChange: (val: SosmedKey) => void; +}) { + const [selected, setSelected] = useState('facebook'); + + useEffect(() => { + if (value && sosmedMap[value as SosmedKey]) { + setSelected(value as SosmedKey); + } + }, [value]); + + return ( + + { - pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? ""; - }} data={pengumumanState.category.findMany.data?.map((item) => ({ label: item.name, value: item.id, - }))} + })) || []} + value={pengumumanState.pengumuman.create.form.categoryPengumumanId || null} + onChange={(val: string | null) => { + if (val) { + const selected = pengumumanState.category.findMany.data?.find( + (item) => item.id === val + ); + if (selected) { + pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id; + } + } else { + pengumumanState.pengumuman.create.form.categoryPengumumanId = ''; + } + }} searchable + clearable nothingFoundMessage="Tidak ditemukan" + required /> {/* Deskripsi Singkat */} (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)} label="Deskripsi Singkat" placeholder="Masukkan deskripsi singkat" @@ -115,17 +172,31 @@ function CreatePengumuman() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx index e4c819e4..a309b68d 100644 --- a/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx +++ b/src/app/admin/(dashboard)/desa/pengumuman/list-pengumuman/page.tsx @@ -17,10 +17,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -47,46 +46,58 @@ function Pengumuman() { function ListPengumuman({ search }: { search: string }) { const pengumumanState = useProxy(stateDesaPengumuman); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = pengumumanState.pengumuman.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - - Daftar Pengumuman - - - + + + + + Daftar Pengumuman + + - - + + {/* Desktop Table */} + +
- Judul - Kategori - Detail + + Judul + + + Kategori + + + Detail + @@ -94,14 +105,12 @@ function ListPengumuman({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.judul} - - + + {item.judul} + - + {item.CategoryPengumuman?.name || '-'} @@ -112,9 +121,12 @@ function ListPengumuman({ search }: { search: string }) { onClick={() => router.push(`/admin/desa/pengumuman/list-pengumuman/${item.id}`) } + fz="sm" + px="sm" + py="xs" > - - Detail + + Detail @@ -122,8 +134,10 @@ function ListPengumuman({ search }: { search: string }) { ) : ( -
- Tidak ada pengumuman yang cocok +
+ + Tidak ada pengumuman yang cocok +
@@ -131,7 +145,59 @@ function ListPengumuman({ search }: { search: string }) {
+ + {/* Mobile Card List */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Judul + + + {item.judul} + + + + + Kategori + + + {item.CategoryPengumuman?.name || '-'} + + + + + + + + )) + ) : ( +
+ + Tidak ada pengumuman yang cocok + +
+ )} +
+
+
, - tooltip: "Lihat semua potensi desa" + icon: }, { label: "Kategori Potensi", value: "kategori_potensi", href: "/admin/desa/potensi/kategori-potensi", - icon: , - tooltip: "Kelola kategori potensi" + icon: }, ]; @@ -70,19 +68,18 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) { }} > {tabs.map((tab, i) => ( - - - {tab.label} - - + + {tab.label} + ))} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx index 16d0d951..66809e6c 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/[id]/page.tsx @@ -10,7 +10,7 @@ import { Stack, TextInput, Title, - Tooltip + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -27,6 +27,19 @@ function EditKategoriPotensi() { nama: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' + ); + }; + // Load data dari backend -> isi ke formData lokal useEffect(() => { const loadKategori = async () => { @@ -39,6 +52,9 @@ function EditKategoriPotensi() { setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { console.error('Error loading kategori potensi:', error); @@ -56,8 +72,21 @@ function EditKategoriPotensi() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { + if (!formData.nama?.trim()) { + toast.error('Nama kategori potensi wajib diisi'); + return; + } + try { + setIsSubmitting(true); // Update global state hanya pas submit editState.update.form = { ...editState.update.form, @@ -70,13 +99,14 @@ function EditKategoriPotensi() { } catch (error) { console.error('Error updating kategori potensi:', error); toast.error('Terjadi kesalahan saat memperbarui kategori potensi'); + } finally { + setIsSubmitting(false); } }; return ( - + - - Edit Kategori Potensi @@ -109,17 +138,31 @@ function EditKategoriPotensi() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx index 68f63d0a..72659e74 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/create/page.tsx @@ -9,15 +9,24 @@ import { Stack, TextInput, Title, - Tooltip + Loader } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; function CreateKategoriPotensi() { const createState = useProxy(potensiDesaState.kategoriPotensi); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.form.nama?.trim() !== '' + ); + }; const resetForm = () => { createState.create.form = { @@ -26,25 +35,35 @@ function CreateKategoriPotensi() { }; const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push('/admin/desa/potensi/kategori-potensi'); + if (!createState.create.form.nama?.trim()) { + alert('Nama kategori potensi wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push('/admin/desa/potensi/kategori-potensi'); + } catch (error) { + console.error('Error creating kategori potensi:', error); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header dengan back button */} - - - + Tambah Kategori Potensi @@ -63,23 +82,37 @@ function CreateKategoriPotensi() { (createState.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx index 3d408f8f..8f4af700 100644 --- a/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/kategori-potensi/page.tsx @@ -1,14 +1,32 @@ /* eslint-disable react-hooks/exhaustive-deps */ -'use client' +'use client'; import colors from '@/con/colors'; -import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core'; -import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import potensiDesaState from '../../../_state/desa/potensi'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import potensiDesaState from '../../../_state/desa/potensi'; +import { useDebouncedValue } from '@mantine/hooks'; function KategoriPotensi() { const [search, setSearch] = useState(''); @@ -27,87 +45,112 @@ function KategoriPotensi() { } function ListKategoriPotensi({ search }: { search: string }) { - const listDataState = useProxy(potensiDesaState.kategoriPotensi) + const listDataState = useProxy(potensiDesaState.kategoriPotensi); const router = useRouter(); - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); const { data, page, totalPages, loading, load } = listDataState.findMany; + const [debouncedSearch] = useDebouncedValue(search, 1000); useEffect(() => { - load(1, 10, search) - }, [search]) + load(1, 10, debouncedSearch); + }, [debouncedSearch]); const handleDelete = () => { if (selectedId) { - listDataState.delete.delete(selectedId) - setModalHapus(false) - setSelectedId(null) - load(page, 10, search) + listDataState.delete.delete(selectedId); + setModalHapus(false); + setSelectedId(null); + load(page, 10, search); } - } + }; - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } return ( - + - - - List Kategori Potensi - - - + + + + List Kategori Potensi + + - - + {/* Desktop Table */} + +
- No - Nama - Edit - Hapus + + + Nama + + + + + Edit + + + + + Hapus + + {filteredData.length > 0 ? ( - filteredData.map((item, index) => ( + filteredData.map((item) => ( - {(page - 1) * 10 + index + 1} + + {item.nama} + - {item.nama} - - - @@ -116,8 +159,10 @@ function ListKategoriPotensi({ search }: { search: string }) { ) : ( -
- Tidak ada data kategori potensi yang cocok +
+ + Tidak ada data kategori potensi yang cocok +
@@ -125,10 +170,70 @@ function ListKategoriPotensi({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + + + + No + + + {(page - 1) * 10 + index + 1} + + + + + Nama + + + {item.nama} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data kategori potensi yang cocok + +
+ )} +
-
+
load(newPage, 10, search)} @@ -145,7 +250,7 @@ function ListKategoriPotensi({ search }: { search: string }) { text='Apakah anda yakin ingin menghapus kategori Potensi ini?' /> - ) + ); } -export default KategoriPotensi; +export default KategoriPotensi; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/potensi/layout.tsx b/src/app/admin/(dashboard)/desa/potensi/layout.tsx index b677970c..7e83e086 100644 --- a/src/app/admin/(dashboard)/desa/potensi/layout.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/layout.tsx @@ -1,8 +1,29 @@ 'use client' import React from 'react'; import LayoutTabsPotensi from './_lib/layoutTabs'; +import { usePathname } from 'next/navigation'; +import { Box } from '@mantine/core'; function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + return ( {children} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx index 68a453b3..600fc2ef 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/edit/page.tsx @@ -16,7 +16,8 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; @@ -39,6 +40,34 @@ function EditPotensi() { content: "", imageId: "", }); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.kategoriId !== '' && + (file !== null || originalData.imageId !== '') && // Either a new file is selected or an existing image exists + !isHtmlEmpty(formData.content) + ); + }; + + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + kategoriId: "", + content: "", + imageId: "", + imageUrl: "", + }); // handle input changes const handleChange = (field: string, value: string) => { @@ -47,11 +76,11 @@ function EditPotensi() { useEffect(() => { potensiDesaState.kategoriPotensi.findMany.load(); - + const loadPotensi = async () => { const id = params?.id as string; if (!id) return; - + try { const data = await potensiState.edit.load(id); if (data) { @@ -62,35 +91,70 @@ function EditPotensi() { content: data.content || "", imageId: data.imageId || "", }); - - // // merge, bukan replace - // setFormData((prev) => ({ - // ...prev, - // name: data.name ?? prev.name, - // deskripsi: data.deskripsi ?? prev.deskripsi, - // kategoriId: data.kategoriId ?? prev.kategoriId, - // content: data.content ?? prev.content, - // imageId: data.imageId ?? prev.imageId, - // })); - - if (data?.image?.link) { - setPreviewImage(data.image.link); - } + + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + kategoriId: data.kategoriId || "", + content: data.content || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }); + setPreviewImage(data.image.link); + } } catch (error) { console.error("Error loading potensi:", error); toast.error("Gagal memuat data potensi"); } }; - + loadPotensi(); }, [params?.id]); - + + const handleResetForm = () => { + setFormData({ + name: originalData.name || "", + deskripsi: originalData.deskripsi || "", + kategoriId: originalData.kategoriId || "", + content: originalData.content || "", + imageId: originalData.imageId || "" + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { - try { - let imageId = formData.imageId; + if (!formData.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi singkat wajib diisi'); + return; + } + + if (!formData.kategoriId) { + toast.error('Kategori wajib dipilih'); + return; + } + + if (!file && !originalData.imageId) { + toast.error('Gambar wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.content)) { + toast.error('Konten lengkap wajib diisi'); + return; + } + try { + setIsSubmitting(true); + let imageId = formData.imageId; if (file) { const res = await ApiFetch.api.fileStorage.create.post({ file, @@ -116,22 +180,22 @@ function EditPotensi() { } catch (error) { console.error("Error updating potensi:", error); toast.error("Terjadi kesalahan saat memperbarui potensi"); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Potensi Desa @@ -167,6 +231,32 @@ function EditPotensi() { handleChange("kategoriId", val || "")} label="Kategori" @@ -181,7 +271,7 @@ function EditPotensi() { searchable required error={!formData.kategoriId ? "Pilih kategori" : undefined} - /> + /> */} @@ -222,25 +312,45 @@ function EditPotensi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -258,17 +368,32 @@ function EditPotensi() { + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx index 73531d4e..35fe407a 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/[id]/page.tsx @@ -1,13 +1,14 @@ 'use client' -import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi'; +import colors from '@/con/colors'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import DOMPurify from 'dompurify'; export default function DetailPotensi() { const router = useRouter(); @@ -40,7 +41,7 @@ export default function DetailPotensi() { const data = potensiState.findUnique.data; return ( - + - - - diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx index 6eeb29b4..3fc3fa66 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/create/page.tsx @@ -15,7 +15,8 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -29,30 +30,82 @@ function CreatePotensi() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + potensiState.create.form.name?.trim() !== '' && + !isHtmlEmpty(potensiState.create.form.deskripsi) && + potensiState.create.form.kategoriId !== '' && + file !== null && + !isHtmlEmpty(potensiState.create.form.content) + ); + }; useEffect(() => { potensiDesaState.kategoriPotensi.findMany.load(); }, []); const handleSubmit = async () => { - if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); + if (!potensiState.create.form.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(potensiState.create.form.deskripsi)) { + toast.error('Deskripsi singkat wajib diisi'); + return; + } + + if (!potensiState.create.form.kategoriId) { + toast.error('Kategori wajib dipilih'); + return; + } + + if (!file) { + toast.error('Gambar wajib dipilih'); + return; + } + + if (isHtmlEmpty(potensiState.create.form.content)) { + toast.error('Konten lengkap wajib diisi'); + return; } - potensiState.create.form.imageId = uploaded.id; + try { + setIsSubmitting(true); + if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); - await potensiState.create.create(); + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); - resetForm(); - router.push('/admin/desa/potensi/list-potensi'); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + potensiState.create.form.imageId = uploaded.id; + + await potensiState.create.create(); + + resetForm(); + router.push('/admin/desa/potensi/list-potensi'); + } catch (error) { + console.error('Error creating potensi:', error); + toast.error('Terjadi kesalahan saat menambahkan potensi'); + } finally { + setIsSubmitting(false); + } }; const resetForm = () => { @@ -69,14 +122,12 @@ function CreatePotensi() { }; return ( - + {/* Header */} - - - + Tambah Potensi Desa @@ -93,7 +144,7 @@ function CreatePotensi() { {/* Judul */} (potensiState.create.form.name = val.target.value)} label="Judul" placeholder="Masukkan judul potensi" @@ -115,6 +166,32 @@ function CreatePotensi() { {/* Kategori */} + /> */} {/* Upload Gambar */} @@ -142,7 +219,7 @@ function CreatePotensi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -160,17 +237,44 @@ function CreatePotensi() { Seret gambar atau klik untuk memilih file (maks 5MB) + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -190,17 +294,31 @@ function CreatePotensi() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx index 0599c55b..5ed853da 100644 --- a/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx +++ b/src/app/admin/(dashboard)/desa/potensi/list-potensi/page.tsx @@ -18,8 +18,7 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -27,6 +26,8 @@ import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import potensiDesaState from '../../../_state/desa/potensi'; +import { useDebouncedValue } from '@mantine/hooks'; +import DOMPurify from 'dompurify'; function Potensi() { const [search, setSearch] = useState(""); @@ -47,6 +48,7 @@ function Potensi() { function ListPotensi({ search }: { search: string }) { const potensiState = useProxy(potensiDesaState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -58,43 +60,63 @@ function ListPotensi({ search }: { search: string }) { useEffect(() => { potensiState.kategoriPotensi.findMany.load(); - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - - Daftar Potensi Desa - - - + + + + + Daftar Potensi Desa + + - - + + {/* Desktop Table */} + +
- Judul - Kategori - Deskripsi - Detail + + + Judul + + + + + Kategori + + + + + Deskripsi + + + + + Detail + + @@ -102,27 +124,28 @@ function ListPotensi({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - {item.kategori?.nama || '-'} - + + {item.kategori?.nama || '-'} + - - - +
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + + {filteredData.map((item) => ( + + + + Judul + + + {item.name} + + + + + Kategori + + + {item.kategori?.nama || '-'} + + + + + Deskripsi + + + + + + ))} + + ) : ( +
+ + Tidak ada data potensi yang cocok + +
+ )} +
+
+ }, + { + label: "Profil Perbekel", + value: "profilperbekel", + href: "/admin/desa/profil/profil-perbekel", + icon: + }, + { + label: "Profil Perbekel Dari Masa Ke Masa", + value: "profilperbekeldarimasakemasa", + href: "/admin/desa/profil/profil-perbekel-dari-masa-ke-masa", + icon: + } + ]; + + const currentTab = tabs.find(tab => tab.href === pathname) + const [activeTab, setActiveTab] = useState(currentTab?.value || tabs[0].value); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find(t => t.value === value) + if (tab) { + router.push(tab.href) + } + setActiveTab(value) + } + + useEffect(() => { + const match = tabs.find(tab => tab.href === pathname) + if (match) { + setActiveTab(match.value) + } + }, [pathname]) + + return ( + + Profile Desa + + {/* ✅ Scroll horizontal wrapper */} + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + {tabs.map((tab, i) => ( + + {/* Konten dummy, bisa diganti sesuai routing */} + <>{children} + + ))} + + + ); +} + +export default LayoutTabsDetail; diff --git a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsEdit.tsx b/src/app/admin/(dashboard)/desa/profil/_lib/layoutTabsEdit.tsx similarity index 89% rename from src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsEdit.tsx rename to src/app/admin/(dashboard)/desa/profil/_lib/layoutTabsEdit.tsx index 75201d3d..c384b326 100644 --- a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsEdit.tsx +++ b/src/app/admin/(dashboard)/desa/profil/_lib/layoutTabsEdit.tsx @@ -12,22 +12,22 @@ function LayoutTabsEdit({ children }: { children: React.ReactNode }) { { label: "Sejarah Desa", value: "sejarahdesa", - href: "/admin/desa/profile/edit/sejarah_desa" + href: "/admin/desa/profil/edit/sejarah_desa" }, { label: "Visi Misi Desa", value: "visimisidesa", - href: "/admin/desa/profile/edit/visi_misi_desa" + href: "/admin/desa/profil/edit/visi_misi_desa" }, { label: "Lambang Desa", value: "lambangdesa", - href: "/admin/desa/profile/edit/lambang_desa" + href: "/admin/desa/profil/edit/lambang_desa" }, { label: "Maskot Desa", value: "maskotdesa", - href: "/admin/desa/profile/edit/maskot_desa" + href: "/admin/desa/profil/edit/maskot_desa" }, ]; const curentTab = tabs.find(tab => tab.href === pathname) diff --git a/src/app/admin/(dashboard)/desa/profil/layout.tsx b/src/app/admin/(dashboard)/desa/profil/layout.tsx new file mode 100644 index 00000000..475635df --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/layout.tsx @@ -0,0 +1,33 @@ +'use client' + +import { usePathname } from "next/navigation"; +import LayoutTabsDetail from "./_lib/layoutTabsDetail" +import { Box } from "@mantine/core"; + +export default function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx new file mode 100644 index 00000000..955ce10d --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/lambang_desa/page.tsx @@ -0,0 +1,266 @@ +'use client'; +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; +import colors from '@/con/colors'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; +import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; + +// 🧩 Type untuk form +interface FormData { + judul: string; + deskripsi: string; +} + +// 🧩 Main Component +function Page() { + const router = useRouter(); + const params = useParams(); + + const [formData, setFormData] = useState({ judul: '', deskripsi: '' }); + const [originalData, setOriginalData] = useState({ judul: '', deskripsi: '' }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); + + // 🧭 Load data awal + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profil/profil-desa'); + return; + } + + setIsLoading(true); + setLoadError(null); + + try { + const data = await stateProfileDesa.lambangDesa.findUnique.load(id); + + if (data) { + const initial: FormData = { + judul: data.judul || '', + deskripsi: data.deskripsi || '', + }; + setFormData(initial); + setOriginalData(initial); + + // Penting untuk isi id di state sebelum submit + stateProfileDesa.lambangDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); + } + } catch (error) { + console.error('Error loading lambang:', error); + setLoadError('Gagal memuat data lambang desa'); + toast.error('Gagal memuat data lambang desa'); + } finally { + setIsLoading(false); + } + }; + + loadData(); + + return () => { + stateProfileDesa.lambangDesa.update.reset(); + stateProfileDesa.lambangDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + + // 🔁 Reset form + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit handler + const handleSubmit = async () => { + if (!formData.judul.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + setIsSubmitting(true); + try { + const state = stateProfileDesa.lambangDesa; + state.update.form.judul = formData.judul; + state.update.form.deskripsi = formData.deskripsi; + + const success = await state.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profil/profil-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update lambang desa:', error); + toast.error('Terjadi kesalahan saat update lambang desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 📝 Handlers + const handleJudulChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, judul: e.target.value })); + }; + + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; + + const handleBack = () => router.back(); + + // 🔄 Loading + if (isLoading) { + return ( + +
+ + + + Memuat data lambang desa... + + +
+
+ ); + } + + // ❌ Error + if (loadError) { + return ( + + + + } color="red" title="Terjadi Kesalahan" radius="md"> + {loadError} + + + + + ); + } + + // 🧱 UI utama + return ( + + + {/* Header */} + + + + Edit Lambang Desa + + + + {/* Form */} + + + {/* Judul */} + Judul} + placeholder="Masukkan judul lambang desa" + value={formData.judul} + onChange={handleJudulChange} + error={!formData.judul.trim() && 'Judul wajib diisi'} + required + size="md" + radius="md" + /> + + {/* Deskripsi */} + + + Deskripsi + + + + + {/* Tombol Aksi */} + + + + + + + + + ); +} + +export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx similarity index 72% rename from src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx rename to src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx index 8ead63e3..f5dfff4d 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/maskot_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx @@ -5,9 +5,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } from '@mantine/core'; +import { Alert, Box, Button, Center, Group, Image, Loader, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } from '@tabler/icons-react'; +import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -19,8 +19,8 @@ function Page() { const params = useParams(); const [images, setImages] = useState< - Array<{ file: File | null; preview: string; label: string; imageId?: string }> ->([]); + Array<{ file: File | null; preview: string; label: string; imageId?: string }> + >([]); const [formData, setFormData] = useState({ judul: '', deskripsi: '', @@ -28,13 +28,34 @@ function Page() { }); const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + images: [] as Array<{ label: string; imageId: string }> + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data useEffect(() => { const loadData = async () => { const id = params?.id as string; if (!id) { toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); + router.push("/admin/desa/profil/profil-desa"); return; } @@ -52,6 +73,17 @@ function Page() { })), }); + setOriginalData({ + judul: data.judul || '', + deskripsi: data.deskripsi || '', + images: (data.images || []).map((img: any) => ({ + label: img.label, + imageId: img.image?.id ?? '', + preview: img.image?.link ?? '', + })), + }); + + if (data?.images?.length > 0 && data.images[0].image?.link) { setImages(data.images.map((img: any) => ({ file: null, @@ -77,15 +109,42 @@ function Page() { const handleBack = () => router.back(); + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + images: originalData.images.map((img) => ({ + label: img.label, + imageId: img.imageId, + })), + }); + + setImages( + originalData.images.map((img: any) => ({ + file: null, + preview: img.preview, // pakai preview masing-masing, bukan cuma satu + label: img.label, + imageId: img.imageId, + })) + ); + + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { if (isSubmitting || !formData.judul.trim()) { toast.error("Judul wajib diisi"); return; } - - setIsSubmitting(true); - + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error("Deskripsi wajib diisi"); + return; + } + try { + setIsSubmitting(true); const uploadedImages = []; // Upload semua gambar baru @@ -95,7 +154,7 @@ function Page() { uploadedImages.push({ imageId: img.imageId, label: img.label }); continue; } - + // upload baru const res = await ApiFetch.api.fileStorage.create.post({ file: img.file, @@ -108,7 +167,7 @@ function Page() { } uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" }); } - + // Update ke global state maskotState.update.updateField("judul", formData.judul); @@ -119,7 +178,7 @@ function Page() { if (success) { toast.success("Maskot berhasil diperbarui!"); - router.push("/admin/desa/profile/profile-desa"); + router.push("/admin/desa/profil/profil-desa"); } } catch (error) { console.error("Error update maskot:", error); @@ -132,7 +191,7 @@ function Page() { // Loading state if (maskotState.findUnique.loading || maskotState.update.loading) { return ( - +
Memuat data...
@@ -143,7 +202,7 @@ function Page() { // Error state if (maskotState.findUnique.error) { return ( - + - + Edit Maskot Desa @@ -177,7 +234,7 @@ function Page() { Judul} placeholder="Masukkan judul maskot" - defaultValue={formData.judul} + value={formData.judul} onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })} error={!formData.judul && "Judul wajib diisi"} /> @@ -233,7 +290,7 @@ function Page() { setImages(updated); }} > - Hapus + {/* Buttons */} - + + {/* Tombol Batal */} - + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx new file mode 100644 index 00000000..10f3105e --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/sejarah_desa/page.tsx @@ -0,0 +1,302 @@ +'use client'; +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; +import colors from '@/con/colors'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; +import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; + +// 🔹 Types +interface FormData { + judul: string; + deskripsi: string; +} + +// 🔹 Main Component +function Page() { + const router = useRouter(); + const params = useParams(); + + // 🧩 Local State + const [formData, setFormData] = useState({ + judul: '', + deskripsi: '', + }); + const [originalData, setOriginalData] = useState({ + judul: '', + deskripsi: '', + }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); + + // 🧭 Load Initial Data + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profil/profil-desa'); + return; + } + + setIsLoading(true); + setLoadError(null); + + try { + const data = await stateProfileDesa.sejarahDesa.findUnique.load(id); + + if (data) { + const initialData: FormData = { + judul: data.judul || '', + deskripsi: data.deskripsi || '', + }; + + setFormData(initialData); + setOriginalData(initialData); + + stateProfileDesa.sejarahDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); + } + } catch (error) { + console.error('Error loading sejarah:', error); + setLoadError('Gagal memuat data sejarah desa'); + toast.error('Gagal memuat data sejarah desa'); + } finally { + setIsLoading(false); + } + }; + + loadData(); + + return () => { + stateProfileDesa.sejarahDesa.update.reset(); + stateProfileDesa.sejarahDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // 🔄 Check if form has changes + + + // 🔁 Reset Form to Original Data + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + + // 💾 Submit Handler + const handleSubmit = async () => { + // Validation + if (!formData.judul.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + setIsSubmitting(true); + + try { + // Access original state directly (not proxy) + const originalState = stateProfileDesa.sejarahDesa; + + // Update form data + originalState.update.form.judul = formData.judul; + originalState.update.form.deskripsi = formData.deskripsi; + + // Submit + const success = await originalState.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profil/profil-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update sejarah desa:', error); + toast.error('Terjadi kesalahan saat update sejarah desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 📝 Form Field Handlers + const handleJudulChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ ...prev, judul: e.target.value })); + }; + + const handleDeskripsiChange = (html: string) => { + setFormData(prev => ({ ...prev, deskripsi: html })); + }; + + const handleBack = () => { + router.back(); + }; + // 🔄 Loading State + if (isLoading) { + return ( + +
+ + + + Memuat data... + + +
+
+ ); + } + + // ❌ Error State + if (loadError) { + return ( + + + + } + color="red" + title="Terjadi Kesalahan" + radius="md" + > + {loadError} + + + + + ); + } + + return ( + + + {/* Header */} + + + + Edit Sejarah Desa + + + + {/* Form */} + + + {/* Form Title */} + + + Informasi Sejarah Desa + + + {/* Judul Field */} + Judul Sejarah} + placeholder="Masukkan judul sejarah desa" + value={formData.judul} + onChange={handleJudulChange} + error={!formData.judul.trim() && 'Judul wajib diisi'} + required + size="md" + radius="md" + /> + + {/* Deskripsi Field */} + + + Deskripsi Sejarah + + + + + + {/* Action Buttons */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + + ); +} + +export default Page; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx new file mode 100644 index 00000000..7a743755 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/visi_misi_desa/page.tsx @@ -0,0 +1,269 @@ +'use client'; + +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; +import colors from '@/con/colors'; +import { + Alert, + Box, + Button, + Center, + Group, + Loader, + Paper, + Stack, + Text, + Title, +} from '@mantine/core'; +import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; + +// 🔹 Types +interface FormData { + visi: string; + misi: string; +} + +// 🔹 Main Component +function Page() { + const router = useRouter(); + const params = useParams(); + const [formData, setFormData] = useState({ visi: '', misi: '' }); + const [originalData, setOriginalData] = useState({ visi: '', misi: '' }); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loadError, setLoadError] = useState(null); + + // 🧭 Load Data + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/desa/profil/profil-desa'); + return; + } + + setIsLoading(true); + setLoadError(null); + + try { + const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id); + if (data) { + const initialData: FormData = { + visi: data.visi || '', + misi: data.misi || '', + }; + setFormData(initialData); + setOriginalData(initialData); + + // set id ke state agar submit pakai endpoint benar + stateProfileDesa.visiMisiDesa.update.initialize(data); + } else { + setLoadError('Data tidak ditemukan'); + } + } catch (error) { + console.error('Error load visi misi:', error); + setLoadError('Gagal memuat data visi misi desa'); + toast.error('Gagal memuat data visi misi desa'); + } finally { + setIsLoading(false); + } + }; + + loadData(); + + return () => { + stateProfileDesa.visiMisiDesa.update.reset(); + stateProfileDesa.visiMisiDesa.findUnique.reset(); + }; + }, [params?.id, router]); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + !isHtmlEmpty(formData.visi) && + !isHtmlEmpty(formData.misi) + ); + }; + + // 🔄 Reset Form + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Submit + const handleSubmit = async () => { + if (isHtmlEmpty(formData.visi)) { + toast.error('Visi wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.misi)) { + toast.error('Misi wajib diisi'); + return; + } + + setIsSubmitting(true); + try { + const originalState = stateProfileDesa.visiMisiDesa; + + // update data form ke state sebelum submit + originalState.update.form.visi = formData.visi; + originalState.update.form.misi = formData.misi; + + const success = await originalState.update.submit(); + + if (success) { + toast.success('Data berhasil disimpan'); + router.push('/admin/desa/profil/profil-desa'); + } else { + toast.error('Gagal menyimpan data'); + } + } catch (error) { + console.error('Error update visi misi desa:', error); + toast.error('Terjadi kesalahan saat update visi misi desa'); + } finally { + setIsSubmitting(false); + } + }; + + // 🧭 Field handlers + const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html })); + const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html })); + const handleBack = () => router.back(); + + // ⏳ Loading + if (isLoading) { + return ( + +
+ + + + Memuat data... + + +
+
+ ); + } + + // ❌ Error + if (loadError) { + return ( + + + + } + color="red" + title="Terjadi Kesalahan" + radius="md" + > + {loadError} + + + + + ); + } + + // ✅ UI + return ( + + + {/* Header */} + + + + Edit Visi & Misi Desa + + + + {/* Form */} + + + + + Informasi Visi & Misi Desa + + + + {/* Visi */} + + + Visi + + + + + {/* Misi */} + + + Misi + + + + + {/* Actions */} + + + + + + + + + ); +} + +export default Page; diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx new file mode 100644 index 00000000..5d7a166b --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/page.tsx @@ -0,0 +1,590 @@ +'use client' + +import colors from '@/con/colors'; +import { Box, Button, Card, Center, Divider, Group, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; +import { IconEdit } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { useSnapshot } from 'valtio'; +import stateProfileDesa from '../../../_state/desa/profile'; + +function Page() { + const router = useRouter(); + const snap = useSnapshot(stateProfileDesa); + + useEffect(() => { + stateProfileDesa.sejarahDesa.findUnique.load("edit"); + stateProfileDesa.visiMisiDesa.findUnique.load("edit"); + stateProfileDesa.lambangDesa.findUnique.load("edit"); + stateProfileDesa.maskotDesa.findUnique.load("edit"); + }, []); + + const sejarah = snap.sejarahDesa.findUnique.data; + const visiMisi = snap.visiMisiDesa.findUnique.data; + const lambang = snap.lambangDesa.findUnique.data; + const maskot = snap.maskotDesa.findUnique.data; + + return ( + + + Preview Profil Desa + + {/* Sejarah Desa */} + + + {sejarah && ( + + + Preview Sejarah Desa + + + + + + +
+ Logo Desa +
+ + + {sejarah.judul} + + +
+ + + + +
+
+
+ )} +
+ + + {sejarah && ( + + {/* Header */} + + + Preview Sejarah Desa + + + + + + {/* Content Wrapper */} + + + {/* Logo + Title */} + + Logo Desa + + + + {sejarah.judul} + + + + + + + {/* Deskripsi */} + + + + + + + )} + + + {/* Visi Misi Desa */} + + {visiMisi && ( + + + Preview Visi Misi Desa + + + + + + + +
+ Logo Desa +
+ + + Visi Misi Desa + + +
+ + + Visi Desa + + + + Misi Desa + + +
+
+
+ )} +
+ + + {visiMisi && ( + + {/* Header */} + + + Preview Visi Misi Desa + + + + + + {/* Content Wrapper */} + + + {/* Logo + Title */} + + Logo Desa + + + + Visi Misi Desa + + + + + + + + Visi Desa + + + + + Misi Desa + + + + + + )} + + + {/* Lambang Desa */} + + {lambang && ( + + + Preview Lambang Desa + + + + + + +
+ Logo Desa +
+ + + {lambang.judul} + + +
+ + + + +
+
+
+ )} +
+ + + {lambang && ( + + {/* Header */} + + + Preview Lambang Desa + + + + + + {/* Content Wrapper */} + + + {/* Logo + Title */} + + Logo Desa + + + + {lambang.judul} + + + + + + + {/* Deskripsi */} + + + + + + + )} + + + {/* Maskot Desa */} + + {maskot && ( + + + Preview Maskot Desa + + + + + + +
+ Maskot Desa +
+ + + Maskot Desa + + +
+ + + + + + + {maskot.images.map((img, idx) => ( + +
+ {img.label} +
+ {img.label} +
+ ))} +
+
+
+
+
+ )} +
+ + + {maskot && ( + + {/* Header */} + + + Preview Maskot Desa + + + + + + {/* Content Wrapper */} + + + {/* Logo + Title */} + + Logo Desa + + + + Maskot Desa + + + + + + + {/* Deskripsi */} + + + + + + {maskot.images.map((img, idx) => ( + +
+ {img.label} +
+ {img.label} +
+ ))} +
+
+
+
+
+ )} +
+
+
+ ); +} + +export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx similarity index 70% rename from src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx index 56f2c2e2..c83faf16 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/edit/page.tsx @@ -4,6 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, @@ -13,7 +14,7 @@ import { Text, TextInput, Title, - Tooltip + Loader } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -36,6 +37,16 @@ function EditPerbekelDariMasaKeMasa() { imageId: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: '', + daerah: '', + periode: '', + imageId: '', + imageUrl: "", + }); + // load data pertama kali useEffect(() => { const loadFoto = async () => { @@ -50,9 +61,14 @@ function EditPerbekelDariMasaKeMasa() { periode: data.periode || '', imageId: data.imageId || '' }); - if (data?.imageGalleryFoto?.link) { - setPreviewImage(data.imageGalleryFoto.link); - } + setOriginalData({ + nama: data.nama || '', + daerah: data.daerah || '', + periode: data.periode || '', + imageId: data.imageId || '', + imageUrl: data.image.link || '', + }) + setPreviewImage(data.image.link); } } catch (error) { console.error('Error loading foto:', error); @@ -70,8 +86,22 @@ function EditPerbekelDariMasaKeMasa() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + daerah: originalData.daerah, + periode: originalData.periode, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); // update global state hanya sekali pas submit state.update.form = { ...state.update.form, ...formData }; @@ -87,21 +117,21 @@ function EditPerbekelDariMasaKeMasa() { await state.update.update(); toast.success('Perbekel dari masa ke masa berhasil diperbarui!'); - router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); + router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa'); } catch (error) { console.error('Error updating perbekel dari masa ke masa:', error); toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Perbekel Dari Masa Ke Masa @@ -138,7 +168,7 @@ function EditPerbekelDariMasaKeMasa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -157,25 +187,45 @@ function EditPerbekelDariMasaKeMasa() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -197,6 +247,17 @@ function EditPerbekelDariMasaKeMasa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/page.tsx similarity index 87% rename from src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx rename to src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/page.tsx index 048d8494..95653279 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/[id]/page.tsx @@ -2,9 +2,9 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -25,7 +25,7 @@ function DetailPerbekelDariMasa() { state.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/desa/profile/profile-perbekel-dari-masa-ke-masa"); + router.push("/admin/desa/profil/profil-perbekel-dari-masa-ke-masa"); } }; @@ -40,7 +40,7 @@ function DetailPerbekelDariMasa() { const data = state.findUnique.data; return ( - + - - - diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/create/page.tsx similarity index 59% rename from src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx rename to src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/create/page.tsx index d2c5ca5e..aa4f519b 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/create/page.tsx @@ -2,7 +2,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Loader, ActionIcon, Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -14,6 +14,7 @@ function CreatePerbekelDariMasaKeMasa() { const state = useProxy(stateProfileDesa.mantanPerbekel); const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [file, setFile] = useState(null); const resetForm = () => { @@ -28,33 +29,39 @@ function CreatePerbekelDariMasaKeMasa() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) return toast.error('Gagal upload gambar'); + + state.create.form.imageId = uploaded.id; + await state.create.create(); + resetForm(); + router.push('/admin/desa/profil/profil-perbekel-dari-masa-ke-masa'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan perbekel dari masa ke masa'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) return toast.error('Gagal upload gambar'); - - state.create.form.imageId = uploaded.id; - await state.create.create(); - resetForm(); - router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); }; return ( - + {/* Back button + Title */} - - - + Create Perbekel Dari Masa Ke Masa @@ -72,21 +79,21 @@ function CreatePerbekelDariMasaKeMasa() { (state.create.form.nama = e.target.value)} required /> (state.create.form.daerah = e.target.value)} required /> (state.create.form.periode = e.target.value)} required /> @@ -104,7 +111,7 @@ function CreatePerbekelDariMasaKeMasa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -120,25 +127,63 @@ function CreatePerbekelDariMasaKeMasa() { - Seret gambar atau klik untuk memilih file (maks 5MB) + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/page.tsx new file mode 100644 index 00000000..7065da65 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel-dari-masa-ke-masa/page.tsx @@ -0,0 +1,193 @@ +'use client' +import colors from '@/con/colors'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import stateProfileDesa from '../../../_state/desa/profile'; + +function PerbekelDariMasaKeMasa() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListPerbekelDariMasaKeMasa({ search }: { search: string }) { + const state = useProxy(stateProfileDesa.mantanPerbekel) + const router = useRouter(); + const { data, page, totalPages, loading, load } = state.findMany; + const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay + + useShallowEffect(() => { + load(page, 10, debouncedSearch) + }, [page, debouncedSearch]); + + const filteredData = data || []; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + + + + List Perbekel Dari Masa Ke Masa + + + + + {/* Desktop Table */} + + + + + Nama Perbekel + Periode + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + {item.nama} + + + {item.periode} + + + + + + )) + ) : ( + + +
+ + Tidak ada data perbekel yang cocok + +
+
+
+ )} +
+
+
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama Perbekel + {item.nama} + + + Periode + {item.periode} + + + + + + + )) + ) : ( +
+ + Tidak ada data perbekel yang cocok + +
+ )} +
+
+
+ +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt={{ base: 'sm', md: 'md' }} + mb={{ base: 'sm', md: 'md' }} + color="blue" + radius="md" + /> +
+
+ ); +} + +export default PerbekelDariMasaKeMasa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/[id]/page.tsx similarity index 93% rename from src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx rename to src/app/admin/(dashboard)/desa/profil/profil-perbekel/[id]/page.tsx index 43eefc8a..a135b37e 100644 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/[id]/page.tsx @@ -4,9 +4,9 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react'; +import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; @@ -25,7 +25,7 @@ function ProfilePerbekel() { const id = params?.id as string; if (!id) { toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-perbekel"); + router.push("/admin/desa/profil/profil-perbekel"); return; } @@ -74,7 +74,7 @@ function ProfilePerbekel() { const success = await perbekelState.edit.submit() if (success) { toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-perbekel"); + router.push("/admin/desa/profil/profil-perbekel"); } } catch (error) { console.error("Error update sejarah desa:", error); @@ -97,15 +97,13 @@ function ProfilePerbekel() { } return ( - + {/* Header */} - - Edit Profil Perbekel @@ -128,8 +126,8 @@ function ProfilePerbekel() { handleFileChange(files[0])} onReject={() => toast.error('File tidak valid.')} - maxSize={5 * 1024 ** 2} // 5MB - accept={{ 'image/*': [] }} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > diff --git a/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx new file mode 100644 index 00000000..e79103d6 --- /dev/null +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx @@ -0,0 +1,147 @@ +'use client' + +import colors from '@/con/colors'; +import { Box, Button, Center, Divider, Grid, GridCol, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; +import { IconEdit } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { useSnapshot } from 'valtio'; +import stateProfileDesa from '../../../_state/desa/profile'; + +function Page() { + const router = useRouter(); + const snap = useSnapshot(stateProfileDesa); + + // Load data saat mount + useEffect(() => { + stateProfileDesa.profilPerbekel.findUnique.load("edit"); + }, []); + + const perbekel = snap.profilPerbekel.findUnique.data; + + if (!perbekel) { + return ( + + + + ); + } + + return ( + + + {/* Header + tombol edit */} + + Profil Perbekel + + + + {/* Card Profil */} + + + + +
+ Logo Desa +
+
+ + + Profil Pimpinan Badan Publik Desa Darmasaba + + +
+
+ + + +
+ Foto Profil Perbekel { e.currentTarget.src = "/perbekel.png"; }} + loading='lazy' + /> +
+ + + {perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."} + + +
+ + {/* Biodata & Info */} + + + <Text + fz={{ base: 'sm', md: 'md' }} + lh={{ base: 1.6, md: 1.6 }} + ta="justify" + style={{ wordBreak: "break-word", whiteSpace: "normal" }} + dangerouslySetInnerHTML={{ __html: perbekel.biodata }} + /> + + <Title order={3} lh={1.2} mt="md" mb={4} /> + <Text + fz={{ base: 'sm', md: 'md' }} + lh={{ base: 1.6, md: 1.6 }} + ta="justify" + style={{ wordBreak: "break-word", whiteSpace: "normal" }} + dangerouslySetInnerHTML={{ __html: perbekel.pengalaman }} + /> + + <Title order={3} lh={1.2} mt="md" mb={4} /> + <Text + fz={{ base: 'sm', md: 'md' }} + lh={{ base: 1.6, md: 1.6 }} + ta="justify" + style={{ wordBreak: "break-word", whiteSpace: "normal" }} + dangerouslySetInnerHTML={{ __html: perbekel.pengalamanOrganisasi }} + /> + + <Title order={3} lh={1.2} mt="md" mb={4} /> + <Text + fz={{ base: 'sm', md: 'md' }} + lh={{ base: 1.6, md: 1.6 }} + ta="justify" + style={{ wordBreak: "break-word", whiteSpace: "normal" }} + dangerouslySetInnerHTML={{ __html: perbekel.programUnggulan }} + /> + </Box> + </Paper> + </Stack> + </Paper> + ); +} + +export default Page; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx b/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx deleted file mode 100644 index 7554a1c8..00000000 --- a/src/app/admin/(dashboard)/desa/profile/_lib/layoutTabsDetail.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; -import { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react'; - -function LayoutTabsDetail({ children }: { children: React.ReactNode }) { - const router = useRouter() - const pathname = usePathname() - const tabs = [ - { - label: "Profile Desa", - value: "profiledesa", - href: "/admin/desa/profile/profile-desa", - icon: <IconUser size={18} stroke={1.8} />, - tooltip: "Lihat dan kelola profil desa" - }, - { - label: "Profile Perbekel", - value: "profileperbekel", - href: "/admin/desa/profile/profile-perbekel", - icon: <IconUsers size={18} stroke={1.8} />, - tooltip: "Kelola data Perbekel" - }, - { - label: "Profile Perbekel Dari Masa Ke Masa", - value: "profile-perbekel-dari-masa-ke-masa", - href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa", - icon: <IconCalendar size={18} stroke={1.8} />, - tooltip: "Riwayat Perbekel dari masa ke masa" - } - ]; - - const currentTab = tabs.find(tab => tab.href === pathname) - const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value); - - const handleTabChange = (value: string | null) => { - const tab = tabs.find(t => t.value === value) - if (tab) { - router.push(tab.href) - } - setActiveTab(value) - } - - useEffect(() => { - const match = tabs.find(tab => tab.href === pathname) - if (match) { - setActiveTab(match.value) - } - }, [pathname]) - - return ( - <Stack gap="lg"> - <Title order={3} fw={700} style={{ color: "#1A1B1E" }}>Profile Desa - - {/* ✅ Scroll horizontal wrapper */} - - - {tabs.map((tab, i) => ( - - - {tab.label} - - - ))} - - - - {tabs.map((tab, i) => ( - - {/* Konten dummy, bisa diganti sesuai routing */} - <>{children} - - ))} - -
- ); -} - - export default LayoutTabsDetail; diff --git a/src/app/admin/(dashboard)/desa/profile/layout.tsx b/src/app/admin/(dashboard)/desa/profile/layout.tsx deleted file mode 100644 index f82687f5..00000000 --- a/src/app/admin/(dashboard)/desa/profile/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import LayoutTabsDetail from "./_lib/layoutTabsDetail" - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx deleted file mode 100644 index 03660631..00000000 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/lambang_desa/page.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; -import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; -import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; - -function Page() { - const lambangState = useProxy(stateProfileDesa.lambangDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } - - try { - const data = await lambangState.findUnique.load(id); - lambangState.update.initialize(data); - } catch (error) { - console.error("Error loading lambang:", error); - toast.error("Gagal memuat data lambang desa"); - } - }; - - loadData(); - - return () => { - lambangState.update.reset(); - lambangState.findUnique.reset(); - }; - }, [params?.id, router]); - - const handleSubmit = async () => { - if (isSubmitting || !lambangState.update.form.judul.trim()) { - toast.error("Judul wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - const success = await lambangState.update.submit(); - - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update lambang desa:", error); - toast.error("Terjadi kesalahan saat update lambang desa"); - } finally { - setIsSubmitting(false); - } - }; - - const handleBack = () => router.back(); - - // Loading state - if (lambangState.findUnique.loading || lambangState.update.loading) { - return ( - -
- Memuat data... -
-
- ); - } - - // Error state - if (lambangState.findUnique.error) { - return ( - - - - } color="red"> - Error - {lambangState.findUnique.error} - - - - ); - } - - return ( - - - - - - - Edit Lambang Desa - - - - - Edit Lambang Desa - - {/* Judul */} - Judul} - placeholder="Judul lambang" - defaultValue={lambangState.update.form.judul} - onChange={(e) => lambangState.update.form.judul = e.currentTarget.value} - error={!lambangState.update.form.judul && "Judul wajib diisi"} - /> - - {/* Deskripsi */} - - Deskripsi - lambangState.update.form.deskripsi = val} - /> - - - {/* Buttons */} - - - - - - - - - - ); -} - -export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx deleted file mode 100644 index c941dd4f..00000000 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/sejarah_desa/page.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; -import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; -import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; - -function Page() { - const sejarahState = useProxy(stateProfileDesa.sejarahDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); - - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } - - try { - const data = await sejarahState.findUnique.load(id); - if (data) { - sejarahState.update.initialize(data); - } - } catch (error) { - console.error("Error loading sejarah:", error); - toast.error("Gagal memuat data sejarah desa"); - } - }; - - loadData(); - - return () => { - sejarahState.update.reset(); - sejarahState.findUnique.reset(); - }; - }, [params?.id, router]); - - const handleSubmit = async () => { - if (isSubmitting || !sejarahState.update.form.judul.trim()) { - toast.error("Judul wajib diisi"); - return; - } - - setIsSubmitting(true); - try { - const success = await sejarahState.update.submit(); - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update sejarah desa:", error); - toast.error("Terjadi kesalahan saat update sejarah desa"); - } finally { - setIsSubmitting(false); - } - }; - - const handleBack = () => router.back(); - - // Loading state - if (sejarahState.findUnique.loading || sejarahState.update.loading) { - return ( - -
- Memuat data... -
-
- ); - } - - // Error state - if (sejarahState.findUnique.error) { - return ( - - - - } color="red"> - Error - {sejarahState.findUnique.error} - - - - ); - } - - return ( - - - - - - - Edit Sejarah Desa - - - - - Edit Sejarah Desa - - {/* Judul */} - Judul} - placeholder="Judul sejarah" - defaultValue={sejarahState.update.form.judul} - onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} - error={!sejarahState.update.form.judul && "Judul wajib diisi"} - /> - - {/* Deskripsi */} - - Deskripsi - sejarahState.update.form.deskripsi = val} - /> - - - {/* Buttons */} - - - - - - - - - - ); -} - -export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx deleted file mode 100644 index 0a06bc88..00000000 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/[id]/visi_misi_desa/page.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; -import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; -import colors from '@/con/colors'; -import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core'; -import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; - -function Page() { - const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa) - const router = useRouter() - const params = useParams() - const [isSubmitting, setIsSubmitting] = useState(false); - - // Load data - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) { - toast.error("ID tidak valid"); - router.push("/admin/desa/profile/profile-desa"); - return; - } - - try { - const data = await visiMisiState.findUnique.load(id); - visiMisiState.update.initialize(data); - } catch (error) { - console.error("Error loading visi misi:", error); - toast.error("Gagal memuat data visi misi desa"); - } - }; - - loadData(); - - return () => { - visiMisiState.update.reset(); - visiMisiState.findUnique.reset(); - }; - }, [params?.id, router]); - - const handleSubmit = async () => { - if (isSubmitting || !visiMisiState.update.form.visi.trim()) { - toast.error("Visi wajib diisi"); - return; - } - - setIsSubmitting(true); - - try { - const success = await visiMisiState.update.submit(); - - if (success) { - toast.success("Data berhasil disimpan"); - router.push("/admin/desa/profile/profile-desa"); - } - } catch (error) { - console.error("Error update visi misi desa:", error); - toast.error("Terjadi kesalahan saat update visi misi desa"); - } finally { - setIsSubmitting(false); - } - }; - - const handleBack = () => router.back(); - - // Loading state - if (visiMisiState.findUnique.loading || visiMisiState.update.loading) { - return ( - -
- Memuat data... -
-
- ); - } - - // Error state - if (visiMisiState.findUnique.error) { - return ( - - - - } color="red"> - Error - {visiMisiState.findUnique.error} - - - - ); - } - - return ( - - - - - - - Edit Visi Misi Desa - - - - - Edit Visi Misi Desa - - {/* Visi */} - - Visi - visiMisiState.update.form.visi = val} - /> - - - {/* Misi */} - - Misi - visiMisiState.update.form.misi = val} - /> - - - {/* Buttons */} - - - - - - - - - - ); -} - -export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx deleted file mode 100644 index 0dee1b80..00000000 --- a/src/app/admin/(dashboard)/desa/profile/profile-desa/page.tsx +++ /dev/null @@ -1,248 +0,0 @@ -'use client' - -import colors from '@/con/colors'; -import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core'; -import { useSnapshot } from 'valtio'; -import stateProfileDesa from '../../../_state/desa/profile'; -import { useEffect } from 'react'; -import { IconEdit } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; - -function Page() { - const router = useRouter(); - const snap = useSnapshot(stateProfileDesa); - - useEffect(() => { - stateProfileDesa.sejarahDesa.findUnique.load("edit"); - stateProfileDesa.visiMisiDesa.findUnique.load("edit"); - stateProfileDesa.lambangDesa.findUnique.load("edit"); - stateProfileDesa.maskotDesa.findUnique.load("edit"); - }, []); - - const sejarah = snap.sejarahDesa.findUnique.data; - const visiMisi = snap.visiMisiDesa.findUnique.data; - const lambang = snap.lambangDesa.findUnique.data; - const maskot = snap.maskotDesa.findUnique.data; - - return ( - - - Preview Profile Desa - - {/* Sejarah Desa */} - {sejarah && ( - - - - Preview Sejarah Desa - - - - - - - - - - - -
- Logo Desa -
- - - {sejarah.judul} - - -
- - -
-
-
- )} - - {/* Visi Misi Desa */} - {visiMisi && ( - - - - Preview Visi Misi Desa - - - - - - - - - - - -
- Logo Desa -
- - - Visi Misi Desa - - -
- - Visi Desa - - Misi Desa - -
-
-
- )} - - {/* Lambang Desa */} - {lambang && ( - - - - Preview Lambang Desa - - - - - - - - - - - -
- Logo Desa -
- - - {lambang.judul} - - -
- - -
-
-
- )} - - {/* Maskot Desa */} - {maskot && ( - - - - Preview Maskot Desa - - - - - - - - - - - -
- Maskot Desa -
- - - Maskot Desa - - -
- - - - - {maskot.images.map((img, idx) => ( - -
- {img.label} -
- {img.label} -
- ))} -
-
-
-
-
- )} -
-
- ); -} - -export default Page; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx deleted file mode 100644 index 6957d8f5..00000000 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel-dari-masa-ke-masa/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import HeaderSearch from '../../../_com/header'; -import stateProfileDesa from '../../../_state/desa/profile'; - -function PerbekelDariMasaKeMasa() { - const [search, setSearch] = useState(""); - return ( - - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - ); -} - -function ListPerbekelDariMasaKeMasa({ search }: { search: string }) { - const state = useProxy(stateProfileDesa.mantanPerbekel) - const router = useRouter(); - const { data, page, totalPages, loading, load } = state.findMany; - - useShallowEffect(() => { - load(page, 10, search) - }, [page, search]); - - const filteredData = data || []; - - if (loading || !data) { - return ( - - - - ); - } - - return ( - - - - List Perbekel Dari Masa Ke Masa - - - - - - - - - - Nama Perbekel - Periode - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - {item.nama} - - - - - {item.periode} - - - - - - - - - )) - ) : ( - - -
- Tidak ada data perbekel yang cocok -
-
-
- )} -
-
-
-
- -
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); -} - -export default PerbekelDariMasaKeMasa; diff --git a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx deleted file mode 100644 index 73da958e..00000000 --- a/src/app/admin/(dashboard)/desa/profile/profile-perbekel/page.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client' - -import colors from '@/con/colors'; -import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core'; -import { IconEdit } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; -import { useSnapshot } from 'valtio'; -import stateProfileDesa from '../../../_state/desa/profile'; - -function Page() { - const router = useRouter(); - const snap = useSnapshot(stateProfileDesa); - - // Load data saat mount - useEffect(() => { - stateProfileDesa.profilPerbekel.findUnique.load("edit"); - }, []); - - const perbekel = snap.profilPerbekel.findUnique.data; - - if (!perbekel) { - return ( - - - - ); - } - - return ( - - - {/* Header + tombol edit */} - - - Preview Profil PPID - - - - - - - - - {/* Card Profil */} - - - - -
- Logo Desa -
-
- - - Profil Pimpinan Badan Publik Desa Darmasaba - - -
-
- - - -
- Foto Profil Perbekel { e.currentTarget.src = "/perbekel.png"; }} - loading='lazy' - /> -
- - - I.B. Surya Prabhawa Manuaba, S.H., M.H. - - -
- - {/* Biodata & Info */} - - Biodata - - - Pengalaman - - - Pengalaman Organisasi - - - Program Kerja Unggulan - - -
-
-
- ); -} - -export default Page; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx index dca2dfe2..e252c31d 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx @@ -2,23 +2,23 @@ 'use client' import colors from '@/con/colors'; import { + Box, + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; import { - IconFileAnalytics, IconCoins, + IconFileAnalytics, IconShoppingCart, IconWallet, } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -29,29 +29,25 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "APB Desa", value: "apbdesa", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa", - icon: , - tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa", + icon: }, { label: "Pendapatan", value: "pendapatan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan", icon: , - tooltip: "Kelola data pendapatan desa", }, { label: "Belanja", value: "belanja", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja", icon: , - tooltip: "Atur data belanja desa", }, { label: "Pembiayaan", value: "pembiayaan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan", icon: , - tooltip: "Kelola data pembiayaan desa", }, ]; @@ -90,43 +86,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper */} - - - {tabs.map((tab, i) => ( - + + + + {tabs.map((tab, i) => ( {tab.label} - - ))} - - + ))} + + +
+ + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( { + if (!Array.isArray(arr)) return []; + return arr + .filter(item => item != null && item !== '') + .map(item => String(item)) + .filter(item => item.trim() !== ''); +}; + +const createEmptyForm = () => ({ + tahun: '', + pendapatanIds: [] as string[], + belanjaIds: [] as string[], + pembiayaanIds: [] as string[], +}); + +// ==================== COMPONENT ==================== function EditAPBDesa() { const apbState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); const params = useParams(); - const [formData, setFormData] = useState({ - tahun: '', - pendapatanIds: [] as string[], - belanjaIds: [] as string[], - pembiayaanIds: [] as string[], - }); + const [formData, setFormData] = useState(createEmptyForm()); + const [originalData, setOriginalData] = useState(createEmptyForm()); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(true); - // Load APB desa by id → hanya update formData, bukan global state + // Check if form is valid + const isFormValid = () => { + return ( + formData.tahun?.trim() !== '' && + Number(formData.tahun) > 0 && + formData.pendapatanIds.length > 0 && + formData.belanjaIds.length > 0 && + formData.pembiayaanIds.length > 0 + ); + }; + + // ==================== LOAD DATA ==================== useEffect(() => { const loadAPBdesa = async () => { const id = params?.id as string; - if (!id) return; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + return; + } try { + setIsLoading(true); const data = await apbState.update.load(id); - if (data) { - setFormData({ - tahun: String(data.tahun || ''), - pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [], - belanjaIds: data.belanja?.map((b: any) => b.id) || [], - pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [], - }); + + if (!data) { + toast.error('Data APB Desa tidak ditemukan'); + return; } - } catch (error) { - console.error("Error loading APBdesa:", error); - toast.error("Gagal memuat data APBdesa"); + + const normalized = { + tahun: String(data.tahun || ''), + pendapatanIds: safeStringArray(data.pendapatan?.map((p: any) => p.id) || []), + belanjaIds: safeStringArray(data.belanja?.map((b: any) => b.id) || []), + pembiayaanIds: safeStringArray(data.pembiayaan?.map((p: any) => p.id) || []), + }; + + setFormData(normalized); + setOriginalData(normalized); + } catch (err) { + console.error('Error loading APBdesa:', err); + toast.error('Gagal memuat data APB Desa'); + } finally { + setIsLoading(false); } }; loadAPBdesa(); }, [params?.id]); + // ==================== HANDLERS ==================== const handleChange = (field: keyof typeof formData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { - // update global state cuma pas submit + setIsSubmitting(true); + + if (!formData.tahun.trim()) { + toast.warning('Tahun harus diisi'); + return; + } + apbState.update.form = { ...apbState.update.form, tahun: Number(formData.tahun), - pendapatanIds: formData.pendapatanIds, - belanjaIds: formData.belanjaIds, - pembiayaanIds: formData.pembiayaanIds, + pendapatanIds: safeStringArray(formData.pendapatanIds), + belanjaIds: safeStringArray(formData.belanjaIds), + pembiayaanIds: safeStringArray(formData.pembiayaanIds), }; await apbState.update.update(); - toast.success("APB Desa berhasil diperbarui!"); - router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"); + toast.success('APB Desa berhasil diperbarui!'); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); } catch (error) { - console.error("Error updating APBdesa:", error); - toast.error("Terjadi kesalahan saat memperbarui APBdesa"); + console.error('Error updating APBdesa:', error); + toast.error('Terjadi kesalahan saat memperbarui APB Desa'); + } finally { + setIsSubmitting(false); } }; + // ==================== UI ==================== + if (isLoading) { + return ( + + + Memuat data APB Desa... + + ); + } + return ( - + {/* Header */} - - - + Edit APB Desa @@ -117,151 +180,128 @@ function EditAPBDesa() { handleChange("tahun", e.target.value)} + onChange={(e) => handleChange('tahun', e.target.value)} label={Tahun} placeholder="Masukkan tahun anggaran" required /> {/* Selects */} - handleChange("pendapatanIds", ids)} + onSelectionChange={(ids) => handleChange('pendapatanIds', ids)} /> - handleChange("belanjaIds", ids)} + onSelectionChange={(ids) => handleChange('belanjaIds', ids)} /> - handleChange("pembiayaanIds", ids)} + onSelectionChange={(ids) => handleChange('pembiayaanIds', ids)} /> {/* Save Button */} - + + + ); +} - /* --- Sub Components --- */ +// ==================== SUB COMPONENT ==================== +function SelectAPBItem({ + label, + state, + selectedIds, + onSelectionChange, +}: { + label: string; + state: any; + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; +}) { + const proxyState = useProxy(state); - function SelectPendapatan({ - selectedIds, - onSelectionChange, - }: { - selectedIds: string[]; - onSelectionChange: (ids: string[]) => void; - }) { - const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); + useShallowEffect(() => { + proxyState.findMany.load(); + }, []); - useShallowEffect(() => { - pendapatanState.findMany.load(); - }, []); + const data = proxyState.findMany.data; + const isLoading = !data; - if (!pendapatanState.findMany.data) { - return ; - } + const options = + data + ?.filter((item: any) => item?.id) + .map((item: any) => ({ + value: String(item.id), + label: String(item?.name || '(Tanpa Nama)'), + })) || []; + if (isLoading) { return ( - Pendapatan} - data={pendapatanState.findMany.data.map((p: any) => ({ - value: p.id, - label: p.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih pendapatan..." - nothingFoundMessage="Tidak ditemukan" - /> + + {label} + + ); } - function SelectBelanja({ - selectedIds, - onSelectionChange, - }: { - selectedIds: string[]; - onSelectionChange: (ids: string[]) => void; - }) { - const belanjaState = useProxy(PendapatanAsliDesa.belanja); - - useShallowEffect(() => { - belanjaState.findMany.load(); - }, []); - - if (!belanjaState.findMany.data) { - return ; - } - + if (options.length === 0) { return ( - Belanja} - data={belanjaState.findMany.data.map((b: any) => ({ - value: b.id, - label: b.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih belanja..." - nothingFoundMessage="Tidak ditemukan" - /> + + + Tidak ada data {label.toLowerCase()} tersedia. + + ); } - function SelectPembiayaan({ - selectedIds, - onSelectionChange, - }: { - selectedIds: string[]; - onSelectionChange: (ids: string[]) => void; - }) { - const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); - - useShallowEffect(() => { - pembiayaanState.findMany.load(); - }, []); - - if (!pembiayaanState.findMany.data) { - return ; - } - - return ( - Pembiayaan} - data={pembiayaanState.findMany.data.map((p: any) => ({ - value: p.id, - label: p.name, - }))} - value={selectedIds} - onChange={onSelectionChange} - searchable - clearable - placeholder="Pilih pembiayaan..." - nothingFoundMessage="Tidak ditemukan" - /> - ); - } + return ( + {label}} + data={options} + value={selectedIds} + onChange={(ids) => onSelectionChange(safeStringArray(ids))} + searchable + clearable + placeholder={`Pilih ${label.toLowerCase()}...`} + nothingFoundMessage="Tidak ditemukan" + /> + ); } export default EditAPBDesa; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx index ee57dc50..3075f2e1 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx @@ -9,8 +9,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -58,7 +57,7 @@ function DetailAPBDesa() { const data = apbState.findUnique.data; return ( - + - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx index 797cc92f..cc237b70 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx @@ -6,23 +6,37 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateAPBDesa() { const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + apbDesaState.create.form.tahun !== null && + apbDesaState.create.form.tahun > 0 && + apbDesaState.create.form.pendapatanIds.length > 0 && + apbDesaState.create.form.belanjaIds.length > 0 && + apbDesaState.create.form.pembiayaanIds.length > 0 + ); + }; const resetForm = () => { apbDesaState.create.form = { @@ -34,20 +48,26 @@ function CreateAPBDesa() { }; const handleSubmit = async () => { - await apbDesaState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + try { + setIsSubmitting(true); + await apbDesaState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); + } catch (error) { + console.error('Error creating APB Desa:', error); + toast.error('Gagal membuat APB Desa'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + Tambah APB Desa @@ -65,7 +85,7 @@ function CreateAPBDesa() { { apbDesaState.create.form.tahun = Number(val.target.value); }} @@ -97,17 +117,31 @@ function CreateAPBDesa() { {/* Action */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx index ff0c6464..7c4792d4 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -16,13 +16,13 @@ import { TableThead, TableTr, Text, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; + import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; @@ -45,6 +45,7 @@ function APBDesa() { function ListAPBDesa({ search }: { search: string }) { const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -62,96 +63,122 @@ function ListAPBDesa({ search }: { search: string }) { }).format(value); useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - - + + + + List APB Desa - </Text> - <Tooltip label="Tambah APB Desa" withArrow> - <Button - leftSection={<IconPlus size={18} />} - color="blue" - variant="light" - onClick={() => - router.push( - "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create" - ) - } - > - Tambah Baru - </Button> - </Tooltip> + + - - + + {/* Desktop Table */} + +
- Tahun - Pembiayaan - Belanja - Pendapatan - Aksi + + Tahun + + + Pembiayaan + + + Belanja + + + Pendapatan + + + Aksi + {filteredData.length > 0 ? ( filteredData.map((item) => ( - {item.tahun} - {formatRupiah( - item.pembiayaan.reduce( - (sum, val) => sum + Number(val.value), - 0 - ) - )} + {item.tahun} - {formatRupiah( - item.belanja.reduce( - (sum, val) => sum + Number(val.value), - 0 - ) - )} + + {formatRupiah( + item.pembiayaan.reduce( + (sum, val) => sum + Number(val.value), + 0 + ) + )} + - {formatRupiah( - item.pendapatan.reduce( - (sum, val) => sum + Number(val.value), - 0 - ) - )} + + {formatRupiah( + item.belanja.reduce( + (sum, val) => sum + Number(val.value), + 0 + ) + )} + - - - + + {formatRupiah( + item.pendapatan.reduce( + (sum, val) => sum + Number(val.value), + 0 + ) + )} + + + + )) @@ -159,7 +186,7 @@ function ListAPBDesa({ search }: { search: string }) {
- + Tidak ada data APB Desa yang cocok
@@ -169,7 +196,81 @@ function ListAPBDesa({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Tahun + {item.tahun} + + + Pembiayaan + + {formatRupiah( + item.pembiayaan.reduce( + (sum, val) => sum + Number(val.value), + 0 + ) + )} + + + + Belanja + + {formatRupiah( + item.belanja.reduce( + (sum, val) => sum + Number(val.value), + 0 + ) + )} + + + + Pendapatan + + {formatRupiah( + item.pendapatan.reduce( + (sum, val) => sum + Number(val.value), + 0 + ) + )} + + + + + + )) + ) : ( +
+ + Tidak ada data APB Desa yang cocok + +
+ )} +
+
+
{ + return ( + formData.name?.trim() !== '' && + formData.value !== '' && + Number(formData.value) > 0 + ); + }; + // format angka ke rupiah const formatRupiah = (value: number | string) => { const number = @@ -59,6 +74,10 @@ function EditBelanja() { name: data.name || '', value: String(data.value || ''), }); + setOriginalData({ + name: data.name || '', + value: String(data.value || ''), + }); } } catch (error) { console.error("Error loading belanja:", error); @@ -71,6 +90,7 @@ function EditBelanja() { const handleSubmit = async () => { try { + setIsSubmitting(true); belanjaState.update.form = { ...belanjaState.update.form, name: formData.name, @@ -83,23 +103,31 @@ function EditBelanja() { } catch (error) { console.error("Error updating jenis belanja:", error); toast.error("Terjadi kesalahan saat memperbarui jenis belanja"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info("Form dikembalikan ke data awal"); + }; + return ( - + {/* Header */} - - - + Edit Jenis Belanja @@ -138,17 +166,31 @@ function EditBelanja() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx index 7cc80cf9..d5fede9b 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx @@ -6,21 +6,32 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateBelanja() { const belanjaState = useProxy(PendapatanAsliDesa.belanja); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + belanjaState.create.form.name?.trim() !== '' && + belanjaState.create.form.value !== null && + belanjaState.create.form.value > 0 + ); + }; const formatRupiah = (value: number | string) => { const number = @@ -48,25 +59,31 @@ function CreateBelanja() { return toast.warn('Lengkapi semua field terlebih dahulu'); } - await belanjaState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); + try { + setIsSubmitting(true); + await belanjaState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); + } catch (error) { + console.error('Error creating belanja:', error); + toast.error('Gagal menambahkan jenis belanja'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header dengan back button */} - - - + Tambah Jenis Belanja @@ -85,7 +102,7 @@ function CreateBelanja() { Nama Jenis Belanja} placeholder="Masukkan nama jenis belanja" - defaultValue={belanjaState.create.form.name} + value={belanjaState.create.form.name} onChange={(e) => (belanjaState.create.form.name = e.target.value)} required /> @@ -94,7 +111,7 @@ function CreateBelanja() { type="text" label={Nilai} placeholder="Masukkan nilai belanja" - defaultValue={formatRupiah(belanjaState.create.form.value)} + value={formatRupiah(belanjaState.create.form.value)} onChange={(e) => { const raw = e.currentTarget.value; belanjaState.create.form.value = unformatRupiah(raw); @@ -103,17 +120,31 @@ function CreateBelanja() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx index 467c8142..dc185abc 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx @@ -17,10 +17,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -32,7 +31,7 @@ import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; function Belanja() { const [search, setSearch] = useState(""); return ( - + setSearch(e.currentTarget.value)} /> - + ); } @@ -50,6 +49,7 @@ function ListBelanja({ search }: { search: string }) { const router = useRouter(); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -73,51 +73,73 @@ function ListBelanja({ search }: { search: string }) { belanjaState.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - load(page, 10, search); + load(page, 10, debouncedSearch); } }; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - + + {/* Desktop Table */} - Daftar Belanja - - - + + Daftar Belanja + + - - + +
- Nama - Nilai - Persentase - Aksi + + Nama + + + Nilai + + + Edit + + + Delete + @@ -126,64 +148,66 @@ function ListBelanja({ search }: { search: string }) { {filteredData.map((item) => ( - + {item.name} - {formatRupiah(item.value)} - {totalBelanja > 0 - ? ((item.value / totalBelanja) * 100).toFixed(0) + '%' - : '0%'} + {formatRupiah(item.value)} + + + + {totalBelanja > 0 + ? ((item.value / totalBelanja) * 100).toFixed(0) + '%' + : '0%'} + - - - - - - + + ))} - Total + Total - {formatRupiah(totalBelanja)} + {formatRupiah(totalBelanja)} ) : ( -
- Tidak ada data belanja yang cocok +
+ + Tidak ada data belanja yang cocok +
@@ -193,21 +217,107 @@ function ListBelanja({ search }: { search: string }) { + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama + + + {item.name} + + + + + Nilai + + + {formatRupiah(item.value)} + + + + + Persentase + + + {totalBelanja > 0 + ? ((item.value / totalBelanja) * 100).toFixed(0) + '%' + : '0%'} + + + + + + + + + )) + ) : ( + +
+ + Tidak ada data belanja yang cocok + +
+
+ )} + + {filteredData.length > 0 && ( + + + + Total + + + {formatRupiah(totalBelanja)} + + + + )} +
+ {/* Pagination */} -
- { - load(newPage, 10, search); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
+ {(totalPages > 1 || page > 1) && ( +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + color="blue" + radius="md" + /> +
+ )} {/* Modal Konfirmasi Hapus */} - + ); } -export default Belanja; +export default Belanja; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx index 3a414c77..34fe9efa 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/layout.tsx @@ -1,7 +1,30 @@ +'use client' import React from 'react'; import LayoutTabs from './_lib/layoutTabs'; +import { usePathname } from 'next/navigation'; +import { Box } from '@mantine/core'; function Layout({ children }: { children: React.ReactNode }) { +const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + + return ( {children} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx index 25df9c5b..378a34fd 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,12 +22,27 @@ function EditPembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', value: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + value: '', + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.value !== '' && + Number(formData.value) > 0 + ); + }; + const formatRupiah = (value: number | string) => { const number = typeof value === 'number' @@ -56,6 +71,10 @@ function EditPembiayaan() { name: data.name || '', value: String(data.value || ''), }); + setOriginalData({ + name: data.name || '', + value: String(data.value || ''), + }); } } catch (error) { console.error('Error loading pembiayaan:', error); @@ -66,8 +85,17 @@ function EditPembiayaan() { loadPembiayaan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); pembiayaanState.update.form = { ...pembiayaanState.update.form, name: formData.name, @@ -80,23 +108,23 @@ function EditPembiayaan() { } catch (error) { console.error('Error updating jenis pembiayaan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Jenis Pembiayaan @@ -135,17 +163,31 @@ function EditPembiayaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx index a432a431..63897a2f 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx @@ -1,26 +1,36 @@ 'use client'; -import React from 'react'; -import { useProxy } from 'valtio/utils'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; -import { useRouter } from 'next/navigation'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, - Title, - TextInput, Text, - Tooltip, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreatePembiayaan() { const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + pembiayaanState.create.form.name?.trim() !== '' && + pembiayaanState.create.form.value !== null && + pembiayaanState.create.form.value > 0 + ); + }; const formatRupiah = (value: number | string) => { const number = @@ -44,29 +54,35 @@ function CreatePembiayaan() { }; const handleSubmit = async () => { - if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { - return toast.warn('Nama dan nilai wajib diisi'); - } + try { + setIsSubmitting(true); + if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { + return toast.warn('Nama dan nilai wajib diisi'); + } - await pembiayaanState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); + await pembiayaanState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); + } catch (error) { + console.error('Error creating pembiayaan:', error); + toast.error('Gagal menambahkan jenis pembiayaan'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + Tambah Jenis Pembiayaan @@ -85,7 +101,7 @@ function CreatePembiayaan() { Nama Jenis Pembiayaan} placeholder="Masukkan nama jenis pembiayaan" - defaultValue={pembiayaanState.create.form.name} + value={pembiayaanState.create.form.name} onChange={(e) => { pembiayaanState.create.form.name = e.currentTarget.value; }} @@ -96,7 +112,7 @@ function CreatePembiayaan() { type="text" label={Nilai} placeholder="Masukkan nilai" - defaultValue={formatRupiah(pembiayaanState.create.form.value)} + value={formatRupiah(pembiayaanState.create.form.value)} onChange={(e) => { const raw = e.currentTarget.value; pembiayaanState.create.form.value = unformatRupiah(raw); @@ -105,17 +121,31 @@ function CreatePembiayaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx index 627f5894..f791e75d 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx @@ -1,9 +1,11 @@ 'use client' +import colors from '@/con/colors'; import { Box, Button, Center, Group, + Pagination, Paper, Skeleton, Stack, @@ -15,18 +17,15 @@ import { TableTr, Text, Title, - Tooltip, - Pagination, } from '@mantine/core'; -import React, { useState } from 'react'; -import HeaderSearch from '../../../_com/header'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; -import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; -import { useProxy } from 'valtio/utils'; import { useRouter } from 'next/navigation'; -import { useShallowEffect } from '@mantine/hooks'; -import colors from '@/con/colors'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; function Pembiayaan() { const [search, setSearch] = useState(""); @@ -49,6 +48,7 @@ function ListPembiayaan({ search }: { search: string }) { const router = useRouter(); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -72,17 +72,17 @@ function ListPembiayaan({ search }: { search: string }) { pembiayaanState.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - load(page, 10, search); + load(page, 10, debouncedSearch); } }; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( - + ); @@ -91,32 +91,49 @@ function ListPembiayaan({ search }: { search: string }) { const filteredData = data || []; return ( - + - Daftar Pembiayaan - - - + Daftar Pembiayaan + - -
- - - Nama - Nilai - Persentase - Aksi + {/* Desktop Table */} + +
+ + + + Nama + + + Nilai + + + Edit + + + Delete + @@ -125,15 +142,19 @@ function ListPembiayaan({ search }: { search: string }) { {filteredData.map((item) => ( - + {item.name} - {formatRupiah(item.value)} - {totalPembiayaan > 0 - ? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%' - : '0%'} + {formatRupiah(item.value)} + + + + {totalPembiayaan > 0 + ? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%' + : '0%'} + @@ -166,16 +187,20 @@ function ListPembiayaan({ search }: { search: string }) { {/* Total Row */} - Total + Total + + + {formatRupiah(totalPembiayaan)} - {formatRupiah(totalPembiayaan)} ) : (
- Tidak ada data pembiayaan yang cocok + + Tidak ada data pembiayaan yang cocok +
@@ -183,6 +208,79 @@ function ListPembiayaan({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + + {item.name} + + + + Nilai + + {formatRupiah(item.value)} + + + + Persentase + + {totalPembiayaan > 0 + ? ((item.value / totalPembiayaan) * 100).toFixed(0) + '%' + : '0%'} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data pembiayaan yang cocok + +
+ )} + + {filteredData.length > 0 && ( + + + Total + {formatRupiah(totalPembiayaan)} + + + )} +
{/* Pagination */} @@ -208,8 +306,8 @@ function ListPembiayaan({ search }: { search: string }) { onConfirm={handleDelete} text="Apakah anda yakin ingin menghapus pembiayaan ini?" /> -
+
); } -export default Pembiayaan; +export default Pembiayaan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx index cf8e65f0..9bddfead 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -22,12 +22,27 @@ function EditPendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + name: "", + value: "", + }); const [formData, setFormData] = useState({ name: '', value: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.value !== '' && + Number(formData.value) > 0 + ); + }; + // helper format const formatRupiah = (value: number | string) => { const number = typeof value === 'number' @@ -56,6 +71,10 @@ function EditPendapatan() { name: data.name ?? '', value: data.value?.toString() ?? '', }); + setOriginalData({ + name: data.name ?? '', + value: data.value?.toString() ?? '', + }); } } catch (error) { console.error('Error loading pendapatan:', error); @@ -73,8 +92,18 @@ function EditPendapatan() { })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + value: originalData.value, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); pendapatanState.update.form = { ...pendapatanState.update.form, name: formData.name, @@ -87,23 +116,24 @@ function EditPendapatan() { } catch (error) { console.error('Error updating jenis pendapatan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan'); + } finally { + setIsSubmitting(false); } + }; return ( - + {/* Header with Back Button */} - - - + Edit Jenis Pendapatan @@ -140,17 +170,31 @@ function EditPendapatan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx index 663ae35b..ca32a516 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx @@ -5,19 +5,31 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreatePendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + pendapatanState.create.form.name?.trim() !== '' && + pendapatanState.create.form.value !== null && + pendapatanState.create.form.value > 0 + ); + }; const formatRupiah = (value: number | string) => { const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); @@ -40,25 +52,31 @@ function CreatePendapatan() { }; const handleSubmit = async () => { - await pendapatanState.create.submit(); - resetForm(); - router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); + try { + setIsSubmitting(true); + await pendapatanState.create.submit(); + resetForm(); + router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); + } catch (error) { + console.error('Error creating pendapatan:', error); + toast.error('Gagal menambahkan jenis pendapatan'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header dengan tombol back + judul */} - - - + Tambah Jenis Pendapatan @@ -75,7 +93,7 @@ function CreatePendapatan() { > { pendapatanState.create.form.name = val.target.value; }} @@ -86,7 +104,7 @@ function CreatePendapatan() { { const raw = val.currentTarget.value; const cleanValue = unformatRupiah(raw); @@ -98,17 +116,31 @@ function CreatePendapatan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx index 47ba7b86..92dd7e1d 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx @@ -17,10 +17,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -50,6 +49,7 @@ function ListPendapatan({ search }: { search: string }) { const router = useRouter(); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -71,19 +71,19 @@ function ListPendapatan({ search }: { search: string }) { pendapatanState.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - load(page, 10, search); + load(page, 10, debouncedSearch); } }; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); @@ -92,32 +92,48 @@ function ListPendapatan({ search }: { search: string }) { const totalValue = filteredData.reduce((total, item) => total + item.value, 0); return ( - - + + - Daftar Pendapatan - - - + + Daftar Pendapatan + + - - + {/* Desktop Table */} + +
- Nama - Nilai - Edit - Delete + + Nama + + + Nilai + + + Edit + + + Delete + @@ -126,11 +142,13 @@ function ListPendapatan({ search }: { search: string }) { {filteredData.map((item) => ( - + {item.name} - {formatRupiah(item.value)} + + {formatRupiah(item.value)} + @@ -152,9 +171,10 @@ function ListPendapatan({ search }: { search: string }) { setSelectedId(item.id); setModalHapus(true); }} + fz="sm" + px="xs" > - - Hapus + @@ -162,19 +182,21 @@ function ListPendapatan({ search }: { search: string }) { {/* Row total */} - - Total + + Total - {formatRupiah(totalValue)} + {formatRupiah(totalValue)} ) : ( -
- Tidak ada data pendapatan yang cocok +
+ + Tidak ada data pendapatan yang cocok +
@@ -182,23 +204,82 @@ function ListPendapatan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + {item.name} + + + Nilai + {formatRupiah(item.value)} + + + + + + + + )) + ) : ( +
+ + Tidak ada data pendapatan yang cocok + +
+ )} + + {filteredData.length > 0 && ( + + + Total + {formatRupiah(totalValue)} + + + )} +
- {/* Pagination */} -
- { - load(newPage, 10, search); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
+
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + color="blue" + radius="md" + size="sm" + /> +
{/* Modal Konfirmasi Hapus */} + }, + { + label: "Posisi Organisasi", + value: "posisiorganisasi", + href: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi", + icon: + }, + { + label: "Struktur Organisasi", + value: "strukturorganisasi", + href: "/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/struktur-organisasi", + icon: + } + ]; + + const currentTab = tabs.find((tab) => tab.href === pathname); + const [activeTab, setActiveTab] = useState( + currentTab?.value || tabs[0].value + ); + + const handleTabChange = (value: string | null) => { + const tab = tabs.find((t) => t.value === value); + if (tab) { + router.push(tab.href); + } + setActiveTab(value); + }; + + useEffect(() => { + const match = tabs.find((tab) => tab.href === pathname); + if (match) { + setActiveTab(match.value); + } + }, [pathname]); + + return ( + + + Struktur Organisasi & SK Pengurus BUMDes + + + + {/* ✅ Scroll horizontal biar rapi kalau label panjang */} + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + {tabs.map((tab, i) => ( + + {children} + + ))} + + + ); +} + +export default LayoutTabs; diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/layout.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/layout.tsx new file mode 100644 index 00000000..cfb11bcd --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/layout.tsx @@ -0,0 +1,33 @@ +'use client' + +import { usePathname } from "next/navigation"; +import LayoutTabs from "./_lib/layoutTabs" +import { Box } from "@mantine/core"; + + +export default function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx similarity index 70% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx index c1a8eef5..aebb8c18 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx @@ -5,17 +5,18 @@ import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Select, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -28,7 +29,7 @@ export default function EditPegawaiBumDes() { const router = useRouter(); const { id } = useParams<{ id: string }>(); const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ namaLengkap: '', gelarAkademik: '', @@ -40,9 +41,29 @@ export default function EditPegawaiBumDes() { posisiId: '', isActive: true, }); + const [originalData, setOriginalData] = useState({ + namaLengkap: '', + gelarAkademik: '', + imageId: '', + tanggalMasuk: '', + email: '', + telepon: '', + alamat: '', + posisiId: '', + isActive: true, + imageUrl: '' + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + // Check if form is valid + const isFormValid = () => { + return ( + formData.namaLengkap?.trim() !== '' && + formData.posisiId?.trim() !== '' + ); + }; + // Format date for const formatDateForInput = (dateString: string) => { if (!dateString) return ''; @@ -68,6 +89,18 @@ export default function EditPegawaiBumDes() { posisiId: data.posisiId || '', isActive: data.isActive ?? true, }); + setOriginalData({ + 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 ?? true, + imageUrl: data.image?.link || '', + }); setPreviewImage(data.image?.link || null); } @@ -80,8 +113,26 @@ export default function EditPegawaiBumDes() { loadPegawai(); }, [id]); + 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"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); if (!formData.namaLengkap.trim()) { return toast.error('Nama lengkap tidak boleh kosong'); } @@ -100,22 +151,22 @@ export default function EditPegawaiBumDes() { if (id && !stateOrganisasi.edit.id) stateOrganisasi.edit.id = id; const success = await stateOrganisasi.edit.submit(); - if (success) router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai'); + if (success) router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai'); } catch (error) { console.error('Error updating pegawai:', error); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - - Edit Data Pegawai PPID + + Edit Data Pegawai BumDes Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -247,19 +316,33 @@ export default function EditPegawaiBumDes() {
{/* Submit Button */} - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/page.tsx similarity index 80% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx rename to src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/page.tsx index 7a0626e4..ab0c59a7 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/page.tsx @@ -1,9 +1,10 @@ 'use client' import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import { ModalKonfirmasiNonAktif } from '@/app/admin/(dashboard)/_com/modalNonaktif'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -28,7 +29,7 @@ function DetailPegawai() { statePegawai.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); + router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai"); } }; @@ -37,7 +38,7 @@ function DetailPegawai() { statePegawai.nonActive.byId(selectedId); setModalNonActive(false); setSelectedId(null); - router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); + router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai"); } }; @@ -52,7 +53,7 @@ function DetailPegawai() { const data = statePegawai.findUnique.data; return ( - + - - - + - - - + @@ -191,7 +188,7 @@ function DetailPegawai() { /> {/* Modal NonActive */} - setModalNonActive(false)} onConfirm={handleNonActive} diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx similarity index 73% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx rename to src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx index dbf697ee..4999449a 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx @@ -4,7 +4,7 @@ import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; -import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { ActionIcon, Box, Button, Group, Image, Loader, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -20,6 +20,17 @@ function CreatePegawaiBumDes() { stateStrukturBumDes.posisiOrganisasi.findManyAll.load(); resetForm(); }, []); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateOrganisasi.create.form.namaLengkap?.trim() !== '' && + stateOrganisasi.create.form.posisiId?.trim() !== '' && + file !== null + ); + }; const resetForm = () => { stateOrganisasi.create.form = { @@ -33,6 +44,8 @@ function CreatePegawaiBumDes() { posisiId: "", isActive: true, }; + setPreviewImage(null); + setFile(null); }; const handleSubmit = async () => { @@ -41,15 +54,18 @@ function CreatePegawaiBumDes() { } try { + setIsSubmitting(true); // Upload gambar dulu + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } const res = await ApiFetch.api.fileStorage.create.post({ - file: previewImage.file, - name: previewImage.file.name, + file, + name: file.name, }); - const uploaded = res.data?.data; if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); } // Set status aktif secara otomatis @@ -65,23 +81,23 @@ function CreatePegawaiBumDes() { // Reset form dan redirect resetForm(); toast.success("Data pegawai berhasil ditambahkan"); - router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai"); + router.push("/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai"); } catch (error) { console.error("Error creating pegawai:", error); toast.error("Terjadi kesalahan saat menambahkan pegawai"); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + - Tambah Pegawai BUMDesa + Tambah Pegawai BUMDes @@ -98,7 +114,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)} required /> @@ -107,7 +123,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)} /> @@ -165,7 +181,7 @@ function CreatePegawaiBumDes() { {previewImage && ( - + Preview Gambar @@ -182,6 +198,24 @@ function CreatePegawaiBumDes() { }} loading='lazy' /> + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -190,7 +224,7 @@ function CreatePegawaiBumDes() { label="Tanggal Masuk" type="date" placeholder="Contoh: 2022-01-01" - defaultValue={stateOrganisasi.create.form.tanggalMasuk} + value={stateOrganisasi.create.form.tanggalMasuk} onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)} /> @@ -200,7 +234,7 @@ function CreatePegawaiBumDes() { label="Email" type="email" placeholder="Contoh: email@example.com" - defaultValue={stateOrganisasi.create.form.email} + value={stateOrganisasi.create.form.email} onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)} /> @@ -209,7 +243,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.telepon = e.currentTarget.value)} /> @@ -218,7 +252,7 @@ function CreatePegawaiBumDes() { (stateOrganisasi.create.form.alamat = e.currentTarget.value)} /> @@ -244,17 +278,31 @@ function CreatePegawaiBumDes() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/page.tsx new file mode 100644 index 00000000..1e34d32e --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/page.tsx @@ -0,0 +1,232 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title } from '@mantine/core'; +import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; +import { useDebouncedValue } from '@mantine/hooks'; + +function PegawaiBumDes() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListPegawaiBumdes({ search }: { search: string }) { + const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); + + const { + data, + page, + totalPages, + loading, + load, + } = stateOrganisasi.findMany; + + useEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + const filteredData = data || []; + + // Handle loading state + if (loading || !data) { + return ( + + + + ); + } + + if (data.length === 0) { + return ( + + + + Daftar Pegawai BUMDes + Daftar Pegawai BUMDes + + +
+ + Tidak ada data pegawai yang ditemukan + +
+
+
+ ); + } + + const sortedData = [...filteredData].sort((a, b) => { + if (a.isActive === b.isActive) { + return a.namaLengkap.localeCompare(b.namaLengkap); + } + return Number(b.isActive) - Number(a.isActive); + }); + + return ( + + + + Daftar Pegawai BUMDes + Daftar Pegawai BUMDes + + + + {/* Desktop: Table */} + + + + + Nama Lengkap + Posisi + Status + Aksi + + + + {sortedData.map((item) => ( + + + + {item.namaLengkap} + + + + + {item.posisi?.nama || 'Belum diatur'} + + + + + {item.isActive ? "Aktif" : "Tidak Aktif"} + + + + + + + ))} + +
+
+ + {/* Mobile: Card List */} + + {sortedData.map((item) => ( + + + + Nama Lengkap + + {item.namaLengkap} + + + + Posisi + + {item.posisi?.nama || 'Belum diatur'} + + + + Status + + {item.isActive ? ( + + + + + Aktif + + ) : ( + + + + + Tidak Aktif + + )} + + + + + + + + ))} + + +
+ { + load(newPage, 10); + window.scrollTo(0, 0); + }} + total={totalPages} + withEdges + withControls + radius="md" + /> +
+
+
+ ); +} + +export default PegawaiBumDes; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx similarity index 64% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx rename to src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx index 0d8f7ed7..32496484 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx @@ -5,7 +5,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -17,6 +17,7 @@ function EditPosisiOrganisasiBumDes() { const params = useParams(); const id = params?.id as string; const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', @@ -24,6 +25,29 @@ function EditPosisiOrganisasiBumDes() { hierarki: 0, }); + const [originalData, setOriginalData] = useState({ + nama: '', + deskripsi: '', + hierarki: 0, + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.hierarki !== null && + formData.hierarki >= 0 + ); + }; + // Fungsi generik untuk update formData const handleChange = (field: keyof typeof formData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -42,6 +66,11 @@ function EditPosisiOrganisasiBumDes() { deskripsi: data.deskripsi || '', hierarki: data.hierarki || 0, }); + setOriginalData({ + nama: data.nama || '', + deskripsi: data.deskripsi || '', + hierarki: data.hierarki || 0, + }); } } catch (err) { console.error('Error loading posisi organisasi:', err); @@ -52,6 +81,15 @@ function EditPosisiOrganisasiBumDes() { loadPosisiOrganisasi(); }, [id]); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + hierarki: originalData.hierarki, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { if (!formData.nama.trim()) { toast.error('Nama posisi organisasi tidak boleh kosong'); @@ -59,6 +97,7 @@ function EditPosisiOrganisasiBumDes() { } try { + setIsSubmitting(true); // Update global state hanya saat submit stateOrganisasi.edit.form = { nama: formData.nama.trim(), @@ -73,22 +112,22 @@ function EditPosisiOrganisasiBumDes() { const success = await stateOrganisasi.edit.update(); if (success) { - router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); + router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi'); } } catch (err) { console.error('Error updating posisi organisasi:', err); // toast error biasanya sudah ada di update + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Posisi Organisasi BUMDes @@ -134,19 +173,33 @@ function EditPosisiOrganisasiBumDes() { required /> - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx similarity index 59% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx rename to src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx index 2045244c..32b88dd3 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx @@ -3,59 +3,71 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreatePosisiOrganisasiBumDes() { const router = useRouter(); const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); - + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateOrganisasi.create.form.nama?.trim() !== '' && + !isHtmlEmpty(stateOrganisasi.create.form.deskripsi) && + stateOrganisasi.create.form.hierarki !== null && + stateOrganisasi.create.form.hierarki >= 0 + ); + }; + useEffect(() => { stateOrganisasi.findMany.load(); - // Initialize form with default values + }, []); + + const resetForm = () => { stateOrganisasi.create.form = { nama: "", deskripsi: "", hierarki: 0, }; - - return () => { - // Clean up form on unmount - stateOrganisasi.create.form = { - nama: "", - deskripsi: "", - hierarki: 0, - }; - }; - }, []); + } const handleSubmit = async () => { + setIsSubmitting(true); try { if (!stateOrganisasi.create.form.nama.trim()) { return toast.error('Nama posisi tidak boleh kosong'); } - + await stateOrganisasi.create.submit(); toast.success('Posisi organisasi berhasil ditambahkan'); - router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi'); + router.push('/admin/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi'); } catch (error) { toast.error('Gagal menambahkan posisi organisasi'); console.error('Error:', error); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Tambah Posisi Organisasi BUMDes @@ -73,11 +85,11 @@ function CreatePosisiOrganisasiBumDes() { (stateOrganisasi.create.form.nama = e.target.value)} required /> - + Deskripsi @@ -89,13 +101,13 @@ function CreatePosisiOrganisasiBumDes() { }} /> - + { const value = parseInt(e.target.value, 10); stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value; @@ -103,19 +115,33 @@ function CreatePosisiOrganisasiBumDes() { required /> - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/page.tsx new file mode 100644 index 00000000..f6664164 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/page.tsx @@ -0,0 +1,313 @@ +'use client' +import colors from '@/con/colors'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; + +function PosisiOrganisasiBumDes() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListPosisiOrganisasiBumDes({ search }: { search: string }) { + const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); + + const { + data, + page, + totalPages, + loading, + load, + } = stateOrganisasi.findMany; + + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + const handleHapus = async () => { + if (selectedId) { + await stateOrganisasi.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + } + }; + + const filteredData = data || []; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + + + + Daftar Posisi Organisasi BumDes + + + + + {/* Desktop Table */} + + + + + + + Nama Posisi + + + + + Deskripsi + + + + + Hierarki + + + + + Edit + + + + + Hapus + + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.nama} + + + + + + + {item.hierarki || '-'} + + + + + + + + + )) + ) : ( + + +
+ + Tidak ada data posisi organisasi yang cocok + +
+
+
+ )} +
+
+
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Posisi + + + {item.nama} + + + + + Deskripsi + + + + + + Hierarki + + + {item.hierarki || '-'} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data posisi organisasi yang cocok + +
+ )} +
+
+
+ +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+ + {/* Modal Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?" + /> +
+ ); +} + +export default PosisiOrganisasiBumDes; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/struktur-organisasi/page.tsx similarity index 88% rename from src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx rename to src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/struktur-organisasi/page.tsx index be24d78b..89ab5a7e 100644 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/struktur-organisasi/page.tsx @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' -import { Box, Center, Image, Loader, Paper, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Center, Image, Loader, Paper, Stack, Text } from '@mantine/core'; import { IconUsers } from '@tabler/icons-react'; import { OrganizationChart } from 'primereact/organizationchart'; import { useEffect } from 'react'; @@ -110,20 +110,18 @@ function nodeTemplate(node: any) { return ( - - {name} - + {name} {name} {status} diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx index 016cb96b..b5f5f37b 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx @@ -6,15 +6,15 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState, useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; @@ -29,12 +29,28 @@ export default function EditDemografiPekerjaan() { const router = useRouter(); const { id } = useParams() as { id: string }; const stateDemografi = useProxy(demografiPekerjaan); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ pekerjaan: '', lakiLaki: 0, perempuan: 0, }); + const [originalData, setOriginalData] = useState({ + pekerjaan: '', + lakiLaki: 0, + perempuan: 0, + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.pekerjaan?.trim() !== '' && + formData.lakiLaki !== null && + formData.lakiLaki >= 0 && + formData.perempuan !== null && + formData.perempuan >= 0 + ); + }; // ✅ Load data hanya sekali di awal (tidak reset form) useEffect(() => { @@ -42,6 +58,7 @@ export default function EditDemografiPekerjaan() { const loadData = async () => { try { + setIsSubmitting(true); stateDemografi.update.id = id; await stateDemografi.findUnique.load(id); @@ -52,10 +69,17 @@ export default function EditDemografiPekerjaan() { lakiLaki: Number(data.lakiLaki ?? 0), perempuan: Number(data.perempuan ?? 0), }); + setOriginalData({ + pekerjaan: data.pekerjaan ?? '', + lakiLaki: Number(data.lakiLaki ?? 0), + perempuan: Number(data.perempuan ?? 0), + }); } } catch (error) { console.error('Error loading data:', error); toast.error('Gagal memuat data'); + } finally { + setIsSubmitting(false); } }; @@ -76,9 +100,19 @@ export default function EditDemografiPekerjaan() { [] ); + const handleResetForm = () => { + setFormData({ + pekerjaan: originalData.pekerjaan, + lakiLaki: Number(originalData.lakiLaki), + perempuan: Number(originalData.perempuan), + }); + toast.info("Form dikembalikan ke data awal"); + }; + // ✅ Submit hanya update global state sekali const handleSubmit = async () => { try { + setIsSubmitting(true); stateDemografi.update.id = id; stateDemografi.update.form = { ...formData }; @@ -89,23 +123,23 @@ export default function EditDemografiPekerjaan() { } catch (error) { console.error('Error updating data:', error); toast.error('Gagal memperbarui data'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Demografi Pekerjaan @@ -148,17 +182,31 @@ export default function EditDemografiPekerjaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx index d590b347..ed5c84f1 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx @@ -7,22 +7,35 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; +import { toast } from 'react-toastify'; function CreateDemografiPekerjaan() { const stateDemografi = useProxy(demografiPekerjaan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateDemografi.create.form.pekerjaan?.trim() !== '' && + stateDemografi.create.form.lakiLaki !== null && + stateDemografi.create.form.lakiLaki >= 0 && + stateDemografi.create.form.perempuan !== null && + stateDemografi.create.form.perempuan >= 0 + ); + }; const resetForm = () => { stateDemografi.create.form = { @@ -33,32 +46,37 @@ function CreateDemografiPekerjaan() { }; const handleSubmit = async () => { - const id = await stateDemografi.create.create(); - if (id) { - const idStr = String(id); - await stateDemografi.findUnique.load(idStr); - if (stateDemografi.findUnique.data) { - setChartData([stateDemografi.findUnique.data]); + try { + const id = await stateDemografi.create.create(); + if (id) { + const idStr = String(id); + await stateDemografi.findUnique.load(idStr); + if (stateDemografi.findUnique.data) { + setChartData([stateDemografi.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/demografi-pekerjaan'); + } catch (error) { + console.error('Error creating demografi pekerjaan:', error); + toast.error('Terjadi kesalahan saat menambah demografi pekerjaan'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/demografi-pekerjaan'); }; return ( - + {/* Header */} - - - + Tambah Demografi Pekerjaan @@ -77,7 +95,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.pekerjaan = val.currentTarget.value; @@ -87,7 +105,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value); @@ -97,7 +115,7 @@ function CreateDemografiPekerjaan() { { stateDemografi.create.form.perempuan = Number(val.currentTarget.value); @@ -106,17 +124,31 @@ function CreateDemografiPekerjaan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx index 0b2d4a92..108ff26c 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; import { BarChart } from '@mantine/charts'; @@ -5,7 +6,9 @@ import { Box, Button, Center, + Flex, Group, + Pagination, Paper, Skeleton, Stack, @@ -16,12 +19,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, - Pagination, - Flex, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -59,6 +59,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) { const [chartData, setChartData] = useState([]); const [mounted, setMounted] = useState(false); const [modalHapus, setModalHapus] = useState(false); + const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay + const [selectedId, setSelectedId] = useState(null); const { @@ -79,13 +81,13 @@ function ListDemografiPekerjaan({ search }: { search: string }) { useShallowEffect(() => { setMounted(true); - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); useEffect(() => { if (data) { setChartData( - data.map((item) => ({ + data.map((item: any) => ({ id: item.id, pekerjaan: item.pekerjaan, lakiLaki: Number(item.lakiLaki), @@ -106,40 +108,52 @@ function ListDemografiPekerjaan({ search }: { search: string }) { } return ( - - - - List Demografi Pekerjaan - - - + + + + + List Demografi Pekerjaan + + - - + {/* Desktop Table */} + +
- Pekerjaan - Laki - Laki - Perempuan - Edit - Hapus + Pekerjaan + Laki - Laki + Perempuan + Edit + Hapus {filteredData.length > 0 ? ( filteredData.map((item) => ( - {item.pekerjaan} - {item.lakiLaki} - {item.perempuan} + {item.pekerjaan} + {item.lakiLaki} + {item.perempuan} @@ -160,17 +177,22 @@ function ListDemografiPekerjaan({ search }: { search: string }) { setSelectedId(item.id); setModalHapus(true); }} + fz="sm" + px="xs" + py="xs" > - + )) ) : ( - +
- Tidak ada data demografi pekerjaan yang cocok + + Tidak ada data demografi pekerjaan yang cocok +
@@ -178,6 +200,78 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
+ + {/* Mobile Card */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Pekerjaan + + + {item.pekerjaan} + + + + + Laki - Laki + + + {item.lakiLaki} + + + + + Perempuan + + + {item.perempuan} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data demografi pekerjaan yang cocok + +
+ )} +
+
{/* Pagination */} @@ -197,10 +291,13 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
{/* Chart */} - - - - + <Box mt={{ base: 'lg', md: 'xl' }}> + <Paper bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} radius="md" withBorder> + <Stack gap="xs"> + <Title + order={4} + lh={{ base: 1.2, md: 1.15 }} + > Grafik Demografi Pekerjaan {mounted && chartData.length > 0 ? ( @@ -215,17 +312,23 @@ function ListDemografiPekerjaan({ search }: { search: string }) { ]} /> ) : ( - Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik + )} - - - - - Laki - Laki + + + + + + Laki - Laki + - - - Perempuan + + + + Perempuan + @@ -244,4 +347,4 @@ function ListDemografiPekerjaan({ search }: { search: string }) { ); } -export default DemografiPekerjaan; +export default DemografiPekerjaan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx index b7a217a4..4637dfeb 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/[id]/page.tsx @@ -6,24 +6,24 @@ import colors from '@/con/colors'; import { Box, Button, + Group, + Loader, Paper, Stack, TextInput, - Title, - Group, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function EditJumlahPendudukMiskin() { const router = useRouter(); const params = useParams() as { id: string }; const stateJPM = useProxy(jumlahPendudukMiskin); - + const [isSubmitting, setIsSubmitting] = useState(false); const id = params.id; // 🔹 State lokal untuk form @@ -32,6 +32,21 @@ function EditJumlahPendudukMiskin() { totalPoorPopulation: 0, }); + const [originalData, setOriginalData] = useState({ + year: 0, + totalPoorPopulation: 0, + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.year !== null && + formData.year > 0 && + formData.totalPoorPopulation !== null && + formData.totalPoorPopulation >= 0 + ); + }; + // 🔹 Load data awal dari backend useEffect(() => { if (!id) return; @@ -45,6 +60,10 @@ function EditJumlahPendudukMiskin() { year: data.year || 0, totalPoorPopulation: data.totalPoorPopulation || 0, }); + setOriginalData({ + year: data.year || 0, + totalPoorPopulation: data.totalPoorPopulation || 0, + }); } } catch (error) { console.error('Gagal memuat data:', error); @@ -63,9 +82,18 @@ function EditJumlahPendudukMiskin() { })); }; + const handleResetForm = () => { + setFormData({ + year: originalData.year, + totalPoorPopulation: originalData.totalPoorPopulation, + }); + toast.info('Form dikembalikan ke data awal'); + }; + // 🔹 Submit form const handleSubmit = async () => { try { + setIsSubmitting(true); stateJPM.update.id = id; // update global state cuma saat submit stateJPM.update.form = { ...formData }; @@ -76,22 +104,22 @@ function EditJumlahPendudukMiskin() { } catch (error) { console.error('Gagal menyimpan data:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Jumlah Penduduk Miskin @@ -127,17 +155,31 @@ function EditJumlahPendudukMiskin() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx index fdebedfd..a911ceef 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/create/page.tsx @@ -1,18 +1,30 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; +import colors from '@/con/colors'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import colors from '@/con/colors'; import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin'; +import { toast } from 'react-toastify'; export default function CreateJumlahPendudukMiskin() { const stateJPM = useProxy(jumlahPendudukMiskin); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateJPM.create.form.year !== null && + stateJPM.create.form.year > 0 && + stateJPM.create.form.totalPoorPopulation !== null && + stateJPM.create.form.totalPoorPopulation >= 0 + ); + }; const resetForm = () => { stateJPM.create.form = { @@ -22,27 +34,33 @@ export default function CreateJumlahPendudukMiskin() { }; const handleSubmit = async () => { - const id = await stateJPM.create.create(); - if (id) { - const idStr = String(id); - await stateJPM.findUnique.load(idStr); - if (stateJPM.findUnique.data) { - setChartData([stateJPM.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stateJPM.create.create(); + if (id) { + const idStr = String(id); + await stateJPM.findUnique.load(idStr); + if (stateJPM.findUnique.data) { + setChartData([stateJPM.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/jumlah-penduduk-miskin'); + } catch (error) { + console.error(error) + toast.error(error instanceof Error ? error.message : "Gagal menambahkan jumlah penduduk miskin") + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/jumlah-penduduk-miskin'); }; return ( - + {/* Header */} - - - + Tambah Jumlah Penduduk Miskin @@ -61,7 +79,7 @@ export default function CreateJumlahPendudukMiskin() { { const value = e.currentTarget.value; @@ -73,7 +91,7 @@ export default function CreateJumlahPendudukMiskin() { { stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value); @@ -82,17 +100,31 @@ export default function CreateJumlahPendudukMiskin() { /> + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx index 57fde078..bd42a5fc 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-miskin/page.tsx @@ -1,4 +1,4 @@ -'use client' +'use client'; import colors from '@/con/colors'; import { Box, @@ -17,22 +17,19 @@ import { TableTr, Text, Title, - Tooltip, } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../_com/header'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; - -// ✅ BarChart Mantine import { BarChart } from '@mantine/charts'; function JumlahPendudukMiskin() { - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(''); return ( (null); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, loading, load, totalPages } = stateJPM.findMany; - // Load data awal useShallowEffect(() => { setMounted(true); - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - // Update chart data useEffect(() => { if (stateJPM.findMany.data) { setChartData( @@ -89,48 +85,80 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { if (loading || !data) { return ( - + ); } return ( - - {/* Tabel */} - - - Daftar Jumlah Penduduk Miskin - - - + + {/* Main Table/Card Section */} + + + + Daftar Jumlah Penduduk Miskin + + - - + {/* Desktop Table */} + +
- Tahun - Jumlah Penduduk Miskin - Edit - Delete + + + Tahun + + + + + Jumlah Penduduk Miskin + + + + + Edit + + + + + Delete + + {filteredData.length > 0 ? ( filteredData.map((item) => ( - {item.year} - {item.totalPoorPopulation} + + + {item.year} + + + + + {item.totalPoorPopulation} + +
+ + {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Tahun + + + {item.year} + + + + + Jumlah Penduduk Miskin + + + {item.totalPoorPopulation} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data yang cocok + +
+ )} +
{/* Pagination */} @@ -188,9 +276,9 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
{/* Bar Chart */} - - - + <Paper bg={colors['white-1']} p={{ base: 'sm', md: 'md' }} mt="lg" withBorder radius="md"> + <Stack gap="xs"> + <Title order={4} lh={1.2} mb="sm"> Grafik Jumlah Penduduk Miskin {mounted && chartData.length > 0 ? ( @@ -201,14 +289,14 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { value: item.totalPoorPopulation, }))} dataKey="name" - series={[ - { name: 'value', color: colors['blue-button'] }, - ]} + series={[{ name: 'value', color: colors['blue-button'] }]} withTooltip valueFormatter={(v) => `${v.toLocaleString()} jiwa`} /> ) : ( - Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik + )} @@ -220,8 +308,8 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) { onConfirm={handleDelete} text="Apakah anda yakin ingin menghapus data ini?" /> -
+
); } -export default JumlahPendudukMiskin; +export default JumlahPendudukMiskin; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx index 8b884c50..495cc876 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/_lib/layoutTabs.tsx @@ -2,18 +2,18 @@ 'use client' import colors from '@/con/colors'; import { + Box, + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; +import { IconSchool, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconUsers, IconSchool } from '@tabler/icons-react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -24,15 +24,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Pengangguran Berdasarkan Usia", value: "pengangguranberdasarkanusia", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia", - icon: , - tooltip: "Data pengangguran menurut kelompok usia", + icon: }, { label: "Pengangguran Berdasarkan Pendidikan", value: "pengangguranberdasarkanpendidikan", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan", - icon: , - tooltip: "Data pengangguran menurut tingkat pendidikan", + icon: }, ]; @@ -64,43 +62,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { radius="lg" keepMounted={false} > - - - {tabs.map((tab, i) => ( - + + + + {tabs.map((tab, i) => ( {tab.label} - - ))} - - + ))} + + +
+ + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( = 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + return ( {children} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx index 7e069310..62e66df3 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/[id]/page.tsx @@ -2,10 +2,11 @@ 'use client'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function EditGrafikBerdasarkanPendidikan() { @@ -13,6 +14,7 @@ function EditGrafikBerdasarkanPendidikan() { const params = useParams() as { id: string }; const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); // state lokal untuk form const [formData, setFormData] = useState({ @@ -23,6 +25,25 @@ function EditGrafikBerdasarkanPendidikan() { S1: '', }); + const [originalData, setOriginalData] = useState({ + SD: '', + SMP: '', + SMA: '', + D3: '', + S1: '', + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.SD?.trim() !== '' && + formData.SMP?.trim() !== '' && + formData.SMA?.trim() !== '' && + formData.D3?.trim() !== '' && + formData.S1?.trim() !== '' + ); + }; + useEffect(() => { if (id) { stategrafik.findUnique.load(id).then(() => { @@ -35,41 +56,64 @@ function EditGrafikBerdasarkanPendidikan() { D3: data.D3 || '', S1: data.S1 || '', }); + setOriginalData({ + SD: data.SD || '', + SMP: data.SMP || '', + SMA: data.SMA || '', + D3: data.D3 || '', + S1: data.S1 || '', + }); } }); } }, [id]); - const handleChange = (field: keyof typeof formData) => - (e: React.ChangeEvent) => { - setFormData((prev) => ({ - ...prev, - [field]: e.currentTarget.value, - })); - }; + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.currentTarget; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + + + const handleResetForm = () => { + setFormData({ + SD: originalData.SD, + SMP: originalData.SMP, + SMA: originalData.SMA, + D3: originalData.D3, + S1: originalData.S1, + }); + toast.info("Form dikembalikan ke data awal"); + }; const handleSubmit = async () => { - stategrafik.update.id = id; - stategrafik.update.form = { ...formData }; // update global state pas submit aja - await stategrafik.update.submit(); - router.push( - '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' - ); + try { + setIsSubmitting(true); + stategrafik.update.id = id; + stategrafik.update.form = { ...formData }; // update global state pas submit aja + await stategrafik.update.submit(); + router.push( + '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' + ); + } catch (error) { + console.error(error); + toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); + } }; return ( - + - - - + Edit Grafik Pengangguran Berdasarkan Pendidikan @@ -85,53 +129,72 @@ function EditGrafikBerdasarkanPendidikan() { > + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx index e57a797a..dfa0f954 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create/page.tsx @@ -1,19 +1,31 @@ 'use client'; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import { useRouter } from 'next/navigation'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; -import { useProxy } from 'valtio/utils'; -import { useState } from 'react'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Loader, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateGrafikBerdasarkanPendidikan() { const router = useRouter(); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stategrafik.create.form.SD?.trim() !== '' && + stategrafik.create.form.SMP?.trim() !== '' && + stategrafik.create.form.SMA?.trim() !== '' && + stategrafik.create.form.D3?.trim() !== '' && + stategrafik.create.form.S1?.trim() !== '' + ); + }; const resetForm = () => { stategrafik.create.form = { @@ -27,28 +39,34 @@ function CreateGrafikBerdasarkanPendidikan() { }; const handleSubmit = async () => { - const id = await stategrafik.create.create(); - if (id) { - const idStr = String(id); - await stategrafik.findUnique.load(idStr); - if (stategrafik.findUnique.data) { - setDonutData([stategrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stategrafik.create.create(); + if (id) { + const idStr = String(id); + await stategrafik.findUnique.load(idStr); + if (stategrafik.findUnique.data) { + setDonutData([stategrafik.findUnique.data]); + } } + resetForm(); + router.push( + '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' + ); + } catch (error) { + console.error(error); + toast.error('Terjadi kesalahan saat menambahkan data grafik'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push( - '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' - ); }; return ( - + - - - + Tambah Data Pengangguran Berdasarkan Pendidikan @@ -67,7 +85,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="SD" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.SD} + value={stategrafik.create.form.SD} onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)} required /> @@ -75,7 +93,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="SMP" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.SMP} + value={stategrafik.create.form.SMP} onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)} required /> @@ -83,7 +101,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="SMA" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.SMA} + value={stategrafik.create.form.SMA} onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)} required /> @@ -91,7 +109,7 @@ function CreateGrafikBerdasarkanPendidikan() { label="D3" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.D3} + value={stategrafik.create.form.D3} onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)} required /> @@ -99,23 +117,37 @@ function CreateGrafikBerdasarkanPendidikan() { label="S1" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.S1} + value={stategrafik.create.form.S1} onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx index 74137342..8220b00d 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/page.tsx @@ -1,11 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; +import { DonutChart } from '@mantine/charts'; import { Box, Button, Center, Flex, + Group, Pagination, Paper, Skeleton, @@ -17,15 +19,13 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { DonutChart } from '@mantine/charts'; import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; @@ -52,6 +52,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const handleDelete = async () => { if (selectedId) { @@ -65,8 +66,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { const { data, page, totalPages, loading, load } = stategrafik.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); useEffect(() => { if (stategrafik.findMany.data) { @@ -104,45 +105,71 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { if (loading || !data) { return ( - + ); } return ( - - {/* Table Data */} - - - List Pengangguran Berdasarkan Pendidikan - - - + + {/* Section: List Table */} + + + + List Pengangguran Berdasarkan Pendidikan + + - - + + + List Pengangguran Berdasarkan Pendidikan + + + + + {/* Desktop Table */} + +
- SD - SMP - SMA - D3 - S1 - Edit - Delete + SD + SMP + SMA + D3 + S1 + Edit + Delete @@ -150,7 +177,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
- + Belum ada data grafik responden
@@ -159,40 +186,56 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { ) : ( filteredData.map((item) => ( - {item.SD} - {item.SMP} - {item.SMA} - {item.D3} - {item.S1} - - - + + {item.SD} + - - - + + {item.SMP} + + + + + {item.SMA} + + + + + {item.D3} + + + + + {item.S1} + + + + + + + )) @@ -200,6 +243,92 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length === 0 ? ( +
+ + Belum ada data grafik responden + +
+ ) : ( + + {filteredData.map((item) => ( + + + + + SD + + + {item.SD} + + + + + SMP + + + {item.SMP} + + + + + SMA + + + {item.SMA} + + + + + D3 + + + {item.D3} + + + + + S1 + + + {item.S1} + + + + + + + + + ))} + + )} +
{/* Pagination */} @@ -218,26 +347,28 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { />
- {/* Donut Chart */} - - - + {/* Section: Donut Chart */} + <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md"> + <Stack gap="md"> + <Title order={4} lh={1.2}> Grafik Pengangguran Berdasarkan Pendidikan - {donutData.length > 0 ? ( - - ) : ( - - Belum ada data untuk ditampilkan dalam grafik - - )} +
+ {donutData.length > 0 ? ( + + ) : ( + + Belum ada data untuk ditampilkan dalam grafik + + )} +
@@ -248,8 +379,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) { onConfirm={handleDelete} text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?" /> - + ); } -export default GrafikBerdasarkanPendidikan; +export default GrafikBerdasarkanPendidikan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx index f5e77711..8c87f29b 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/[id]/page.tsx @@ -5,18 +5,18 @@ import colors from '@/con/colors'; import { Box, Button, + Group, + Loader, Paper, Stack, TextInput, - Title, - Group, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { const router = useRouter(); @@ -26,6 +26,8 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { ); const id = params.id; + const [isSubmitting, setIsSubmitting] = useState(false); + // ✅ state lokal, controlled const [formData, setFormData] = useState({ usia18_25: '', @@ -34,6 +36,23 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { usia46_keatas: '', }); + const [originalData, setOriginalData] = useState({ + usia18_25: '', + usia26_35: '', + usia36_45: '', + usia46_keatas: '', + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.usia18_25?.trim() !== '' && + formData.usia26_35?.trim() !== '' && + formData.usia36_45?.trim() !== '' && + formData.usia46_keatas?.trim() !== '' + ); + }; + // load data dari global state -> masukin ke local state useEffect(() => { if (id) { @@ -46,6 +65,13 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { usia36_45: data.usia36_45 || '', usia46_keatas: data.usia46_keatas || '', }); + setOriginalData({ + usia18_25: data.usia18_25 || '', + usia26_35: data.usia26_35 || '', + usia36_45: data.usia36_45 || '', + usia46_keatas: data.usia46_keatas || '', + }); + } }); } @@ -58,8 +84,19 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { })); }; + const handleResetForm = () => { + setFormData({ + usia18_25: originalData.usia18_25, + usia26_35: originalData.usia26_35, + usia36_45: originalData.usia36_45, + usia46_keatas: originalData.usia46_keatas, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // ✅ baru update global state pas submit stategrafik.update.id = id; stategrafik.update.form = { ...formData }; @@ -73,22 +110,22 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui data grafik'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Grafik Pengangguran Berdasarkan Usia Kerja @@ -137,17 +174,31 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx index 77c362fd..26d51682 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create/page.tsx @@ -2,18 +2,30 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; -import React, { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { const router = useRouter(); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur); const [donutData, setDonutData] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stategrafik.create.form.usia18_25?.trim() !== '' && + stategrafik.create.form.usia26_35?.trim() !== '' && + stategrafik.create.form.usia36_45?.trim() !== '' && + stategrafik.create.form.usia46_keatas?.trim() !== '' + ); + }; const resetForm = () => { stategrafik.create.form = { @@ -26,27 +38,33 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { }; const handleSubmit = async () => { - const id = await stategrafik.create.create(); - if (id) { - const idStr = String(id); - await stategrafik.findUnique.load(idStr); - if (stategrafik.findUnique.data) { - setDonutData([stategrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stategrafik.create.create(); + if (id) { + const idStr = String(id); + await stategrafik.findUnique.load(idStr); + if (stategrafik.findUnique.data) { + setDonutData([stategrafik.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); + } catch (error) { + console.error('Error creating:', error); + toast.error('Terjadi kesalahan saat membuat data'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); }; return ( - + {/* Header */} - - - + Tambah Data Pengangguran Berdasarkan Usia @@ -66,7 +84,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 18 - 25" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia18_25} + value={stategrafik.create.form.usia18_25} onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)} required /> @@ -74,7 +92,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 26 - 35" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia26_35} + value={stategrafik.create.form.usia26_35} onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)} required /> @@ -82,7 +100,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 36 - 45" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia36_45} + value={stategrafik.create.form.usia36_45} onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)} required /> @@ -90,24 +108,38 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { label="Usia 46 +" type="number" placeholder="Masukkan jumlah" - defaultValue={stategrafik.create.form.usia46_keatas} + value={stategrafik.create.form.usia46_keatas} onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)} required /> {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx index 3c0fcfd6..19b7d0e7 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/page.tsx @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -'use client' +'use client'; import colors from '@/con/colors'; import { Box, Button, Center, Flex, + Group, Pagination, Paper, Skeleton, @@ -19,7 +20,7 @@ import { Text, Title, } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -51,6 +52,7 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const handleDelete = async () => { if (selectedId) { @@ -64,8 +66,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri const { data, page, totalPages, loading, load } = stategrafik.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); useEffect(() => { if (stategrafik.findMany.data) { @@ -87,19 +89,21 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri if (loading || !data) { return ( - + ); } return ( - - {/* Table */} - - - - List Pengangguran Berdasarkan Usia Kerja + + {/* Table - Desktop */} + + + + + List Pengangguran Berdasarkan Usia Kerja + - - + + + List Pengangguran Berdasarkan Usia Kerja + + + + + {/* Desktop Table */} + +
- Usia 18-25 - Usia 26-35 - Usia 36-45 - Usia 46+ - Edit - Delete + Usia 18-25 + Usia 26-35 + Usia 36-45 + Usia 46+ + Edit + Delete {filteredData.length > 0 ? ( filteredData.map((item) => ( - {item.usia18_25} - {item.usia26_35} - {item.usia36_45} - {item.usia46_keatas} + + {item.usia18_25} + + + {item.usia26_35} + + + {item.usia36_45} + + + {item.usia46_keatas} +
+ + {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + + {filteredData.map((item) => ( + + + + + Usia 18-25 + + + {item.usia18_25} + + + + + Usia 26-35 + + + {item.usia26_35} + + + + + Usia 36-45 + + + {item.usia36_45} + + + + + Usia 46+ + + + {item.usia46_keatas} + + + + + + + + + ))} + + ) : ( +
+ + Belum ada data grafik responden + +
+ )} +
@@ -189,8 +301,8 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri {/* Donut Chart */} - - + <Stack gap="md"> + <Title order={4} lh={1.2}> Grafik Pengangguran Berdasarkan Usia Kerja {donutData.length > 0 ? ( @@ -205,7 +317,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri />
) : ( - Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik + )}
@@ -221,4 +335,4 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri ); } -export default GrafikBerdasarkanUsiaKerjaYangMenganggur; +export default GrafikBerdasarkanUsiaKerjaYangMenganggur; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx index 22ae288d..5ccf3989 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, @@ -31,6 +32,7 @@ function EditDetailDataPengangguran() { const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); // --- state lokal form const [formData, setFormData] = useState({ @@ -42,6 +44,28 @@ function EditDetailDataPengangguran() { percentageChange: 0, }); + const [originalData, setOriginalData] = useState({ + month: '', + year: new Date().getFullYear(), + educatedUnemployment: 0, + uneducatedUnemployment: 0, + totalUnemployment: 0, + percentageChange: 0, + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.month?.trim() !== '' && + formData.year !== null && + formData.year > 0 && + formData.educatedUnemployment !== null && + formData.educatedUnemployment >= 0 && + formData.uneducatedUnemployment !== null && + formData.uneducatedUnemployment >= 0 + ); + }; + // --- hitung total + persentase perubahan const calculateTotalAndChange = useCallback( async (data: typeof formData) => { @@ -109,6 +133,15 @@ function EditDetailDataPengangguran() { totalUnemployment: data.totalUnemployment, percentageChange: data.percentageChange || 0, }); + + setOriginalData({ + month: data.month, + year: yearValue, + educatedUnemployment: data.educatedUnemployment, + uneducatedUnemployment: data.uneducatedUnemployment, + totalUnemployment: data.totalUnemployment, + percentageChange: data.percentageChange || 0, + }); } catch (err) { console.error('Error loading detail:', err); toast.error('Gagal memuat data detail'); @@ -118,9 +151,22 @@ function EditDetailDataPengangguran() { loadDetail(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + month: originalData.month, + year: originalData.year, + educatedUnemployment: originalData.educatedUnemployment, + uneducatedUnemployment: originalData.uneducatedUnemployment, + totalUnemployment: originalData.totalUnemployment, + percentageChange: originalData.percentageChange, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // --- submit form const handleSubmit = async () => { try { + setIsSubmitting(true); const { total, percentageChange } = await calculateTotalAndChange(formData); stateDetail.update.form = { @@ -137,11 +183,13 @@ function EditDetailDataPengangguran() { } catch (err) { console.error('Error updating:', err); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; return ( - + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx index a4043b22..ab93008d 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -40,7 +40,7 @@ function DetailJumlahPengangguran() { const data = stateDetail.findUnique.data; return ( - + {/* Tombol Kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx index df0cb26a..3cd144f6 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx @@ -7,23 +7,38 @@ import { Box, Button, Group, + Loader, + NumberInput, Paper, + Select, Stack, Text, - NumberInput, - Title, - Select, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJumlahPengangguran() { const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateDetail.create.form.month?.trim() !== '' && + stateDetail.create.form.year !== null && + stateDetail.create.form.year > 0 && + stateDetail.create.form.educatedUnemployment !== null && + stateDetail.create.form.educatedUnemployment >= 0 && + stateDetail.create.form.uneducatedUnemployment !== null && + stateDetail.create.form.uneducatedUnemployment >= 0 + ); + }; const monthOptions = [ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', @@ -73,32 +88,38 @@ function CreateJumlahPengangguran() { }; const handleSubmit = async () => { - await calculateTotalAndChange(); - const id = await stateDetail.create.create(); - if (id) { - await stateDetail.findUnique.load(String(id)); - if (stateDetail.findUnique.data) { - setChartData([stateDetail.findUnique.data]); + try { + setIsSubmitting(true); + await calculateTotalAndChange(); + const id = await stateDetail.create.create(); + if (id) { + await stateDetail.findUnique.load(String(id)); + if (stateDetail.findUnique.data) { + setChartData([stateDetail.findUnique.data]); + } + resetForm(); + router.push('/admin/ekonomi/jumlah-pengangguran'); } - resetForm(); - router.push('/admin/ekonomi/jumlah-pengangguran'); + } catch (error) { + console.error("Error creating jumlah pengangguran:", error); + toast.error("Gagal menambahkan data pengangguran"); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Tambah Data Pengangguran @@ -179,19 +200,33 @@ function CreateJumlahPengangguran() { {/* Action Button */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx index ff3a8209..7406f7b6 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/page.tsx @@ -1,17 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import colors from '@/con/colors'; +import { BarChart } from '@mantine/charts'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, - Text, Title, Tooltip + Text, Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { BarChart } from '@mantine/charts'; import HeaderSearch from '../../_com/header'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; @@ -20,7 +20,7 @@ function DetailDataPengangguran() { const [search, setSearch] = useState(""); return ( - + setSearch(e.currentTarget.value)} /> - + ); } @@ -38,6 +38,7 @@ function ListDetailDataPengangguran({ search }: { search: string }) { const [mounted, setMounted] = useState(false); const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -49,8 +50,8 @@ function ListDetailDataPengangguran({ search }: { search: string }) { useShallowEffect(() => { setMounted(true); - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); useEffect(() => { if (data) { @@ -68,52 +69,74 @@ function ListDetailDataPengangguran({ search }: { search: string }) { } }, [data]); - const filteredData = data || [] + const filteredData = data || []; // Loading state if (loading || !data) { return ( - + ); } return ( - - {/* Table Section */} - - - Daftar Detail Data Pengangguran - - - + + {/* Table / Card Section */} + + + + Daftar Detail Data Pengangguran + + - - + {/* Desktop Table */} + +
- Bulan - Terdidik - Tidak Terdidik - Aksi + Bulan + Terdidik + Tidak Terdidik + Aksi {filteredData.length > 0 ? ( filteredData.map((item) => ( - {item.month} {item.year} - {item.educatedUnemployment} - {item.uneducatedUnemployment} + + + {item.month} {item.year} + + + + + {item.educatedUnemployment} + + + + + {item.uneducatedUnemployment} + + @@ -130,7 +155,9 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
- Tidak ada data yang cocok + + Tidak ada data yang cocok +
@@ -138,25 +165,83 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Bulan + + + {item.month} {item.year} + + + + + Terdidik + + + {item.educatedUnemployment} + + + + + Tidak Terdidik + + + {item.uneducatedUnemployment} + + + + + + )) + ) : ( +
+ + Tidak ada data yang cocok + +
+ )} +
+
-
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
+ + {/* Pagination */} +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
{/* Chart Section */} - - + <Paper withBorder bg={colors['white-1']} p={{ base: 'sm', md: 'lg' }} shadow="md" radius="md"> + <Title order={4} lh={1.2} mb={{ base: 'sm', md: 'md' }}> Data Pengangguran Terdidik & Tidak Terdidik {mounted && chartData.length > 0 ? ( @@ -172,11 +257,13 @@ function ListDetailDataPengangguran({ search }: { search: string }) { />
) : ( - Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik + )} ); } -export default DetailDataPengangguran; +export default DetailDataPengangguran; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx index ec501ee0..aee1b8c0 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/edit/page.tsx @@ -7,12 +7,12 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,6 +24,7 @@ function EditLowonganKerja() { const lowonganState = useProxy(lowonganKerjaState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ posisi: '', @@ -36,6 +37,38 @@ function EditLowonganKerja() { notelp: '', }); + const [originalData, setOriginalData] = useState({ + posisi: '', + namaPerusahaan: '', + lokasi: '', + tipePekerjaan: '', + gaji: '', + deskripsi: '', + kualifikasi: '', + notelp: '', + }) + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.posisi?.trim() !== '' && + formData.namaPerusahaan?.trim() !== '' && + formData.notelp?.trim() !== '' && + formData.lokasi?.trim() !== '' && + formData.tipePekerjaan?.trim() !== '' && + formData.gaji?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + !isHtmlEmpty(formData.kualifikasi) + ); + }; + // load data sekali aja ketika mount / id berubah useEffect(() => { const loadLowongan = async () => { @@ -55,6 +88,16 @@ function EditLowonganKerja() { kualifikasi: data.kualifikasi || '', notelp: data.notelp || '', }); + setOriginalData({ + posisi: data.posisi || '', + namaPerusahaan: data.namaPerusahaan || '', + lokasi: data.lokasi || '', + tipePekerjaan: data.tipePekerjaan || '', + gaji: data.gaji || '', + deskripsi: data.deskripsi || '', + kualifikasi: data.kualifikasi || '', + notelp: data.notelp || '', + }); } } catch (error) { console.error("Error loading lowongan kerja:", error); @@ -71,9 +114,23 @@ function EditLowonganKerja() { [field]: value, })); }; + const handleResetForm = () => { + setFormData({ + posisi: originalData.posisi, + namaPerusahaan: originalData.namaPerusahaan, + lokasi: originalData.lokasi, + tipePekerjaan: originalData.tipePekerjaan, + gaji: originalData.gaji, + deskripsi: originalData.deskripsi, + kualifikasi: originalData.kualifikasi, + notelp: originalData.notelp, + }); + toast.info("Form dikembalikan ke data awal"); + }; const handleSubmit = async () => { try { + setIsSubmitting(true); lowonganState.update.id = params?.id as string; lowonganState.update.form = { ...formData }; @@ -83,18 +140,17 @@ function EditLowonganKerja() { } catch (error) { console.error("Error updating lowongan kerja:", error); toast.error("Terjadi kesalahan saat memperbarui lowongan kerja"); + } finally { + setIsSubmitting(false); } }; return ( - - {/* Header dengan tombol back */} + - - - + Edit Lowongan Kerja Lokal @@ -179,17 +235,31 @@ function EditLowonganKerja() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx index 980529aa..d887a0b8 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -42,7 +42,7 @@ function DetailLowonganKerjaLokal() { const data = lowonganState.findUnique.data; return ( - + - + - - - +
diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx index 606100e8..055c9ddd 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/create/page.tsx @@ -4,22 +4,46 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateLowonganKerja() { const lowonganState = useProxy(lowonganKerjaState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + lowonganState.create.form.posisi?.trim() !== '' && + lowonganState.create.form.namaPerusahaan?.trim() !== '' && + lowonganState.create.form.notelp?.trim() !== '' && + lowonganState.create.form.lokasi?.trim() !== '' && + lowonganState.create.form.tipePekerjaan?.trim() !== '' && + lowonganState.create.form.gaji?.trim() !== '' && + !isHtmlEmpty(lowonganState.create.form.deskripsi) && + !isHtmlEmpty(lowonganState.create.form.kualifikasi) + ); + }; const resetForm = () => { lowonganState.create.form = { @@ -35,25 +59,32 @@ function CreateLowonganKerja() { }; const handleSubmit = async () => { - await lowonganState.create.create(); - resetForm(); - router.push('/admin/ekonomi/lowongan-kerja-lokal'); + try { + setIsSubmitting(true); + await lowonganState.create.create(); + resetForm(); + router.push('/admin/ekonomi/lowongan-kerja-lokal'); + } catch (error) { + console.error('Error creating lowongan kerja:', error); + toast.error( + error instanceof Error ? error.message : 'Gagal membuat lowongan kerja' + ); + } finally { + setIsSubmitting(false); + } }; return ( - - {/* Header dengan tombol kembali */} + - - - + Tambah Lowongan Kerja Lokal @@ -70,7 +101,7 @@ function CreateLowonganKerja() { > (lowonganState.create.form.posisi = val.target.value) } @@ -79,7 +110,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.namaPerusahaan = val.target.value) } @@ -88,7 +119,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.notelp = val.target.value) } @@ -97,7 +128,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.lokasi = val.target.value) } @@ -106,7 +137,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.tipePekerjaan = val.target.value) } @@ -115,7 +146,7 @@ function CreateLowonganKerja() { required /> (lowonganState.create.form.gaji = val.target.value) } @@ -150,17 +181,31 @@ function CreateLowonganKerja() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx index 2c4452ac..33df39ad 100644 --- a/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/lowongan-kerja-lokal/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -47,72 +46,87 @@ function LowonganKerjaLokal() { function ListLowonganKerjaLokal({ search }: { search: string }) { const stateLowongan = useProxy(lowonganKerjaState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = stateLowongan.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - + - + Daftar Lowongan Kerja Lokal - - - + - - + {/* Desktop Table */} + +
- Pekerjaan - Nama Perusahaan - Lokasi - Aksi + + Pekerjaan + + + Nama Perusahaan + + + Lokasi + + + Aksi + {filteredData.length > 0 ? ( filteredData.map((item) => ( - - + + {item.posisi} - - + + {item.namaPerusahaan} - - + + {item.lokasi} - + @@ -131,8 +147,8 @@ function ListLowonganKerjaLokal({ search }: { search: string }) { ) : ( -
- +
+ Tidak ada data lowongan kerja yang cocok
@@ -142,6 +158,57 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
+ + {/* Mobile Card List */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Pekerjaan + + {item.posisi} + + + + Nama Perusahaan + + {item.namaPerusahaan} + + + + Lokasi + + {item.lokasi} + + + + + + )) + ) : ( +
+ + Tidak ada data lowongan kerja yang cocok + +
+ )} +
@@ -162,4 +229,4 @@ function ListLowonganKerjaLokal({ search }: { search: string }) { ); } -export default LowonganKerjaLokal; +export default LowonganKerjaLokal; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx index 6c4e5e2e..e82db659 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/_lib/layoutTabs.tsx @@ -2,18 +2,18 @@ 'use client' import colors from '@/con/colors'; import { + Box, + ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip, - ScrollArea, + Title } from '@mantine/core'; +import { IconCategory, IconShoppingBag } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconShoppingBag, IconCategory } from '@tabler/icons-react'; function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -24,15 +24,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Produk Pasar Desa", value: "produkpasardesa", href: "/admin/ekonomi/pasar-desa/produk-pasar-desa", - icon: , - tooltip: "Kelola data produk yang ada di pasar desa", + icon: }, { label: "Kategori Produk", value: "kategoriproduk", href: "/admin/ekonomi/pasar-desa/kategori-produk", - icon: , - tooltip: "Atur kategori produk pasar desa", + icon: }, ]; @@ -71,43 +69,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper */} - - - {tabs.map((tab, i) => ( - - - {tab.label} - - - ))} - - + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( { + return formData.nama?.trim() !== ''; + }; useEffect(() => { const loadKategoriProduk = async () => { @@ -41,12 +46,11 @@ function EditKategoriProduk() { // simpan data ke state lokal setFormData({ nama: data.nama || '' }); + setOriginalData({ nama: data.nama || '' }); } } catch (error) { console.error('Error loading kategori produk:', error); toast.error('Gagal memuat data kategori produk'); - } finally { - setLoading(false); } }; @@ -60,8 +64,16 @@ function EditKategoriProduk() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); if (!formData.nama.trim()) { toast.error('Nama kategori produk tidak boleh kosong'); return; @@ -82,27 +94,23 @@ function EditKategoriProduk() { } catch (error) { console.error('Error updating kategori produk:', error); toast.error('Terjadi kesalahan saat memperbarui kategori produk'); + } finally { + setIsSubmitting(false); } }; - if (loading) { - return Loading...; - } - return ( - + {/* Header dengan tombol back */} - - - + Edit Kategori Produk @@ -128,17 +136,31 @@ function EditKategoriProduk() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx index 93598be7..5873d9ae 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/create/page.tsx @@ -5,15 +5,15 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; @@ -21,6 +21,12 @@ import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; function CreateKategoriProduk() { const router = useRouter(); const statePasar = useProxy(pasarDesaState.kategoriProduk); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return statePasar.create.form.nama?.trim() !== ''; + }; useEffect(() => { statePasar.findMany.load(); @@ -33,29 +39,34 @@ function CreateKategoriProduk() { }; const handleSubmit = async () => { - if (!statePasar.create.form.nama) { - return toast.warn('Nama kategori produk wajib diisi'); + try { + if (!statePasar.create.form.nama) { + return toast.warn('Nama kategori produk wajib diisi'); + } + setIsSubmitting(true); + await statePasar.create.create(); + resetForm(); + router.push('/admin/ekonomi/pasar-desa/kategori-produk'); + } catch (error) { + console.error(error) + toast.error('Gagal menambahkan kategori produk'); + } finally { + setIsSubmitting(false); } - - await statePasar.create.create(); - resetForm(); - router.push('/admin/ekonomi/pasar-desa/kategori-produk'); }; return ( - + {/* Header dengan tombol kembali */} - - - + Tambah Kategori Produk @@ -74,23 +85,37 @@ function CreateKategoriProduk() { (statePasar.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx index 0115aefd..2f1a972a 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/page.tsx @@ -1,7 +1,24 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -10,83 +27,99 @@ import HeaderSearch from '../../../_com/header'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import pasarDesaState from '../../../_state/ekonomi/pasar-desa/pasar-desa'; - function KategoriProduk() { - const [search2, setSearch2] = useState("") + const [search, setSearch] = useState(''); return ( } - value={search2} - onChange={(e) => setSearch2(e.currentTarget.value)} + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} /> - + ); } -function ListKategoriProduk({ search2 }: { search2: string }) { - const statePasar = useProxy(pasarDesaState.kategoriProduk) - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) - const router = useRouter() +function ListKategoriProduk({ search }: { search: string }) { + const statePasar = useProxy(pasarDesaState.kategoriProduk); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); - const { - data, - page, - totalPages, - loading, - load, - } = statePasar.findMany + const { data, page, totalPages, loading, load } = statePasar.findMany; useShallowEffect(() => { - load(page, 10, search2) - }, [page, search2]) + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const handleHapus = () => { if (selectedId) { - statePasar.delete.byId(selectedId) - setModalHapus(false) - setSelectedId(null) + statePasar.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); } - } + }; - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } return ( - + - Daftar Kategori Produk - - - + + Daftar Kategori Produk + + - - + {/* Desktop Table */} + +
- Nama Kategori - Edit - Delete + + + Nama Kategori + + + + + Edit + + + + + Delete + + @@ -94,42 +127,48 @@ function ListKategoriProduk({ search2 }: { search2: string }) { filteredData.map((item) => ( - + {item.nama} - +
- +
- +
- +
)) ) : ( -
- Tidak ada data kategori produk yang cocok +
+ + Tidak ada data kategori produk yang cocok +
@@ -137,14 +176,69 @@ function ListKategoriProduk({ search2 }: { search2: string }) {
+ + {/* Mobile Card */} + + {filteredData.length > 0 ? ( + + {filteredData.map((item) => ( + + + + Nama Kategori + + + {item.nama} + + + + + + + + ))} + + ) : ( +
+ + Tidak ada data kategori produk yang cocok + +
+ )} +
{ - load(newPage, 10) - window.scrollTo({ top: 0, behavior: 'smooth' }) + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} mt="md" @@ -162,7 +256,7 @@ function ListKategoriProduk({ search2 }: { search2: string }) { text='Apakah anda yakin ingin menghapus kategori produk ini?' /> - ) + ); } -export default KategoriProduk; +export default KategoriProduk; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx index 9ebad0e4..db66e722 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/layout.tsx @@ -1,9 +1,29 @@ 'use client' +import { usePathname } from "next/navigation"; import LayoutTabs from "./_lib/layoutTabs" +import { Box } from "@mantine/core"; -export default function Layout({children} : {children: React.ReactNode}) { +export default function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } return ( {children} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx index 17ebd925..0fc729ac 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/edit/page.tsx @@ -1,21 +1,23 @@ /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -32,6 +34,7 @@ type FormData = { rating: number; kategoriId: string[]; kontak: string; + deskripsi: string; }; function EditPasarDesa() { @@ -41,6 +44,7 @@ function EditPasarDesa() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', harga: 0, @@ -49,8 +53,38 @@ function EditPasarDesa() { rating: 0, kategoriId: [], kontak: '', + deskripsi: '' }); + const [originalData, setOriginalData] = useState({ + nama: '', + harga: 0, + alamatUsaha: '', + imageId: '', + imageUrl: "", + rating: 0, + kategoriId: [], + kontak: '', + deskripsi: '' + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.harga !== null && + formData.harga > 0 && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // load data awal useEffect(() => { pasarState.kategoriProduk.findManyAll.load(); @@ -70,6 +104,18 @@ function EditPasarDesa() { rating: data.rating || 0, kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [], kontak: data.kontak || '', + deskripsi: data.deskripsi || '' + }); + setOriginalData({ + nama: data.nama || '', + harga: data.harga || 0, + alamatUsaha: data.alamatUsaha || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || "", + rating: data.rating || 0, + kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [], + kontak: data.kontak || '', + deskripsi: data.deskripsi || '' }); if (data.image?.link) setPreviewImage(data.image.link); } @@ -88,8 +134,26 @@ function EditPasarDesa() { setFormData((prev) => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + harga: originalData.harga, + alamatUsaha: originalData.alamatUsaha, + imageId: originalData.imageId, + rating: originalData.rating, + kategoriId: (originalData as any)?.KategoriToPasar?.map((k: any) => k.kategoriId) || [], + kontak: originalData.kontak, + deskripsi: originalData.deskripsi + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { try { + setIsSubmitting(true); // upload image kalau ada file baru let imageId = formData.imageId; if (file) { @@ -111,17 +175,17 @@ function EditPasarDesa() { } catch (error) { console.error('Error updating pasar desa:', error); toast.error('Terjadi kesalahan saat memperbarui pasar desa'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Pasar Desa @@ -151,7 +215,7 @@ function EditPasarDesa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -170,25 +234,45 @@ function EditPasarDesa() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -256,18 +340,45 @@ function EditPasarDesa() { error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined} /> + {/* Input Deskripsi */} + + + Deskripsi + + + setFormData((prev) => ({ ...prev, deskripsi: htmlContent })) + } + /> + + + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx index 96f52523..d31ce111 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import React, { useState } from 'react'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; -import { useShallowEffect } from '@mantine/hooks'; -import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; function DetailPasarDesa() { const statePasar = useProxy(pasarDesaState); @@ -40,7 +40,7 @@ function DetailPasarDesa() { const data = statePasar.pasarDesa.findUnique.data; return ( - + - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx index 72265081..0fc7fc03 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/create/page.tsx @@ -3,17 +3,18 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -22,12 +23,32 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; export default function CreatePasarDesa() { const router = useRouter(); const statePasar = useProxy(pasarDesaState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + statePasar.pasarDesa.create.form.nama?.trim() !== '' && + statePasar.pasarDesa.create.form.harga !== null && + statePasar.pasarDesa.create.form.harga > 0 && + !isHtmlEmpty(statePasar.pasarDesa.create.form.deskripsi) && + file !== null + ); + }; useEffect(() => { statePasar.kategoriProduk.findManyAll.load(); @@ -42,42 +63,49 @@ export default function CreatePasarDesa() { rating: 0, kategoriId: [], kontak: '', + deskripsi: '' }; setPreviewImage(null); setFile(null); }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + statePasar.pasarDesa.create.form.imageId = uploaded.id; + await statePasar.pasarDesa.create.create(); + + resetForm(); + router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa'); + } catch (error) { + console.error('Error creating kategori produk:', error); + toast.error('Gagal membuat kategori produk'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - statePasar.pasarDesa.create.form.imageId = uploaded.id; - await statePasar.pasarDesa.create.create(); - - resetForm(); - router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa'); }; return ( - + {/* Header dengan tombol kembali */} - - - + Tambah Produk Pasar Desa @@ -108,7 +136,7 @@ export default function CreatePasarDesa() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -129,7 +157,7 @@ export default function CreatePasarDesa() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -145,7 +191,7 @@ export default function CreatePasarDesa() { (statePasar.pasarDesa.create.form.nama = e.target.value)} required /> @@ -155,7 +201,7 @@ export default function CreatePasarDesa() { type="number" label="Harga Produk" placeholder="Masukkan harga produk" - defaultValue={statePasar.pasarDesa.create.form.harga} + value={statePasar.pasarDesa.create.form.harga} onChange={(e) => (statePasar.pasarDesa.create.form.harga = Number(e.target.value))} required /> @@ -168,7 +214,7 @@ export default function CreatePasarDesa() { step={0.1} label="Rating Produk (0–5)" placeholder="Masukkan rating produk" - defaultValue={statePasar.pasarDesa.create.form.rating} + value={statePasar.pasarDesa.create.form.rating} onChange={(e) => { const value = Number(e.target.value); if (value >= 0 && value <= 5) { @@ -181,7 +227,7 @@ export default function CreatePasarDesa() { (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)} /> @@ -190,7 +236,7 @@ export default function CreatePasarDesa() { label="Kontak" type="number" placeholder="Masukkan kontak" - defaultValue={statePasar.pasarDesa.create.form.kontak} + value={statePasar.pasarDesa.create.form.kontak} onChange={(e) => (statePasar.pasarDesa.create.form.kontak = e.target.value)} /> @@ -208,19 +254,45 @@ export default function CreatePasarDesa() { } /> + + + Deskripsi Produk + + { + statePasar.pasarDesa.create.form.deskripsi = val; + }} + /> + + {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx index fc14e3f3..8a83c0c8 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/produk-pasar-desa/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -46,51 +45,58 @@ function PasarDesa() { function ListPasarDesa({ search }: { search: string }) { const statePasar = useProxy(pasarDesaState.pasarDesa); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = statePasar.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - + - Daftar Produk Pasar Desa - - - + Daftar Produk Pasar Desa + - - + {/* Desktop Table */} + +
- Nama Produk - Harga Produk - Rating - Alamat Usaha - Aksi + Nama Produk + Harga Produk + Rating + Alamat Usaha + Aksi @@ -98,18 +104,18 @@ function ListPasarDesa({ search }: { search: string }) { filteredData.map((item) => ( - + {item.nama} - Rp.{item.harga} + Rp.{item.harga} - {item.rating || '-'} + {item.rating || '-'} - + {item.alamatUsaha || '-'} @@ -124,7 +130,7 @@ function ListPasarDesa({ search }: { search: string }) { } > - Detail + Detail @@ -132,8 +138,8 @@ function ListPasarDesa({ search }: { search: string }) { ) : ( -
- +
+ Tidak ada produk pasar desa yang cocok
@@ -143,6 +149,57 @@ function ListPasarDesa({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama Produk + {item.nama} + + + Harga Produk + Rp.{item.harga} + + + Rating + {item.rating || '-'} + + + Alamat Usaha + + {item.alamatUsaha || '-'} + + + + + + + + )) + ) : ( +
+ + Tidak ada produk pasar desa yang cocok + +
+ )} +
@@ -163,4 +220,4 @@ function ListPasarDesa({ search }: { search: string }) { ); } -export default PasarDesa; +export default PasarDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx index 6dbb6b03..a5f2a416 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/edit/page.tsx @@ -9,18 +9,18 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState, useCallback } from 'react'; -import { useProxy } from 'valtio/utils'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; type Statistik = { tahun: string; @@ -50,6 +50,26 @@ function EditProgramKemiskinan() { const stateProgram = useProxy(programKemiskinanState); const [formData, setFormData] = useState(initialForm); + const [isSubmitting, setIsSubmitting] = useState(false); + const [originalData, setOriginalData] = useState(initialForm); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.icon?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.statistik.jumlah?.trim() !== '' && + formData.statistik.tahun?.trim() !== '' + ); + }; // Load data 1x dari global state → isi local state useEffect(() => { @@ -69,6 +89,15 @@ function EditProgramKemiskinan() { jumlah: data.statistik?.jumlah?.toString() ?? '', }, }); + setOriginalData({ + nama: data.nama ?? '', + deskripsi: data.deskripsi ?? '', + icon: data.icon ?? '', + statistik: { + tahun: data.statistik?.tahun?.toString() ?? '', + jumlah: data.statistik?.jumlah?.toString() ?? '', + }, + }); } } catch (err) { console.error('Error load data:', err); @@ -100,8 +129,22 @@ function EditProgramKemiskinan() { [] ); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + icon: originalData.icon, + statistik: { + tahun: originalData.statistik.tahun, + jumlah: originalData.statistik.jumlah, + }, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateProgram.update.id = id; stateProgram.update.form = formData; await stateProgram.update.update(); @@ -111,23 +154,23 @@ function EditProgramKemiskinan() { } catch (error) { console.error('Error update program:', error); toast.error('Terjadi kesalahan saat memperbarui program'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Program Kemiskinan @@ -195,17 +238,31 @@ function EditProgramKemiskinan() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx index 5e4acf64..0ec7cb40 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/[id]/page.tsx @@ -8,17 +8,16 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; +import { IconKey, IconMapper } from '../../../_com/iconMap'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; -import { IconKey, IconMapper } from '../../../_com/iconMap'; function DetailProgramKemiskinan() { const programState = useProxy(programKemiskinanState); @@ -51,7 +50,7 @@ function DetailProgramKemiskinan() { const data = programState.findUnique.data; return ( - + {/* Tombol Kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx index cf4c0696..4d664ffb 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx @@ -7,27 +7,47 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; -import CreateEditor from '../../../_com/createEditor'; -import SelectIconProgram from '../../../_com/selectIcon'; import { useState } from 'react'; import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import CreateEditor from '../../../_com/createEditor'; +import SelectIconProgram from '../../../_com/selectIcon'; +import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; function CreateProgramKemiskinan() { const programState = useProxy(programKemiskinanState); const router = useRouter(); const [lineChart, setLineChart] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + programState.create.form.nama?.trim() !== '' && + programState.create.form.icon?.trim() !== '' && + !isHtmlEmpty(programState.create.form.deskripsi) && + programState.create.form.statistik.jumlah?.trim() !== '' && + programState.create.form.statistik.tahun?.trim() !== '' + ); + }; + const resetForm = () => { programState.create.form = { nama: '', @@ -41,40 +61,46 @@ function CreateProgramKemiskinan() { }; const handleSubmit = async () => { - if (!programState.create.form.nama || !programState.create.form.deskripsi) { - return toast.warn('Judul dan deskripsi wajib diisi'); - } - - const id = await programState.create.create(); - if (id) { - const idStr = String(id); - await programState.findUnique.load(idStr); - if (programState.findUnique.data) { - setLineChart([programState.findUnique.data]); + try { + setIsSubmitting(true); + if (!programState.create.form.nama || !programState.create.form.deskripsi) { + return toast.warn('Judul dan deskripsi wajib diisi'); } - toast.success('Program berhasil ditambahkan'); - } else { - toast.error('Gagal menambahkan program, coba lagi'); - } - resetForm(); - router.push('/admin/ekonomi/program-kemiskinan'); + const id = await programState.create.create(); + if (id) { + const idStr = String(id); + await programState.findUnique.load(idStr); + if (programState.findUnique.data) { + setLineChart([programState.findUnique.data]); + } + toast.success('Program berhasil ditambahkan'); + } else { + toast.error('Gagal menambahkan program, coba lagi'); + } + + resetForm(); + router.push('/admin/ekonomi/program-kemiskinan'); + } catch (error) { + console.error('Gagal menyimpan data:', error); + toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header dengan tombol back */} - - - + Tambah Program Kemiskinan @@ -93,7 +119,7 @@ function CreateProgramKemiskinan() { (programState.create.form.nama = val.target.value)} required /> @@ -128,7 +154,7 @@ function CreateProgramKemiskinan() { (programState.create.form.statistik.jumlah = val.target.value) } @@ -138,7 +164,7 @@ function CreateProgramKemiskinan() { /> (programState.create.form.statistik.tahun = val.target.value) } @@ -150,17 +176,31 @@ function CreateProgramKemiskinan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx index def7ec5e..23abe54f 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/page.tsx @@ -1,18 +1,43 @@ 'use client' /* eslint-disable @typescript-eslint/no-explicit-any */ import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, XAxis, YAxis } from 'recharts'; +import { + CartesianGrid, + Legend, + Line, + LineChart, + Tooltip as RechartTooltip, + XAxis, + YAxis, +} from 'recharts'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../_com/header'; import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan'; function ProgramKemiskinan() { - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(''); return ( ([]); const [mounted, setMounted] = useState(false); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = programState.findMany; useShallowEffect(() => { setMounted(true); - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); useEffect(() => { if (data) { const chartData = data - .filter(item => item.statistik) - .map(item => ({ + .filter((item) => item.statistik) + .map((item) => ({ tahun: item.statistik?.tahun, - jumlah: Number(item.statistik?.jumlah) + jumlah: Number(item.statistik?.jumlah), })) .sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); @@ -58,51 +84,90 @@ function ListProgramKemiskinan({ search }: { search: string }) { if (loading || !data) { return ( - + ); } return ( - - - - Daftar Program Kemiskinan - - - + + {/* Daftar Program Kemiskinan */} + + + + Daftar Program Kemiskinan + + - - + + {/* Desktop Table */} + +
- Judul Program - Deskripsi Singkat - Jumlah Masyarakat Miskin - Aksi + + + Judul Program + + + + + Deskripsi Singkat + + + + + Jumlah Masyarakat Miskin + + + + + Aksi + + {filteredData.length > 0 ? ( - filteredData.map(item => ( + filteredData.map((item) => ( - {item.nama} + + {item.nama} + - + + + + + {item.statistik?.jumlah || '-'} + - {item.statistik?.jumlah || '-'} @@ -112,7 +177,9 @@ function ListProgramKemiskinan({ search }: { search: string }) {
- Tidak ada data program kemiskinan yang cocok + + Tidak ada data program kemiskinan yang cocok +
@@ -120,6 +187,61 @@ function ListProgramKemiskinan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Judul Program + + + {item.nama} + + + + + Deskripsi Singkat + + + + + + Jumlah Masyarakat Miskin + + + {item.statistik?.jumlah || '-'} + + + + + + + + )) + ) : ( +
+ + Tidak ada data program kemiskinan yang cocok + +
+ )} +
+
{/* Pagination */} @@ -139,25 +261,45 @@ function ListProgramKemiskinan({ search }: { search: string }) {
{/* Chart */} - - - Grafik Berdasarkan Responden + + + + Grafik Berdasarkan Responden + {mounted && lineChart.length > 0 ? ( - - - - - - [`${value} orang`, name]} - labelFormatter={(label: any) => `Tahun: ${label}`} - /> - - - + + + + + + + [`${value} orang`, 'Jumlah']} + labelFormatter={(label: any) => `Tahun: ${label}`} + /> + + + + ) : ( - Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik + )} @@ -165,4 +307,4 @@ function ListProgramKemiskinan({ search }: { search: string }) { ); } -export default ProgramKemiskinan; +export default ProgramKemiskinan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx index f746afeb..da00ccba 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/edit/page.tsx @@ -1,31 +1,31 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' +import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa'; import colors from '@/con/colors'; import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; import { toast } from 'react-toastify'; -import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; +import { useProxy } from 'valtio/utils'; function EditSektorUnggulanDesa() { const router = useRouter(); const params = useParams() as { id: string }; const stateGrafik = useProxy(grafikSektorUnggulan); - + const [isSubmitting, setIsSubmitting] = useState(false); const id = params.id; // state lokal buat form @@ -35,6 +35,29 @@ function EditSektorUnggulanDesa() { value: 0, }); + const [originalData, setOriginalData] = useState({ + name: '', + description: '', + value: 0, + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.description) && + formData.value !== null && + formData.value >= 0 + ); + }; + // Load data saat komponen mount useEffect(() => { if (id) { @@ -48,6 +71,11 @@ function EditSektorUnggulanDesa() { description: data.description || '', value: data.value || 0, }); + setOriginalData({ + name: data.name || '', + description: data.description || '', + value: data.value || 0, + }); } }) .catch((err) => { @@ -59,13 +87,14 @@ function EditSektorUnggulanDesa() { const handleChange = (field: keyof typeof formData) => - (e: React.ChangeEvent) => { - const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value; - setFormData((prev) => ({ ...prev, [field]: value })); - }; + (e: React.ChangeEvent) => { + const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value; + setFormData((prev) => ({ ...prev, [field]: value })); + }; const handleSubmit = async () => { try { + setIsSubmitting(true); stateGrafik.update.id = id; stateGrafik.update.form = { ...formData }; // update global pas submit await stateGrafik.update.submit(); @@ -74,22 +103,31 @@ function EditSektorUnggulanDesa() { } catch (error) { console.error('Error update sektor unggulan:', error); toast.error('Terjadi kesalahan saat memperbarui sektor unggulan'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + description: originalData.description, + value: originalData.value, + }); + toast.info('Form dikembalikan ke data awal'); + }; + return ( - + - - - + Edit Sektor Unggulan Desa @@ -132,17 +170,31 @@ function EditSektorUnggulanDesa() { /> + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx index 631a4f52..802e099c 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/[id]/page.tsx @@ -8,8 +8,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -49,7 +48,7 @@ function DetailSektorUnggulanDesa() { const data = stateGrafik.findUnique.data; return ( - + {/* Tombol kembali */} - + - - - +
diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx index 4761de26..edd2c503 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/create/page.tsx @@ -6,24 +6,43 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; import CreateEditor from '../../../_com/createEditor'; +import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; +import { toast } from 'react-toastify'; function CreateSektorUnggulanDesa() { const stateGrafik = useProxy(grafikSektorUnggulan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateGrafik.create.form.name?.trim() !== '' && + !isHtmlEmpty(stateGrafik.create.form.description) && + stateGrafik.create.form.value !== null && + stateGrafik.create.form.value >= 0 + ); + }; const resetForm = () => { stateGrafik.create.form = { @@ -34,32 +53,38 @@ function CreateSektorUnggulanDesa() { }; const handleSubmit = async () => { - const id = await stateGrafik.create.create(); - if (id) { - const idStr = String(id); - await stateGrafik.findUnique.load(idStr); - if (stateGrafik.findUnique.data) { - setChartData([stateGrafik.findUnique.data]); + try { + setIsSubmitting(true); + const id = await stateGrafik.create.create(); + if (id) { + const idStr = String(id); + await stateGrafik.findUnique.load(idStr); + if (stateGrafik.findUnique.data) { + setChartData([stateGrafik.findUnique.data]); + } } + resetForm(); + router.push('/admin/ekonomi/sektor-unggulan-desa'); + } catch (error) { + console.error('Error creating sektor unggulan:', error); + toast.error('Terjadi kesalahan saat menambahkan sektor unggulan'); + } finally { + setIsSubmitting(false); } - resetForm(); - router.push('/admin/ekonomi/sektor-unggulan-desa'); }; return ( - + {/* Header dengan back button */} - - - + Tambah Sektor Unggulan Desa @@ -78,7 +103,7 @@ function CreateSektorUnggulanDesa() { { stateGrafik.create.form.name = e.currentTarget.value; }} @@ -101,7 +126,7 @@ function CreateSektorUnggulanDesa() { label="Jumlah" type="number" placeholder="Masukkan jumlah" - defaultValue={stateGrafik.create.form.value} + value={stateGrafik.create.form.value} onChange={(e) => { stateGrafik.create.form.value = Number(e.currentTarget.value); }} @@ -109,17 +134,31 @@ function CreateSektorUnggulanDesa() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx index 3a7cc1e8..e46cc724 100644 --- a/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/sektor-unggulan-desa/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -59,6 +58,8 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { load, } = state.findMany; + const [debouncedSearch] = useDebouncedValue(search, 1000); + useEffect(() => { if (state.findMany.data) { setChartData( @@ -73,14 +74,14 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { }, [state.findMany.data]); useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch) + }, [page, debouncedSearch]) - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + ); @@ -88,73 +89,131 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { return ( - {/* List Table */} - - - List Sektor Unggulan Desa - - - + {/* List Section */} + + + + List Sektor Unggulan Desa + + - {loading ? ( - - ) : ( - - - - - Nama Sektor - Deskripsi - Detail - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.name} - - - - - - - - - - - - - - - )) - ) : ( - - -
- Tidak ada data sektor unggulan yang cocok -
+ + {/* Desktop Table */} + +
+ + + Nama Sektor + Deskripsi + Detail + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.name} + + + + + + + - )} - -
-
- )} + )) + ) : ( + + +
+ + Tidak ada data sektor unggulan yang cocok + +
+
+
+ )} + + +
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Sektor + + + {item.name} + + + + + Deskripsi + + + + + + + + + + + )) + ) : ( +
+ + Tidak ada data sektor unggulan yang cocok + +
+ )} +
+ {/* Pagination */}
- {/* Chart */} - - + {/* Chart Section */} + <Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md"> + <Title order={4} lh={1.2} pb={{ base: 'sm', md: 'md' }}> Grafik Sektor Unggulan Desa {loading ? ( ) : chartData.length > 0 ? ( - + @@ -191,7 +248,9 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { ) : (
- Belum ada data untuk ditampilkan dalam grafik + + Belum ada data untuk ditampilkan dalam grafik +
)}
@@ -199,4 +258,4 @@ function ListSektorUnggulanDesa({ search }: { search: string }) { ); } -export default SektorUnggulanDesa; +export default SektorUnggulanDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx deleted file mode 100644 index 4425aa88..00000000 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/_lib/layoutTabs.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import colors from '@/con/colors'; -import { - ScrollArea, - Stack, - Tabs, - TabsList, - TabsPanel, - TabsTab, - Title, - Tooltip, -} from '@mantine/core'; -import { - IconBuildingCommunity, - IconHierarchy, - IconUsers -} from '@tabler/icons-react'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; - -function LayoutTabs({ children }: { children: React.ReactNode }) { - const router = useRouter(); - const pathname = usePathname(); - - const tabs = [ - { - label: "Pegawai", - value: "pegawai", - href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai", - icon: , - tooltip: "Kelola data pegawai BUMDesa", - }, - { - label: "Posisi Organisasi", - value: "posisiorganisasi", - href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi", - icon: , - tooltip: "Kelola daftar posisi organisasi", - }, - { - label: "Struktur Organisasi", - value: "strukturorganisasi", - href: "/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/struktur-organisasi", - icon: , - tooltip: "Kelola struktur organisasi BUMDesa" - } - ]; - - const currentTab = tabs.find((tab) => tab.href === pathname); - const [activeTab, setActiveTab] = useState( - currentTab?.value || tabs[0].value - ); - - const handleTabChange = (value: string | null) => { - const tab = tabs.find((t) => t.value === value); - if (tab) { - router.push(tab.href); - } - setActiveTab(value); - }; - - useEffect(() => { - const match = tabs.find((tab) => tab.href === pathname); - if (match) { - setActiveTab(match.value); - } - }, [pathname]); - - return ( - - - Struktur Organisasi & SK Pengurus BUMDesa - - - - {/* ✅ Scroll horizontal biar rapi kalau label panjang */} - - - {tabs.map((tab, i) => ( - - - {tab.label} - - - ))} - - - - {tabs.map((tab, i) => ( - - {children} - - ))} - - - ); -} - -export default LayoutTabs; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx deleted file mode 100644 index 57a94a4f..00000000 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import LayoutTabs from "./_lib/layoutTabs" - - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx deleted file mode 100644 index d56609de..00000000 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/page.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import colors from '@/con/colors'; -import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core'; -import { IconCheck, IconDeviceImacCog, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import HeaderSearch from '../../../_com/header'; -import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; - - -function PegawaiBumDes() { - const [search, setSearch] = useState(""); - return ( - - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - ); -} - -function ListPegawaiBumdes({ search }: { search: string }) { - const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); - const router = useRouter(); - - const { - data, - page, - totalPages, - loading, - load, - } = stateOrganisasi.findMany; - - useEffect(() => { - load(page, 10, search); - }, [page, search]); - - const filteredData = data || [] - - // Handle loading state - if (loading || !data) { - return ( - - - - ); - } - - if (data.length === 0) { - return ( - - - - Daftar Pegawai BUMDesa - - - - -
- Tidak ada data pegawai yang ditemukan -
-
-
- ); - } - return ( - - - - Daftar Pegawai BUMDesa - - - - - - - - - Nama Lengkap - Posisi - Status - Aksi - - - - {(() => { - console.log('Rendering table with items:', stateOrganisasi.findMany.data); - return null; - })()} - {([...filteredData] - .sort((a, b) => { - if (a.isActive === b.isActive) { - return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama - } - return Number(b.isActive) - Number(a.isActive); // aktif duluan - }) // Aktif di atas - ).map((item) => ( - - - - - {item.namaLengkap} - - - - - - - {item.posisi?.nama || 'Belum diatur'} - - - - - - - - {item.isActive ? "Aktif" : "Tidak Aktif"} - - - - {item.isActive ? ( - - - - ) : ( - - - - )} - - - - - - - - ))} - -
-
-
- { - load(newPage, 10); - window.scrollTo(0, 0); - }} - total={totalPages} - withEdges - withControls - radius="md" - /> -
-
-
- ); -} - -export default PegawaiBumDes; diff --git a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx b/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx deleted file mode 100644 index ea181a8f..00000000 --- a/src/app/admin/(dashboard)/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/posisi-organisasi/page.tsx +++ /dev/null @@ -1,175 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import HeaderSearch from '../../../_com/header'; -import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; -import stateStrukturBumDes from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi'; - - -function PosisiOrganisasiBumDes() { - const [search, setSearch] = useState(""); - return ( - - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - ); -} - -function ListPosisiOrganisasiBumDes({ search }: { search: string }) { - const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi) - const router = useRouter(); - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) - - const { - data, - page, - totalPages, - loading, - load, - } = stateOrganisasi.findMany; - - useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); - - const handleHapus = async () => { - if (selectedId) { - await stateOrganisasi.delete.byId(selectedId); - setModalHapus(false) - setSelectedId(null) - } - } - - const filteredData = data || [] - - if (loading || !data) { - return ( - - - - ); - } - - return ( - - - - Daftar Posisi Organisasi BumDes - - - - - - - - - Nama Posisi - Deskripsi - Hierarki - Edit - Hapus - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - {item.nama} - - - - - - - - {item.hierarki || '-'} - - - - - - - - - - - - - )) - ) : ( - - -
- Tidak ada data posisi organisasi yang cocok -
-
-
- )} -
-
-
-
-
- { - load(newPage, 10, search); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
- {/* Modal Hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus posisi organisasi BumDes ini?" - /> -
- ); -} - -export default PosisiOrganisasiBumDes; diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx index f1857aec..27d48063 100644 --- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -40,7 +40,7 @@ function DetailAjukanIdeInofativDesa() { const data = state.findUnique.data; return ( - + {/* Tombol Kembali */} - + {/* Detail Data */} @@ -104,7 +102,7 @@ function DetailAjukanIdeInofativDesa() { Deskripsi - + diff --git a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx index 24bdbd06..2b03f5d2 100644 --- a/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/ajukan-ide-inovatif/page.tsx @@ -17,7 +17,7 @@ import { Text, Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -44,6 +44,7 @@ function AjukanIdeInovatif() { function ListAjukanIdeInovatif({ search }: { search: string }) { const state = useProxy(ajukanIdeInovatifState) const router = useRouter() + const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay const { data, @@ -54,8 +55,8 @@ function ListAjukanIdeInovatif({ search }: { search: string }) { } = state.findMany; useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch) + }, [page, debouncedSearch]) const filteredData = data || [] @@ -72,7 +73,9 @@ function ListAjukanIdeInovatif({ search }: { search: string }) { Daftar Ide Inovatif - +
Nama diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx index 05c93b59..05a941b5 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/edit/page.tsx @@ -5,16 +5,17 @@ import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digita import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -30,12 +31,33 @@ function EditDigitalSmartVillage() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsi: '', imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; useEffect(() => { const loadData = async () => { @@ -50,6 +72,12 @@ function EditDigitalSmartVillage() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } @@ -64,6 +92,7 @@ function EditDigitalSmartVillage() { const handleSubmit = async () => { try { + setIsSubmitting(true); stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData }; if (file) { @@ -80,18 +109,29 @@ function EditDigitalSmartVillage() { } catch (error) { console.error('Error updating desa digital:', error); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; + return ( - + {/* Header */} - - - + Edit Desa Digital Smart Village @@ -122,7 +162,7 @@ function EditDigitalSmartVillage() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -141,25 +181,45 @@ function EditDigitalSmartVillage() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -188,17 +248,31 @@ function EditDigitalSmartVillage() { {/* Tombol Simpan */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx index c82485e0..2ff93106 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/[id]/page.tsx @@ -8,8 +8,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -50,7 +49,7 @@ function DetailDesaDigital() { const data = stateDesaDigital.findUnique.data; return ( - + {/* Tombol Kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx index 6a00b669..051861a2 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/create/page.tsx @@ -2,31 +2,49 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, - Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import ExifOrientationImg from 'react-exif-orientation-img'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import desaDigitalState from '../../../_state/inovasi/desa-digital'; -import { Dropzone } from '@mantine/dropzone'; export default function CreateDesaDigital() { const stateDesaDigital = useProxy(desaDigitalState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateDesaDigital.create.form.name?.trim() !== '' && + !isHtmlEmpty(stateDesaDigital.create.form.deskripsi) && + file !== null + ); + }; const resetForm = () => { stateDesaDigital.create.form = { @@ -44,6 +62,7 @@ export default function CreateDesaDigital() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file, name: file.name, @@ -64,24 +83,24 @@ export default function CreateDesaDigital() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header dengan tombol kembali */} - - - + Tambah Desa Digital Smart Village @@ -104,7 +123,7 @@ export default function CreateDesaDigital() { (stateDesaDigital.create.form.name = e.target.value)} radius="md" withAsterisk @@ -138,7 +157,7 @@ export default function CreateDesaDigital() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" style={{ @@ -166,6 +185,7 @@ export default function CreateDesaDigital() { {/* Preview */} {previewImage && ( - + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Tombol Submit */} - + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx index 84cb3baf..273fada0 100644 --- a/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/desa-digital-smart-village/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -46,50 +45,70 @@ function DesaDigitalSmartVillage() { function ListDesaDigitalSmartVillage({ search }: { search: string }) { const state = useProxy(desaDigitalState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - + - List Desa Digital Smart Village - - - + + List Desa Digital Smart Village + + - -
+ + {/* Desktop Table */} + +
- Nama Inovasi - - Deskripsi Singkat Inovasi + + + Nama Inovasi + + + + + Deskripsi Singkat Inovasi + + + + + Aksi + - Aksi @@ -97,21 +116,18 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - - + @@ -132,8 +150,8 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) { ) : ( -
- +
+ Tidak ada data inovasi digital yang cocok
@@ -143,6 +161,64 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Inovasi + + + {item.name} + + + + + Deskripsi Singkat Inovasi + + + + + + + + + + + )) + ) : ( +
+ + Tidak ada data inovasi digital yang cocok + +
+ )} +
+
(null); const [file, setFile] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsi: '', imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; // Load data pertama kali useEffect(() => { @@ -51,7 +73,12 @@ function EditInfoTeknologiTepatGuna() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); - + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -63,9 +90,21 @@ function EditInfoTeknologiTepatGuna() { loadData(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setFile(null); + setPreviewImage(originalData.imageUrl); + toast.info("Form dikembalikan ke data awal"); + }; + // Submit form const handleSubmit = async () => { try { + setIsSubmitting(true); // sync local → global pas submit stateInfoTekno.edit.form = { ...stateInfoTekno.edit.form, @@ -93,18 +132,18 @@ function EditInfoTeknologiTepatGuna() { } catch (error) { console.error('Error updating info teknologi tepat guna:', error); toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Tombol back + title */} - - - + Edit Info Teknologi Tepat Guna @@ -144,7 +183,7 @@ function EditInfoTeknologiTepatGuna() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -163,25 +202,45 @@ function EditInfoTeknologiTepatGuna() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -201,17 +260,31 @@ function EditInfoTeknologiTepatGuna() { {/* Tombol submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx index 8e9d2dab..17b87de5 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -40,7 +40,7 @@ function DetailInfoTeknologiTepatGuna() { const data = stateInfoTekno.findUnique.data return ( - + {/* Tombol Kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx index be8b23b7..f0c82b63 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,29 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import infoTeknoState from '../../../_state/inovasi/info-tekno'; -import { Dropzone } from '@mantine/dropzone'; function CreateInfoTeknologiTepatGuna() { const stateInfoTekno = useProxy(infoTeknoState); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateInfoTekno.create.form.name?.trim() !== '' && + !isHtmlEmpty(stateInfoTekno.create.form.deskripsi) && + file !== null + ); + }; const resetForm = () => { stateInfoTekno.create.form = { @@ -44,6 +62,7 @@ function CreateInfoTeknologiTepatGuna() { } try { + setIsSubmitting(true); const uploadRes = await ApiFetch.api.fileStorage.create.post({ file: file, name: file.name, @@ -65,18 +84,18 @@ function CreateInfoTeknologiTepatGuna() { } catch (error) { console.error('Error in handleSubmit:', error); toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Tambah Info Teknologi Tepat Guna @@ -94,7 +113,7 @@ function CreateInfoTeknologiTepatGuna() { {/* Nama */} { stateInfoTekno.create.form.name = val.target.value; }} @@ -131,7 +150,7 @@ function CreateInfoTeknologiTepatGuna() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -152,7 +171,7 @@ function CreateInfoTeknologiTepatGuna() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx index 3fcb8f8a..b5d391e1 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/page.tsx @@ -4,6 +4,7 @@ import { Box, Button, Center, + Group, Pagination, Paper, Skeleton, @@ -16,10 +17,8 @@ import { TableTr, Text, Title, - Group, - Tooltip, } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -46,76 +45,94 @@ function InfoTeknologiTepatGuna() { function ListInfoTeknologiTepatGuna({ search }: { search: string }) { const state = useProxy(infoTeknoState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - + + - Daftar Info Teknologi Tepat Guna - - - + + Daftar Info Teknologi Tepat Guna + + - - + {/* Desktop Table */} + +
- Nama Info Teknologi + + Nama Info Teknologi + - Deskripsi Singkat + + Deskripsi Singkat + + + + + Aksi + - Aksi {filteredData.length > 0 ? ( filteredData.map((item) => ( - - + + {item.name} - + - +
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Info Teknologi + + + {item.name} + + + + + Deskripsi Singkat + + + + + + + + + + + )) + ) : ( +
+ + Tidak ada data Info Teknologi yang cocok + +
+ )} +
@@ -167,4 +241,4 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) { ); } -export default InfoTeknologiTepatGuna; +export default InfoTeknologiTepatGuna; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx index 781f1ec5..7ee9bba3 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/_lib/layoutTabs.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; +import { IconListDetails, IconUsers } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; -import { IconListDetails, IconUsers } from '@tabler/icons-react'; function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -15,14 +15,12 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { label: "List Kolaborasi Inovasi", value: "listkolaborasiinovasi", href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi", - tooltip: "Lihat daftar kolaborasi inovasi", icon: , }, { label: "Mitra Kolaborasi", value: "mitarakolaborasi", href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi", - tooltip: "Kelola mitra kolaborasi", icon: , } ]; @@ -59,43 +57,76 @@ function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper biar rapi */} - - - {tabs.map((tab, i) => ( - + + + + {tabs.map((tab, i) => ( {tab.label} - - ))} - - + ))} + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( = 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + return ( {children} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx index cce425a1..48134a46 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/edit/page.tsx @@ -8,13 +8,14 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; +import { YearPickerInput } from "@mantine/dates"; import { IconArrowBack } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -25,6 +26,7 @@ function EditKolaborasiInovasi() { const kolaborasiState = useProxy(kolaborasiInovasiState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: "", @@ -34,6 +36,31 @@ function EditKolaborasiInovasi() { kolaborator: "", }); + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + tahun: "", + slug: "", + kolaborator: "", + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.slug?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.kolaborator?.trim() !== '' + ); + }; + // Load data awal dari server useEffect(() => { const loadKolaborasi = async () => { @@ -50,6 +77,13 @@ function EditKolaborasiInovasi() { slug: data.slug ?? "", kolaborator: data.kolaborator ?? "", }); + setOriginalData({ + name: data.name ?? "", + deskripsi: data.deskripsi ?? "", + tahun: data.tahun?.toString() ?? "", + slug: data.slug ?? "", + kolaborator: data.kolaborator ?? "", + }) } } catch (error) { console.error("Error loading kolaborasi:", error); @@ -60,9 +94,21 @@ function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + tahun: originalData.tahun?.toString(), + slug: originalData.slug, + kolaborator: originalData.kolaborator, + }); + toast.info("Form dikembalikan ke data awal"); + }; + // Handler submit → baru update global state const handleSubmit = async () => { try { + setIsSubmitting(true); kolaborasiState.update.form = { ...kolaborasiState.update.form, name: formData.name, @@ -74,10 +120,12 @@ function EditKolaborasiInovasi() { await kolaborasiState.update.submit(); toast.success("Kolaborasi inovasi berhasil diperbarui!"); - router.push("/admin/inovasi/kolaborasi-inovasi"); + router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"); } catch (error) { console.error("Error updating kolaborasi:", error); toast.error("Terjadi kesalahan saat memperbarui data"); + } finally { + setIsSubmitting(false); } }; @@ -87,13 +135,11 @@ function EditKolaborasiInovasi() { }; return ( - + - - - + Edit Kolaborasi Inovasi @@ -124,12 +170,15 @@ function EditKolaborasiInovasi() { required /> - handleChange("tahun", e.target.value)} - required + Tahun} + placeholder="Pilih tahun" + onChange={(date) => { + const year = date ? new Date(date).getFullYear() : 0; + handleChange("tahun", year.toString()); + }} /> + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx index 0d3b0f9a..3534971a 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -41,7 +41,7 @@ function DetailKolaborasiInovasi() { const data = kolaborasiState.findUnique.data; return ( - + {/* Tombol kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx index 8ed78d71..06179a19 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create/page.tsx @@ -3,17 +3,34 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { YearPickerInput } from '@mantine/dates'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateProgramKreatifDesa() { const stateCreate = useProxy(kolaborasiInovasiState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.slug?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; const resetForm = () => { stateCreate.create.form = { @@ -40,6 +57,7 @@ function CreateProgramKreatifDesa() { const handleSubmit = async () => { try { + setIsSubmitting(true); await stateCreate.create.create(); resetForm(); router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"); @@ -47,18 +65,18 @@ function CreateProgramKreatifDesa() { } catch (error) { console.error("Error creating kolaborasi inovasi:", error); toast.error("Terjadi kesalahan saat menyimpan data"); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Back Button */} - - - + Tambah Kolaborasi Inovasi @@ -77,11 +95,18 @@ function CreateProgramKreatifDesa() { Nama Kolaborasi Inovasi} placeholder="Masukkan nama kolaborasi inovasi" - defaultValue={stateCreate.create.form.name || ''} + value={stateCreate.create.form.name || ''} onChange={(val) => stateCreate.create.form.name = val.target.value} required /> + Deskripsi Singkat} + placeholder="Masukkan deskripsi singkat" + value={stateCreate.create.form.slug || ''} + onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value} + /> + + Kolaborator} + placeholder="Masukkan kolaborator" + value={stateCreate.create.form.kolaborator || ''} + onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value} + /> + Deskripsi @@ -101,27 +133,34 @@ function CreateProgramKreatifDesa() { value={stateCreate.create.form.deskripsi} onChange={(val) => stateCreate.create.form.deskripsi = val} /> - - - Kolaborator} - placeholder="Masukkan kolaborator" - defaultValue={stateCreate.create.form.kolaborator || ''} - onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value} - /> + + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx index f0899d36..d12d2ccd 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/page.tsx @@ -17,15 +17,15 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; -import HeaderSearch from '../../../_com/header'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; function KolaborasiInovasi() { const [search, setSearch] = useState(''); @@ -46,71 +46,105 @@ function KolaborasiInovasi() { function ListKolaborasiInovasi({ search }: { search: string }) { const listState = useProxy(kolaborasiInovasiState); const router = useRouter(); - + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, loading, page, totalPages, load } = listState.findMany; useEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } + return ( - - - - Daftar Kolaborasi Inovasi - - - + + + + + Daftar Kolaborasi Inovasi + + - - + + {/* Desktop Table */} + +
- No - Nama Kolaborasi Inovasi - Tahun - Deskripsi Singkat - Aksi + + + No + + + + + Nama Kolaborasi Inovasi + + + + + Tahun + + + + + Deskripsi Singkat + + + + + Aksi + + {filteredData.length > 0 ? ( filteredData.map((item, index) => ( - {index + 1} - - - {item.name} + + + {index + 1} - + + {item.name} + + + + {item.tahun} - - {item.slug} - + - +
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + + + + No + + + {index + 1} + + + + + Nama Kolaborasi Inovasi + + + {item.name} + + + + + Tahun + + + {item.tahun} + + + + + Deskripsi Singkat + + + + + + + + + )) + ) : ( +
+ + Tidak ada data kolaborasi inovasi yang tersedia + +
+ )} +
+
-
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
+ +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt={{ base: 'sm', md: 'md' }} + mb={{ base: 'sm', md: 'md' }} + color="blue" + radius="md" + /> +
); } -export default KolaborasiInovasi; +export default KolaborasiInovasi; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx index a1762db1..9e592d72 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/[id]/page.tsx @@ -4,17 +4,18 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Center, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -36,6 +37,7 @@ function EditMitraKolaborasi() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); // Local form state (controlled) const [formData, setFormData] = useState({ @@ -43,6 +45,17 @@ function EditMitraKolaborasi() { imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + imageId: '', + imageUrl: '', + }); + + // Check if form is valid + const isFormValid = () => { + return formData.name?.trim() !== ''; + }; + // Load data ke state lokal sekali saja useEffect(() => { const loadData = async () => { @@ -56,6 +69,11 @@ function EditMitraKolaborasi() { name: data.name || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) { setPreviewImage(data.image.link); @@ -77,8 +95,19 @@ function EditMitraKolaborasi() { })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // upload file jika ada let imageId = formData.imageId; if (file) { @@ -106,18 +135,18 @@ function EditMitraKolaborasi() { } catch (error) { console.error('Error updating mitra:', error); toast.error('Terjadi kesalahan saat memperbarui mitra'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Mitra @@ -157,7 +186,7 @@ function EditMitraKolaborasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -176,7 +205,7 @@ function EditMitraKolaborasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
@@ -184,7 +213,7 @@ function EditMitraKolaborasi() { {/* Preview Foto */} {previewImage ? ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : (
@@ -206,17 +253,31 @@ function EditMitraKolaborasi() { {/* Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx index d7da8e8b..ba4c52aa 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create/page.tsx @@ -3,16 +3,17 @@ import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolabo import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -26,6 +27,15 @@ function CreateMitraKolaborasi() { const router = useRouter(); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + state.create.form.name?.trim() !== '' && + file !== null + ); + }; const resetForm = () => { state.create.form = { @@ -37,35 +47,41 @@ function CreateMitraKolaborasi() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + state.create.form.imageId = uploaded.id; + await state.create.create(); + resetForm(); + router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); + } catch (error) { + console.error("Error creating mitra kolaborasi:", error); + toast.error("Terjadi kesalahan saat menambahkan mitra kolaborasi"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - state.create.form.imageId = uploaded.id; - await state.create.create(); - resetForm(); - router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi'); }; return ( - + {/* Back Button + Title */} - - - + Tambah Mitra Kolaborasi @@ -85,7 +101,7 @@ function CreateMitraKolaborasi() { (state.create.form.name = e.target.value)} required /> @@ -105,7 +121,7 @@ function CreateMitraKolaborasi() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -126,7 +142,7 @@ function CreateMitraKolaborasi() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx index a2d6cbbb..cb47b63d 100644 --- a/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/kolaborasi-inovasi/mitra-kolaborasi/page.tsx @@ -19,15 +19,15 @@ import { TableTr, Text, Title, - Tooltip, } from '@mantine/core'; -import { IconEdit, IconSearch, IconX, IconPlus } from '@tabler/icons-react'; +import { useDebouncedValue } from '@mantine/hooks'; +import { IconEdit, IconPlus, IconSearch, IconTrash, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; -import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; function MitraKolaborasi() { const [search, setSearch] = useState(''); @@ -56,126 +56,167 @@ function ListMitraKolaborasi({ search }: { search: string }) { mitraKolaborasi.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push('/admin/inovasi/kolaborasi-inovasi'); } }; + const [debouncedSearch] = useDebouncedValue(search, 1000); + const { data, loading, page, totalPages, load } = listState.findMany; useEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - + - - Daftar Mitra Kolaborasi - - - + + + Daftar Mitra Kolaborasi + + - - + + {/* Desktop Table */} + +
- No - Nama Mitra - Image - Delete - Edit + + + No + + + + + Nama Mitra + + + + + Image + + + + + Delete + + + + + Edit + + {filteredData.length > 0 ? ( filteredData.map((item, index) => ( - {index + 1} - + + {index + 1} + + + + {item.name} - + {item.image?.link ? ( ) : ( - + )} - - - + - - - + )) ) : ( -
- +
+ Tidak ada data mitra kolaborasi yang tersedia
@@ -185,7 +226,96 @@ function ListMitraKolaborasi({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + + + + No + + + {index + 1} + + + + + Nama Mitra + + + {item.name} + + + + + Image + + + {item.image?.link ? ( + {item.name} + ) : ( + + )} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data mitra kolaborasi yang tersedia + +
+ )} +
+
+
- {/* Modal Konfirmasi Hapus */} setModalHapus(false)} @@ -212,4 +341,4 @@ function ListMitraKolaborasi({ search }: { search: string }) { ); } -export default MitraKolaborasi; +export default MitraKolaborasi; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx index 55a8cdb2..cddf9e8e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/_lib/layoutTabs.tsx @@ -2,23 +2,23 @@ 'use client' import colors from '@/con/colors'; import { + Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, - Title, - Tooltip + Title } from '@mantine/core'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; import { + IconAlertCircle, IconFileText, IconListDetails, - IconMessage, - IconAlertCircle + IconMessage } from '@tabler/icons-react'; +import { usePathname, useRouter } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -30,29 +30,25 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode } label: "Administrasi Online", value: "administrasionline", href: "/admin/inovasi/layanan-online-desa/administrasi-online", - icon: , - tooltip: "Kelola administrasi online desa" + icon: }, { label: "Jenis Layanan", value: "jenislayanan", href: "/admin/inovasi/layanan-online-desa/jenis-layanan", - icon: , - tooltip: "Daftar jenis layanan desa" + icon: }, { label: "Pengaduan Masyarakat", value: "pengaduanmasyarakat", href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat", - icon: , - tooltip: "Laporan pengaduan masyarakat" + icon: }, { label: "Jenis Pengaduan", value: "jenispengaduan", href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan", - icon: , - tooltip: "Kategori/jenis pengaduan masyarakat" + icon: } ]; @@ -89,43 +85,76 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode } keepMounted={false} > {/* ✅ Scroll horizontal biar gak overflow */} - - - {tabs.map((tab, i) => ( - - - {tab.label} - - - ))} - - - + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( - + + {/* Tombol Kembali */} - + {/* Konten Detail */} { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - Daftar Administrasi Online + + + + Daftar Administrasi Online + - - + {/* Desktop Table */} + +
- Nama - Alamat - Nomor Telepon - Aksi + + Nama + + + Alamat + + + Nomor Telepon + + + Aksi + @@ -81,17 +100,17 @@ function ListAdministrasiOnline({ search }: { search: string }) { filteredData.map((item) => ( - + {item.name} - + {item.alamat} - + {item.nomorTelepon || '-'} @@ -116,8 +135,10 @@ function ListAdministrasiOnline({ search }: { search: string }) { ) : ( -
- Tidak ada data administrasi online yang cocok +
+ + Tidak ada data administrasi online yang cocok +
@@ -125,6 +146,55 @@ function ListAdministrasiOnline({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + {item.name} + + + Alamat + {item.alamat} + + + Nomor Telepon + {item.nomorTelepon || '-'} + + + + + + + )) + ) : ( +
+ + Tidak ada data administrasi online yang cocok + +
+ )} +
+
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx index 80459e66..189b40cb 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/edit/page.tsx @@ -7,12 +7,12 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -24,6 +24,12 @@ function EditJenisLayanan() { const state = useProxy(layananonlineDesa.jenisLayanan); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: '', + deskripsi: '', + }); const [formData, setFormData] = useState({ nama: '', @@ -42,6 +48,10 @@ function EditJenisLayanan() { nama: data.nama ?? '', deskripsi: data.deskripsi ?? '', }); + setOriginalData({ + nama: data.nama ?? '', + deskripsi: data.deskripsi ?? '', + }); } } catch (error) { console.error('Error loading jenis layanan:', error); @@ -52,8 +62,17 @@ function EditJenisLayanan() { loadJenisLayanan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + deskripsi: originalData.deskripsi, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); state.edit.form = { ...state.edit.form, ...formData, @@ -65,23 +84,23 @@ function EditJenisLayanan() { } catch (error) { console.error('Error updating jenis layanan:', error); toast.error('Terjadi kesalahan saat memperbarui jenis layanan'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Jenis Layanan @@ -126,6 +145,17 @@ function EditJenisLayanan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx index 41990ab2..f6fa5bb2 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -40,7 +40,7 @@ function DetailJenisLayanan() { const data = state.findUnique.data return ( - + {/* Tombol kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx index b1b5a107..e705115e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/create/page.tsx @@ -6,21 +6,23 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJenisLayanan() { const router = useRouter(); const statePasar = useProxy(layananonlineDesa.jenisLayanan); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { statePasar.findMany.load(); @@ -34,25 +36,31 @@ function CreateJenisLayanan() { }; const handleSubmit = async () => { - await statePasar.create.create(); - resetForm(); - router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); + try { + setIsSubmitting(true); + await statePasar.create.create(); + resetForm(); + router.push('/admin/inovasi/layanan-online-desa/jenis-layanan'); + } catch (error) { + console.error('Error creating jenis layanan:', error); + toast.error('Terjadi kesalahan saat menambahkan jenis layanan'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header dengan tombol back */} - - - + Tambah Jenis Layanan @@ -69,7 +77,7 @@ function CreateJenisLayanan() { > { statePasar.create.form.nama = val.target.value; }} @@ -78,7 +86,7 @@ function CreateJenisLayanan() { required /> { statePasar.create.form.deskripsi = val.target.value; }} @@ -88,6 +96,17 @@ function CreateJenisLayanan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx index ba176cd6..c9fca5da 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-layanan/page.tsx @@ -17,9 +17,9 @@ import { TableTr, Text, Title, - Tooltip + useMantineTheme } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -46,50 +46,76 @@ function JenisLayanan() { function ListJenisLayanan({ search }: { search: string }) { const stateList = useProxy(layananonlineDesa.jenisLayanan); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = stateList.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; + const theme = useMantineTheme(); if (loading || !data) { return ( - + ); } return ( - + - Daftar Jenis Layanan - - - + + Daftar Jenis Layanan + + - - + + {/* Desktop Table */} + +
- Nama Jenis Layanan - Deskripsi - Aksi + + + Nama Jenis Layanan + + + + + Deskripsi + + + + + Aksi + + @@ -97,16 +123,28 @@ function ListJenisLayanan({ search }: { search: string }) { filteredData.map((item) => ( - + {item.nama} - - - {item.deskripsi || '-'} - + + - +
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Jenis Layanan + + + {item.nama} + + + + + Deskripsi + + + + + + + + + )) + ) : ( +
+ + Tidak ada jenis layanan yang cocok + +
+ )} +
+
-
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
+ +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + color="blue" + radius="md" + /> +
+
); } diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx index c64bb102..a4917449 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/[id]/page.tsx @@ -6,11 +6,11 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -23,11 +23,16 @@ function EditJenisPengaduan() { const params = useParams(); const id = params?.id as string; const state = useProxy(layananonlineDesa.jenisPengaduan); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ nama: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + }); + // Load data sekali aja useEffect(() => { const loadJenisPengaduan = async () => { @@ -41,6 +46,9 @@ function EditJenisPengaduan() { setFormData({ nama: data.nama || '', }); + setOriginalData({ + nama: data.nama || '', + }); } } catch (error) { console.error('Error loading jenis pengaduan:', error); @@ -58,6 +66,13 @@ function EditJenisPengaduan() { })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { if (!formData.nama.trim()) { toast.error('Nama jenis pengaduan tidak boleh kosong'); @@ -75,6 +90,7 @@ function EditJenisPengaduan() { } try { + setIsSubmitting(true); const success = await state.edit.update(); if (success) { @@ -83,23 +99,23 @@ function EditJenisPengaduan() { } catch (error) { console.error('Error updating jenis pengaduan:', error); // toast ditangani di dalam state.update + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Jenis Pengaduan @@ -124,6 +140,17 @@ function EditJenisPengaduan() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx index bb0ae5b9..00b3f210 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/create/page.tsx @@ -6,21 +6,22 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJenisPengaduan() { const router = useRouter(); const state = useProxy(layananonlineDesa.jenisPengaduan); - + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { state.findMany.load(); }, []); @@ -32,20 +33,26 @@ function CreateJenisPengaduan() { }; const handleSubmit = async () => { - await state.create.create(); - resetForm(); - router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); + try { + setIsSubmitting(true); + await state.create.create(); + resetForm(); + router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan'); + } catch (error) { + console.error('Error creating jenis pengaduan:', error); + toast.error('Terjadi kesalahan saat menambahkan jenis pengaduan'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + Tambah Jenis Pengaduan @@ -64,12 +71,23 @@ function CreateJenisPengaduan() { (state.create.form.nama = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx index 4bbee3b6..eae122a3 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/jenis-pengaduan/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -49,12 +48,13 @@ function ListJenisPengaduan({ search }: { search: string }) { const router = useRouter(); const [modalHapus, setModalHapus] = useState(false); const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); - const { data, page, totalPages, loading, load, } = state.findMany; + const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const handleHapus = async () => { if (selectedId) { @@ -64,44 +64,64 @@ function ListJenisPengaduan({ search }: { search: string }) { } }; - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - - Daftar Jenis Pengaduan - - - + + + + + Daftar Jenis Pengaduan + + - - + {/* Desktop Table */} + +
- Nama Jenis Pengaduan - Edit - Hapus + + + Nama Jenis Pengaduan + + + + + Edit + + + + + Hapus + + @@ -109,7 +129,7 @@ function ListJenisPengaduan({ search }: { search: string }) { filteredData.map((item) => ( - + {item.nama} @@ -150,7 +170,7 @@ function ListJenisPengaduan({ search }: { search: string }) {
- + Tidak ada jenis pengaduan yang cocok
@@ -160,6 +180,63 @@ function ListJenisPengaduan({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Jenis Pengaduan + + + {item.nama} + + + + + + + + + )) + ) : ( +
+ + Tidak ada jenis pengaduan yang cocok + +
+ )} +
+
@@ -170,14 +247,13 @@ function ListJenisPengaduan({ search }: { search: string }) { window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} - mt="md" - mb="md" + mt={{ base: 'sm', md: 'md' }} + mb={{ base: 'sm', md: 'md' }} color="blue" radius="md" />
- {/* Modal Hapus */} setModalHapus(false)} @@ -188,4 +264,4 @@ function ListJenisPengaduan({ search }: { search: string }) { ); } -export default JenisPengaduan; +export default JenisPengaduan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx index dcb53527..dcecdd2e 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/layout.tsx @@ -1,7 +1,28 @@ 'use client' +import { Box } from "@mantine/core"; import LayoutTabsLayananOnlineDesa from "./_lib/layoutTabs"; +import { usePathname } from "next/navigation"; function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + return ( {children} diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx index a9a9ad3e..070d6d11 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; @@ -41,7 +41,7 @@ function DetailPengaduanMasyarakat() { const data = pengaduanState.pengaduanMasyarakat.findUnique.data; return ( - + {/* Tombol Kembali */} - +
diff --git a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index 11140fff..9468eec8 100644 --- a/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -16,12 +16,10 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; - -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; @@ -47,6 +45,7 @@ function PengaduanMasyarakat() { function ListPengaduanMasyarakat({ search }: { search: string }) { const listState = useProxy(layananonlineDesa.pengaduanMasyarakat); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, @@ -56,69 +55,97 @@ function ListPengaduanMasyarakat({ search }: { search: string }) { } = listState.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - - Daftar Pengaduan Masyarakat + + + {/* Section Header */} + + + Daftar Pengaduan Masyarakat + - - + + {/* Desktop Table */} + +
- Nama - Email - Nomor Telepon - Aksi + + Nama + + + Email + + + Nomor Telepon + + + Aksi + {filteredData.length > 0 ? ( filteredData.map((item) => ( - - {item.name} + + + {item.name} + - - {item.email} + + + {item.email} + - - {item.nomorTelepon} + + + {item.nomorTelepon} + - - - - + + )) ) : ( -
- Tidak ada data pengaduan yang cocok +
+ + Tidak ada data pengaduan yang cocok +
@@ -126,7 +153,56 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + {item.name} + + + Email + {item.email} + + + Nomor Telepon + {item.nomorTelepon} + + + + + + + )) + ) : ( +
+ + Tidak ada data pengaduan yang cocok + +
+ )} +
+
+ + {/* Pagination */}
({ name: '', @@ -41,6 +42,32 @@ function EditProgramKreatifDesa() { icon: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + slug: '', + icon: '', + }); + + const [isDataChanged, setIsDataChanged] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.slug?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + formData.icon?.trim() !== '' + ); + }; + // Load data hanya sekali berdasarkan params.id useEffect(() => { const loadProgramKreatif = async () => { @@ -51,12 +78,14 @@ function EditProgramKreatifDesa() { const data = await stateProgramKreatif.update.load(id); if (data) { stateProgramKreatif.update.id = id; - setFormData({ + const loadedData = { name: data.name || '', slug: data.slug || '', deskripsi: data.deskripsi || '', icon: data.icon || '', - }); + }; + setFormData(loadedData); + setOriginalData(loadedData); } } catch (error) { console.error('Error loading program kreatif:', error); @@ -67,14 +96,65 @@ function EditProgramKreatifDesa() { loadProgramKreatif(); }, [params?.id]); + // Deteksi perubahan data + useEffect(() => { + const hasChanged = + formData.name !== originalData.name || + formData.slug !== originalData.slug || + formData.deskripsi !== originalData.deskripsi || + formData.icon !== originalData.icon; + + setIsDataChanged(hasChanged); + }, [formData, originalData]); + + // Prevent browser back/refresh jika ada perubahan + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isDataChanged) { + e.preventDefault(); + e.returnValue = ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [isDataChanged]); + const handleChange = (field: keyof FormProgramKreatif) => (value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleBackClick = () => { + if (isDataChanged) { + const confirmed = window.confirm( + 'Anda memiliki perubahan yang belum disimpan. Apakah Anda yakin ingin keluar dari halaman ini? Semua perubahan akan hilang.' + ); + if (confirmed) { + router.back(); + } + } else { + router.back(); + } + }; + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + slug: originalData.slug, + deskripsi: originalData.deskripsi, + icon: originalData.icon, + }); + setOriginalData({ + ...originalData, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateProgramKreatif.update.form = { name: formData.name.trim(), deskripsi: formData.deskripsi.trim(), @@ -82,26 +162,31 @@ function EditProgramKreatifDesa() { icon: formData.icon.trim(), }; await stateProgramKreatif.update.submit(); + + // Reset isDataChanged agar tidak muncul konfirmasi setelah save + setOriginalData(formData); + setIsDataChanged(false); + router.push('/admin/inovasi/program-kreatif-desa'); } catch (error) { console.error('Error updating program kreatif:', error); toast.error('Gagal menyimpan program kreatif'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Program Kreatif Desa @@ -153,17 +238,31 @@ function EditProgramKreatifDesa() { + + + {/* Tombol Simpan */} @@ -172,4 +271,4 @@ function EditProgramKreatifDesa() { ); } -export default EditProgramKreatifDesa; +export default EditProgramKreatifDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx index e402bf15..1cf3342d 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -41,7 +41,7 @@ function DetailProgramKreatifDesa() { const data = stateProgramKreatif.findUnique.data return ( - + - + - - - + diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx index b5969bf7..a70ce546 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/create/page.tsx @@ -4,23 +4,43 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; -import programKreatifState from '../../../_state/inovasi/program-kreatif'; import SelectIconProgram from '../../../_com/selectIcon'; +import programKreatifState from '../../../_state/inovasi/program-kreatif'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; function CreateProgramKreatifDesa() { const stateCreate = useProxy(programKreatifState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateCreate.create.form.name?.trim() !== '' && + stateCreate.create.form.icon?.trim() !== '' && + stateCreate.create.form.slug?.trim() !== '' && + !isHtmlEmpty(stateCreate.create.form.deskripsi) + ); + }; const resetForm = () => { stateCreate.create.form = { @@ -32,20 +52,29 @@ function CreateProgramKreatifDesa() { }; const handleSubmit = async () => { - await stateCreate.create.create(); - resetForm(); - router.push("/admin/inovasi/program-kreatif-desa"); + try { + const success = await stateCreate.create.create(); + + if (success) { + resetForm(); + router.push("/admin/inovasi/program-kreatif-desa"); + } + } catch (error) { + console.error("Error creating program kreatif desa:", error); + toast.error("Gagal menambahkan program kreatif desa"); + } finally { + setIsSubmitting(false); + } }; + return ( - + {/* Tombol kembali */} - - - + Tambah Program Kreatif Desa @@ -64,7 +93,7 @@ function CreateProgramKreatifDesa() { Nama Program Kreatif Desa} placeholder="Masukkan nama program kreatif desa" - defaultValue={stateCreate.create.form.name || ""} + value={stateCreate.create.form.name || ""} onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)} required /> @@ -81,7 +110,7 @@ function CreateProgramKreatifDesa() { Deskripsi Singkat Program Kreatif Desa} placeholder="Masukkan deskripsi singkat program kreatif desa" - defaultValue={stateCreate.create.form.slug || ""} + value={stateCreate.create.form.slug || ""} onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)} required /> @@ -100,17 +129,31 @@ function CreateProgramKreatifDesa() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx index 5ead6694..89e7140f 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/page.tsx @@ -19,32 +19,48 @@ import { TableTr, Text, Title, - Tooltip } from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; import { + IconAlertTriangle, + IconAmbulance, + IconBook, + IconBuilding, + IconBuildingCommunity, IconCash, IconChartLine, IconChristmasTreeFilled, - IconClipboard, + IconClipboardTextFilled, IconDeviceImac, IconDroplet, + IconFileText, + IconFiretruck, + IconFirstAidKit, IconHome, IconHomeEco, IconHospital, + IconInfoCircle, IconLeaf, + IconLifebuoy, + IconMessageReport, + IconPhoneCall, IconPlus, IconRecycle, + IconRun, IconScale, IconSchool, IconSearch, + IconShield, IconShieldFilled, IconShoppingCart, + IconStethoscope, IconTent, - IconTrash, + IconTrashFilled, IconTree, IconTrendingUp, IconTrophy, - IconTruck, + IconTruckFilled, + IconUsers } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; @@ -53,7 +69,7 @@ import HeaderSearch from '../../_com/header'; import programKreatifState from '../../_state/inovasi/program-kreatif'; function ProgramKreatifDesa() { - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(''); return ( { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; const iconMap: Record> = { + // ===== Umum & Lingkungan ===== ekowisata: IconLeaf, kompetisi: IconTrophy, wisata: IconTent, ekonomi: IconChartLine, sampah: IconRecycle, - truck: IconTruck, + truck: IconTruckFilled, scale: IconScale, - clipboard: IconClipboard, - trash: IconTrash, + clipboard: IconClipboardTextFilled, + trash: IconTrashFilled, lingkunganSehat: IconHomeEco, sumberOksigen: IconChristmasTreeFilled, ekonomiBerkelanjutan: IconTrendingUp, @@ -100,11 +118,35 @@ function ListProgramKreatifDesa({ search }: { search: string }) { pelatihan: IconSchool, subsidi: IconShoppingCart, layananKesehatan: IconHospital, + + // ===== Keamanan & Darurat ===== + polisi: IconShieldFilled, + ambulans: IconAmbulance, + pemadam: IconFiretruck, + darurat: IconAlertTriangle, + sar: IconLifebuoy, + evakuasi: IconRun, + keamanan: IconShield, + teleponDarurat: IconPhoneCall, + + // ===== Kesehatan ===== + rumahSakit: IconHospital, + puskesmas: IconFirstAidKit, + klinik: IconStethoscope, + + // ===== Pemerintahan & Fasilitas ===== + bangunan: IconBuilding, + kantorDesa: IconBuildingCommunity, + administrasi: IconFileText, + informasi: IconInfoCircle, + pengaduan: IconMessageReport, + layananPublik: IconUsers, + book: IconBook }; if (loading || !data) { return ( - + ); @@ -112,37 +154,44 @@ function ListProgramKreatifDesa({ search }: { search: string }) { if (data.length === 0) { return ( - - - - - Daftar Program Kreatif Desa - - - + + + + + + Daftar Program Kreatif Desa + + - - - - No - Nama Program Kreatif Desa - Deskripsi Singkat - Ikon - Detail - - -
+ + + + + + No + Nama Program Kreatif Desa + Deskripsi Singkat + Ikon + Detail + + +
+
+ Tidak ada data program kreatif desa yang tersedia @@ -153,7 +202,7 @@ function ListProgramKreatifDesa({ search }: { search: string }) { } return ( - + - - Daftar Program Kreatif Desa - - - + + + Daftar Program Kreatif Desa + + - - + + {/* Desktop Table */} + +
- No - - Nama Program Kreatif Desa + + + No + + + + + Nama Program Kreatif Desa + + + + + Deskripsi Singkat + + + + + Ikon + + + + + Detail + - Deskripsi Singkat - Ikon - Detail {filteredData.map((item, index) => ( - {index + 1} - - - {item.name} - + + {index + 1} - - - - {item.slug} - - + + + {item.name} + - + + + {item.slug} + + + {iconMap[item.icon] && ( {React.createElement(iconMap[item.icon], { size: 24 })} )} - + @@ -234,7 +308,72 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.map((item, index) => ( + + + + + No + + + {index + 1} + + + + + Nama Program Kreatif Desa + + + {item.name} + + + + + Deskripsi Singkat + + + {item.slug} + + + + + Ikon + + + {iconMap[item.icon] && ( + + {React.createElement(iconMap[item.icon], { size: 24 })} + + )} + + + + + + + + ))} + +
+
@@ -253,4 +392,4 @@ function ListProgramKreatifDesa({ search }: { search: string }) { ); } -export default ProgramKreatifDesa; +export default ProgramKreatifDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx index c1a7d88e..b23b011a 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/edit/page.tsx @@ -1,17 +1,18 @@ "use client"; import { + ActionIcon, Box, Button, Center, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; import { IconArrowBack, @@ -38,12 +39,35 @@ function EditKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: "", deskripsi: "", imageId: "", }); + const [originalData, setOriginalData] = useState({ + name: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data sekali pas mount useEffect(() => { const loadData = async () => { @@ -58,6 +82,12 @@ function EditKeamananLingkungan() { deskripsi: data.deskripsi || "", imageId: data.imageId || "", }); + setOriginalData({ + name: data.name || "", + deskripsi: data.deskripsi || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }); if (data?.image?.link) { setPreviewImage(data.image.link); @@ -77,8 +107,20 @@ function EditKeamananLingkungan() { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { @@ -109,23 +151,23 @@ function EditKeamananLingkungan() { } catch (error) { console.error("Error updating keamananLingkungan:", error); toast.error("Terjadi kesalahan saat memperbarui keamananLingkungan"); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Keamanan Lingkungan @@ -193,7 +235,39 @@ function EditKeamananLingkungan() { {previewImage ? ( - + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + ) : (
@@ -219,17 +293,32 @@ function EditKeamananLingkungan() { + {/* Tombol Batal */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx index 1ce73db9..34feff31 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -44,7 +44,7 @@ function DetailKeamananLingkungan() { const data = keamananState.findUnique.data return ( - + {/* Tombol Back */} - + - - - + diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx index 4ff05394..6b977006 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -32,6 +33,23 @@ function CreateKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false) + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + keamananState.create.form.name?.trim() !== '' && + !isHtmlEmpty(keamananState.create.form.deskripsi) && + file !== null + ); + }; const resetForm = () => { keamananState.create.form = { @@ -44,43 +62,49 @@ function CreateKeamananLingkungan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true) + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengupload file'); + } + + keamananState.create.form.imageId = uploaded.id; + + await keamananState.create.create(); + + resetForm(); + router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan data keamanan lingkungan'); + } finally { + setIsSubmitting(false) } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengupload file'); - } - - keamananState.create.form.imageId = uploaded.id; - - await keamananState.create.create(); - - resetForm(); - router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal'); }; return ( - + {/* Header */} - - - + Tambah Data Keamanan Lingkungan @@ -111,7 +135,7 @@ function CreateKeamananLingkungan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Input Nama */} { keamananState.create.form.name = val.target.value; }} @@ -196,17 +238,31 @@ function CreateKeamananLingkungan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx index 8fde1a33..0477697a 100644 --- a/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/keamanan-lingkungan-pecalang-patwal/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -49,6 +48,7 @@ function KeamananLingkungan() { function ListKeamananLingkungan({ search }: { search: string }) { const keamananState = useProxy(keamananLingkunganState) const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000) const { data, @@ -59,47 +59,53 @@ function ListKeamananLingkungan({ search }: { search: string }) { } = keamananState.findMany; useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch) + }, [page, debouncedSearch]) const filteredData = data || [] if (loading || !data) { return ( - + ) } return ( - - + + {/* Judul + Tombol Tambah */} - - Daftar Keamanan Lingkungan - - - + + + Daftar Keamanan Lingkungan + + - {/* Tabel */} - - + {/* Desktop Table */} + +
Nama Deskripsi - Aksi + + Aksi + @@ -107,25 +113,32 @@ function ListKeamananLingkungan({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - - - + + - + @@ -133,8 +146,8 @@ function ListKeamananLingkungan({ search }: { search: string }) { ) : ( -
- +
+ Tidak ada data keamanan lingkungan yang cocok
@@ -144,6 +157,49 @@ function ListKeamananLingkungan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + {item.name} + + + Deskripsi + + + + + + + + + + )) + ) : ( +
+ + Tidak ada data keamanan lingkungan yang cocok + +
+ )} +
+
{/* Pagination */} @@ -155,8 +211,8 @@ function ListKeamananLingkungan({ search }: { search: string }) { window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} - mt="md" - mb="md" + mt={{ base: 'sm', md: 'md' }} + mb={{ base: 'sm', md: 'md' }} color="blue" radius="md" /> @@ -165,4 +221,4 @@ function ListKeamananLingkungan({ search }: { search: string }) { ); } -export default KeamananLingkungan; +export default KeamananLingkungan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx index 17c298f4..d18e3c92 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { IconPhone, IconTag } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; @@ -15,15 +15,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Kontak Darurat Keamanan", value: "kontak-darurat-keamanan", href: "/admin/keamanan/kontak-darurat/kontak-darurat-keamanan", - icon: , - tooltip: "Lihat dan kelola kontak darurat keamanan", + icon: }, { label: "Kontak Darurat Item", value: "kontak-darurat-item", href: "/admin/keamanan/kontak-darurat/kontak-darurat-item", - icon: , - tooltip: "Kelola data kontak darurat item", + icon: } ]; @@ -59,36 +57,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper */} - - - {tabs.map((tab, i) => ( - + + + + {tabs.map((tab, i) => ( {tab.label} - - ))} - - + ))} + + +
+ + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( { + return ( + formData.name?.trim() !== '' && + formData.nomorTelepon?.trim() !== '' && + formData.icon?.trim() !== '' + ); + }; + // Load data sekali dari global state useEffect(() => { const loadKontakDarurat = async () => { @@ -46,6 +62,11 @@ function EditKontakItem() { nomorTelepon: data.nomorTelepon || '', icon: data.icon || '', }); + setOriginalData({ + name: data.nama || '', + nomorTelepon: data.nomorTelepon || '', + icon: data.icon || '', + }); } } catch (error) { console.error('Error loading kontak darurat:', error); @@ -63,8 +84,18 @@ function EditKontakItem() { })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + nomorTelepon: originalData.nomorTelepon, + icon: originalData.icon, + }); + toast.info('Form dikembalikan ke data awal'); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); // Update global state sekali pas submit kontakState.update.form = { ...kontakState.update.form, @@ -79,18 +110,18 @@ function EditKontakItem() { } catch (error) { console.error('Error updating kontak darurat:', error); toast.error('Terjadi kesalahan saat memperbarui kontak darurat'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Kontak Darurat Item @@ -133,17 +164,31 @@ function EditKontakItem() { + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx index 93c858cd..dd92b74d 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/page.tsx @@ -3,7 +3,7 @@ import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -41,7 +41,7 @@ function DetailKontakDarurat() { const data = kontakState.findUnique.data; return ( - + {/* Tombol Back */} - + - - - +
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx index 3b962470..ea0ac7fe 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/create/page.tsx @@ -6,20 +6,33 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKontakItem() { const kontakState = useProxy(kontakDarurat.kontakDaruratItem); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + kontakState.create.form.nama?.trim() !== '' && + kontakState.create.form.nomorTelepon?.trim() !== '' && + kontakState.create.form.icon?.trim() !== '' + ); + }; + const resetForm = () => { kontakState.create.form = { nama: '', @@ -29,25 +42,31 @@ function CreateKontakItem() { }; const handleSubmit = async () => { - await kontakState.create.create(); - resetForm(); - router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item'); + try { + setIsSubmitting(true); + await kontakState.create.create(); + resetForm(); + router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item'); + } catch (error) { + console.error("Error creating kontak darurat item:", error); + toast.error("Gagal menambahkan kontak darurat item"); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + Tambah Kontak Darurat Item @@ -65,7 +84,7 @@ function CreateKontakItem() { {/* Input Nama Kategori */} { kontakState.create.form.nama = val.target.value; }} @@ -77,7 +96,7 @@ function CreateKontakItem() { Nomor Telepon Kontak} placeholder="Masukkan nomor telepon" - defaultValue={kontakState.create.form.nomorTelepon} + value={kontakState.create.form.nomorTelepon} onChange={(val) => { kontakState.create.form.nomorTelepon = val.target.value; }} @@ -91,17 +110,31 @@ function CreateKontakItem() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx index dab73629..4aaa5784 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -27,7 +26,6 @@ import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import kontakDarurat from '../../../_state/keamanan/kontak-darurat-keamanan'; - function KontakItem() { const [search, setSearch] = useState(""); @@ -50,6 +48,7 @@ function KontakItem() { function ListKontakItem({ search }: { search: string }) { const kontakState = useProxy(kontakDarurat.kontakDaruratItem); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -60,45 +59,54 @@ function ListKontakItem({ search }: { search: string }) { } = kontakState.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - + + {/* Judul + Tombol Tambah */} - - Daftar Kontak Darurat Item - - - + + + Daftar Kontak Darurat Item + + - {/* Tabel */} - - + {/* Desktop: Table */} + +
- Nama Kontak - Nomor Telepon - Aksi + Nama Kontak + Nomor Telepon + Aksi @@ -106,12 +114,12 @@ function ListKontakItem({ search }: { search: string }) { filteredData.map((item) => ( - + {item.nama} - + {item.nomorTelepon || "-"} @@ -119,19 +127,26 @@ function ListKontakItem({ search }: { search: string }) { )) ) : ( - -
- + +
+ Tidak ada data kontak darurat item yang cocok
@@ -141,6 +156,58 @@ function ListKontakItem({ search }: { search: string }) {
+ + {/* Mobile: Card */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Kontak + + + {item.nama} + + + + + Nomor Telepon + + + {item.nomorTelepon || "-"} + + + + + + + + )) + ) : ( +
+ + Tidak ada data kontak darurat item yang cocok + +
+ )} +
{/* Pagination */} @@ -162,4 +229,4 @@ function ListKontakItem({ search }: { search: string }) { ); } -export default KontakItem; +export default KontakItem; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx index 7492194c..de6ac436 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/edit/page.tsx @@ -1,73 +1,133 @@ /* eslint-disable react-hooks/exhaustive-deps */ -"use client"; +'use client'; -import { IconKey } from "@/app/admin/(dashboard)/_com/iconMap"; -import SelectIconProgramEdit from "@/app/admin/(dashboard)/_com/selectIconEdit"; -import kontakDarurat from "@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan"; -import colors from "@/con/colors"; +import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap'; +import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; +import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; +import colors from '@/con/colors'; import { Box, Button, Group, + Loader, MultiSelect, Paper, Stack, Text, TextInput, Title, - Tooltip, -} from "@mantine/core"; -import { IconArrowBack } from "@tabler/icons-react"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import { toast } from "react-toastify"; -import { useProxy } from "valtio/utils"; +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; -function EditKontakDaruratKeamanan() { +type FormData = { + name: string; + icon: IconKey | ''; + kategoriId: string[]; +}; + +export default function EditKontakDaruratKeamanan() { const router = useRouter(); - const params = useParams(); + const params = useParams<{ id: string }>(); const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); - const [isLoading, setIsLoading] = useState(true); - // Remove the dependency on data in the initial state - const [formData, setFormData] = useState({ - name: "", - icon: "" as IconKey | "", - kategoriId: [] as string[], // Initialize as empty array + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + icon: '', + kategoriId: [], + }); + const [originalData, setOriginalData] = useState({ + name: '', + icon: '', + kategoriId: [], }); - // Load data dari backend + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.kategoriId.length > 0 + ); + }; + + // 🔁 Load data saat ID berubah useEffect(() => { - const loadData = async () => { + const loadInitialData = async () => { + const id = params?.id; + if (!id) { + toast.error('ID tidak valid'); + router.push('/admin/keamanan/kontak-darurat'); + return; + } + try { - setIsLoading(true); + // Load dropdown kategori await kontakDarurat.kontakDaruratItem.findMany.load(); - - const id = params?.id as string; - if (id) { - const data = await kontakState.update.load(id); - if (data) { - setFormData({ - name: data.nama || "", - icon: (data.icon as IconKey) || "", - kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [], - }); - } + + // Load data kontak darurat + const data = await kontakState.update.load(id); + if (!data) { + toast.error('Data tidak ditemukan'); + router.push('/admin/keamanan/kontak-darurat'); + return; } - } catch (error) { - console.error("Error loading data:", error); - toast.error("Gagal memuat data"); - } finally { - setIsLoading(false); + + const initial: FormData = { + name: data.nama || '', + icon: (data.icon as IconKey) || '', + kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [], + }; + + setFormData(initial); + setOriginalData(initial); + } catch (err) { + console.error('Gagal memuat data:', err); + toast.error('Gagal memuat data kontak darurat'); + router.push('/admin/keamanan/kontak-darurat'); } }; - loadData(); + loadInitialData(); }, [params?.id]); - // Handle submit + // Debug: Log the kontakDaruratItem data + useEffect(() => { + if (kontakDarurat.kontakDaruratItem.findMany.data) { + console.log('Kontak Item Data:', kontakDarurat.kontakDaruratItem.findMany.data); + } + }, [kontakDarurat.kontakDaruratItem.findMany.data]); + + // 📝 Update field + const updateField = (field: K, value: FormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // ↩️ Reset ke data awal + const handleResetForm = () => { + setFormData(originalData); + toast.info('Form dikembalikan ke data awal'); + }; + + // 💾 Simpan const handleSubmit = async () => { + if (!formData.name.trim()) { + toast.error('Nama kontak darurat wajib diisi'); + return; + } + + if (formData.kategoriId.length === 0) { + toast.error('Pilih minimal satu kontak item'); + return; + } + try { + setIsSubmitting(true); + + // Sinkronisasi ke Valtio state (jika digunakan di `update`) kontakState.update.form = { ...kontakState.update.form, nama: formData.name, @@ -75,114 +135,106 @@ function EditKontakDaruratKeamanan() { kategoriId: formData.kategoriId, }; - await kontakState.update.update(); - toast.success("Kontak Darurat berhasil diperbarui!"); - router.push("/admin/keamanan/kontak-darurat"); + const success = await kontakState.update.update(); + if (success) { + toast.success('Kontak Darurat berhasil diperbarui!'); + router.push('/admin/keamanan/kontak-darurat'); + } } catch (error) { - console.error("Error updating kontak darurat:", error); - toast.error("Terjadi kesalahan saat memperbarui kontak darurat"); + console.error('Error saat menyimpan:', error); + toast.error('Terjadi kesalahan saat menyimpan data'); + } finally { + setIsSubmitting(false); } }; + // 📋 Daftar opsi kategori untuk MultiSelect + const kategoriOptions = (kontakDarurat.kontakDaruratItem.findMany.data || []).map((item: { id: string; nama: string }) => ({ + value: item.id, + label: item.nama, + })); + return ( - - {/* Header */} + - - - + Edit Kontak Darurat Keamanan - {/* Form */} - {/* Nama Kontak */} - setFormData((prev) => ({ ...prev, name: e.target.value })) - } - label="Nama Kontak Darurat" + label={Nama Kontak Darurat} placeholder="Masukkan nama kontak darurat" + value={formData.name} + onChange={(e) => updateField('name', e.target.value)} required /> - {/* MultiSelect */} Kontak Item} + placeholder="Pilih kontak item" + data={kategoriOptions} value={formData.kategoriId} - onChange={(val) => - setFormData((prev) => ({ ...prev, kategoriId: val })) - } - label={Kontak Item} - placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"} - data={ - Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data) - ? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({ - value: v.id, - label: v.nama, - })) - : [] - } + onChange={(values) => updateField('kategoriId', values)} clearable searchable required - error={ - !formData.kategoriId.length - ? "Pilih minimal satu kategori" - : undefined - } - disabled={isLoading} + error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined} /> - {/* Icon Select */} - + Ikon Program Kreatif Desa - setFormData((prev) => ({ ...prev, icon: value })) - } + value={formData.icon} + onChange={(icon) => updateField('icon', icon)} /> - {/* Submit */} + {/* Tombol Batal */} + + + {/* Tombol Simpan */} ); -} - -export default EditKontakDaruratKeamanan; +} \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx index c25192d5..2203fda5 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/[id]/page.tsx @@ -3,7 +3,7 @@ import { IconKey, IconMapper } from '@/app/admin/(dashboard)/_com/iconMap'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import kontakDarurat from '@/app/admin/(dashboard)/_state/keamanan/kontak-darurat-keamanan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -42,7 +42,7 @@ function DetailKontakDaruratKeamanan() { const data = kontakState.findUnique.data; return ( - + {/* Tombol Back */} - + - - - +
diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx index afdbd448..bd8e0b69 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/create/page.tsx @@ -6,22 +6,33 @@ import { Box, Button, Group, + Loader, MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKontakDaruratKeamanan() { const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + kontakState.create.form.nama?.trim() !== '' && + kontakState.create.form.icon?.trim() !== '' + ); + }; useShallowEffect(() => { kontakDarurat.kontakDaruratItem.findMany.load(); @@ -36,25 +47,31 @@ function CreateKontakDaruratKeamanan() { }; const handleSubmit = async () => { - await kontakState.create.create(); - resetForm(); - router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan'); + try { + setIsSubmitting(true); + await kontakState.create.create(); + resetForm(); + router.push('/admin/keamanan/kontak-darurat/kontak-darurat-keamanan'); + } catch (error) { + console.error('Error creating kontak darurat:', error); + toast.error('Terjadi kesalahan saat menambah kontak darurat'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + Tambah Kontak Darurat Keamanan @@ -72,7 +89,7 @@ function CreateKontakDaruratKeamanan() { {/* Input Nama Kategori */} { kontakState.create.form.nama = val.target.value; }} @@ -103,17 +120,31 @@ function CreateKontakDaruratKeamanan() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx index a5b6239e..96d5e5e1 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-keamanan/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -49,6 +48,7 @@ function KontakDaruratKeamanan() { function ListKontakDaruratKeamanan({ search }: { search: string }) { const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -59,47 +59,54 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) { } = kontakState.findMany; useShallowEffect(() => { - load(page, 10, search); + load(page, 10, debouncedSearch); kontakDarurat.kontakDaruratItem.findMany.load(); - }, [page, search]); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - + + {/* Judul + Tombol Tambah */} - - Daftar Kontak Darurat Keamanan - - - + + + Daftar Kontak Darurat Keamanan + + - {/* Tabel */} - - + {/* Desktop Table */} + +
- Kontak Darurat - Nama Kontak - Nomor Telepon - Aksi + Kontak Darurat + Nama Kontak + Nomor Telepon + Aksi @@ -107,17 +114,17 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) { filteredData.map((item) => ( - + {item.nama} - + {item.kategori?.nama} - + {item.kategori?.nomorTelepon || "-"} @@ -128,7 +135,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) { onClick={() => router.push(`/admin/keamanan/kontak-darurat/kontak-darurat-keamanan/${item.id}`)} > - Detail + Detail @@ -137,7 +144,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
- + Tidak ada data kontak darurat keamanan yang cocok
@@ -147,6 +154,55 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Kontak Darurat + + {item.nama} + + + + Nama Kontak + + {item.kategori?.nama} + + + + Nomor Telepon + + {item.kategori?.nomorTelepon || "-"} + + + + + + + + )) + ) : ( +
+ + Tidak ada data kontak darurat keamanan yang cocok + +
+ )} +
+
{/* Pagination */} @@ -168,4 +224,4 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) { ); } -export default KontakDaruratKeamanan; +export default KontakDaruratKeamanan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx index 4f25d331..0a2d0beb 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/layout.tsx @@ -1,8 +1,29 @@ 'use client' import React from 'react'; import LayoutTabs from './_lib/layoutTabs'; +import { usePathname } from 'next/navigation'; +import { Box } from '@mantine/core'; function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + return ( {children} diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx index f52bd6c1..f9f940a7 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik'; @@ -6,13 +7,13 @@ import { Box, Button, Group, + Loader, Paper, Select, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { IconArrowBack } from '@tabler/icons-react'; @@ -27,6 +28,7 @@ function EditLaporanPublik() { const stateLaporan = useProxy(laporanPublikState); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState<{ judul: string; @@ -44,40 +46,89 @@ function EditLaporanPublik() { kronologi: '', }); + const [originalData, setOriginalData] = useState({ + judul: '', + lokasi: '', + tanggalWaktu: '', + status: 'Proses', // Default status + penanganan: '', + kronologi: '', + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + formData.lokasi?.trim() !== '' && + formData.tanggalWaktu?.trim() !== '' && + formData.status?.trim() !== '' && + !isHtmlEmpty(formData.kronologi) && + !isHtmlEmpty(formData.penanganan) + ); + }; + useEffect(() => { const loadLaporanPublik = async () => { const id = params?.id as string; if (!id) return; - + try { const data = await stateLaporan.edit.load(id); if (data) { - setFormData((prev) => ({ - ...prev, - judul: data.judul ?? prev.judul, - lokasi: data.lokasi ?? prev.lokasi, - tanggalWaktu: data.tanggalWaktu ?? prev.tanggalWaktu, - status: (data.status as Status) ?? prev.status, - penanganan: data.penanganan?.[0]?.deskripsi ?? prev.penanganan, - kronologi: data.kronologi ?? prev.kronologi, - })); + setFormData({ + judul: data.judul ?? '', + lokasi: data.lokasi ?? '', + tanggalWaktu: data.tanggalWaktu ?? '', + status: (data.status as Status) ?? 'Proses', + penanganan: data.penanganan?.[0]?.deskripsi ?? '', + kronologi: data.kronologi ?? '', + }); + setOriginalData({ + judul: data.judul ?? '', + lokasi: data.lokasi ?? '', + tanggalWaktu: data.tanggalWaktu ?? '', + status: (data.status as Status) ?? 'Proses', + penanganan: data.penanganan?.[0]?.deskripsi ?? '', + kronologi: data.kronologi ?? '', + }); } } catch (error) { console.error("Error loading laporan publik:", error); toast.error("Gagal mengambil data laporan publik"); } }; - + loadLaporanPublik(); - }, [params?.id, stateLaporan.edit]); - + }, [params?.id]); + + const handleChange = (field: string, value: string | Status) => { setFormData((prev) => ({ ...prev, [field]: value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + lokasi: originalData.lokasi, + tanggalWaktu: originalData.tanggalWaktu, + status: (originalData.status as Status), + penanganan: originalData.penanganan, + kronologi: originalData.kronologi, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateLaporan.edit.form = { ...stateLaporan.edit.form, ...formData, @@ -89,18 +140,18 @@ function EditLaporanPublik() { } catch (error) { console.error("Error updating laporan publik:", error); toast.error("Terjadi kesalahan saat memperbarui laporan publik"); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Laporan Publik @@ -176,17 +227,31 @@ function EditLaporanPublik() { + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx index b5f83ee9..f6f068b0 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/[id]/page.tsx @@ -7,16 +7,15 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import laporanPublikState from '../../../_state/keamanan/laporan-publik'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import laporanPublikState from '../../../_state/keamanan/laporan-publik'; function DetailLaporanPublik() { const [modalHapus, setModalHapus] = useState(false); @@ -49,7 +48,7 @@ function DetailLaporanPublik() { const data = stateLaporan.findUnique.data; return ( - + {/* Tombol Kembali */} - + - - - +
diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx index 994af691..d086b10f 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx @@ -4,24 +4,37 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import laporanPublikState from '../../../_state/keamanan/laporan-publik'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; export type Status = 'Selesai' | 'Proses' | 'Gagal'; function CreateLaporanPublik() { const stateLaporan = useProxy(laporanPublikState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateLaporan.create.form.judul?.trim() !== '' && + stateLaporan.create.form.lokasi?.trim() !== '' && + stateLaporan.create.form.tanggalWaktu?.trim() !== '' && + stateLaporan.create.form.kronologi?.trim() !== '' + ); + }; const resetForm = () => { stateLaporan.create.form = { @@ -33,20 +46,26 @@ function CreateLaporanPublik() { }; const handleSubmit = async () => { - await stateLaporan.create.create(); - resetForm(); - router.push('/admin/keamanan/laporan-publik'); + try { + setIsSubmitting(true); + await stateLaporan.create.create(); + resetForm(); + router.push('/admin/keamanan/laporan-publik'); + } catch (error) { + console.error('Error creating laporan publik:', error); + toast.error('Terjadi kesalahan saat membuat laporan publik'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header with Back Button */} - - - + Tambah Laporan Publik @@ -63,7 +82,7 @@ function CreateLaporanPublik() { > (stateLaporan.create.form.judul = e.target.value)} label={Judul Laporan Publik} placeholder="Masukkan judul laporan publik" @@ -71,7 +90,7 @@ function CreateLaporanPublik() { /> (stateLaporan.create.form.lokasi = e.target.value)} label={Lokasi Laporan Publik} placeholder="Masukkan lokasi laporan publik" @@ -91,7 +110,7 @@ function CreateLaporanPublik() { /> (stateLaporan.create.form.kronologi = e.target.value)} label={Kronologi Laporan Publik} placeholder="Masukkan kronologi laporan publik" @@ -99,17 +118,31 @@ function CreateLaporanPublik() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx b/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx index 68b5d055..884bee6f 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -16,16 +16,15 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; -import HeaderSearch from '../../_com/header'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import laporanPublikState from '../../_state/keamanan/laporan-publik'; -import { useShallowEffect } from '@mantine/hooks'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../_com/header'; +import laporanPublikState from '../../_state/keamanan/laporan-publik'; function LaporanPublik() { const [search, setSearch] = useState(""); @@ -46,6 +45,7 @@ function LaporanPublik() { function ListLaporanPublik({ search }: { search: string }) { const stateLaporan = useProxy(laporanPublikState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -56,44 +56,54 @@ function ListLaporanPublik({ search }: { search: string }) { } = stateLaporan.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - - Daftar Laporan Publik - - - + + + + + Daftar Laporan Publik + + - - + + {/* Desktop Table */} + +
- Judul Laporan Publik - Tanggal - Status - Deskripsi - Aksi + Judul Laporan Publik + Tanggal + Status + Deskripsi + Aksi @@ -101,12 +111,12 @@ function ListLaporanPublik({ search }: { search: string }) { filteredData.map((item) => ( - + {item.judul} - + {new Date(item.tanggalWaktu).toLocaleDateString('id-ID')} @@ -134,9 +144,7 @@ function ListLaporanPublik({ search }: { search: string }) { - - - + @@ -156,7 +164,7 @@ function ListLaporanPublik({ search }: { search: string }) {
- + Tidak ada data laporan publik yang cocok
@@ -166,7 +174,88 @@ function ListLaporanPublik({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Judul Laporan Publik + {item.judul} + + + Tanggal + + {new Date(item.tanggalWaktu).toLocaleDateString('id-ID')} + + + + Status + + {item.status} + + + + Deskripsi + + + + + + )) + ) : ( +
+ + Tidak ada data laporan publik yang cocok + +
+ )} +
+ + {/* Tambah Baru (Mobile only) */} +
+
{ + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsiSingkat) && + !isHtmlEmpty(formData.deskripsi) && + convertYoutubeUrlToEmbed(formData.linkVideo) !== '' + ); + }; + // load data hanya sekali pas id berubah useEffect(() => { const loadKriminalitas = async () => { @@ -48,6 +73,12 @@ function EditPencegahanKriminalitas() { deskripsiSingkat: data.deskripsiSingkat ?? '', linkVideo: data.linkVideo ?? '', }); + setOriginalData({ + judul: data.judul ?? '', + deskripsi: data.deskripsi ?? '', + deskripsiSingkat: data.deskripsiSingkat ?? '', + linkVideo: data.linkVideo ?? '', + }); } } catch (error) { console.error('Error loading pencegahan kriminalitas:', error); @@ -66,6 +97,16 @@ function EditPencegahanKriminalitas() { setFormData((prev) => ({ ...prev, [field]: e.target.value })); }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + deskripsiSingkat: originalData.deskripsiSingkat, + linkVideo: originalData.linkVideo, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { const converted = convertYoutubeUrlToEmbed(formData.linkVideo); if (!converted) { @@ -74,6 +115,7 @@ function EditPencegahanKriminalitas() { } try { + setIsSubmitting(true); // update global state saat submit kriminalitasState.update.form = { judul: formData.judul, @@ -89,23 +131,23 @@ function EditPencegahanKriminalitas() { } catch (error) { console.error('Error updating pencegahan kriminalitas:', error); toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Back button + Title */} - - - + Edit Pencegahan Kriminalitas @@ -180,17 +222,31 @@ function EditPencegahanKriminalitas() { {/* Action button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx index 46ffedf0..23049268 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import { useState } from 'react'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas'; @@ -40,7 +40,7 @@ function DetailPencegahanKriminalitas() { const data = kriminalitasState.findUnique.data; return ( - + {/* Tombol Kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx index 89b189fc..9e12f6d4 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx @@ -5,27 +5,45 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import pencegahanKriminalitasState from '../../../_state/keamanan/pencegahan-kriminalitas'; -import { useState } from 'react'; import { convertYoutubeUrlToEmbed } from '../../../desa/gallery/lib/youtube-utils'; -import { toast } from 'react-toastify'; function CreatePencegahanKriminalitas() { const router = useRouter(); const kriminalitasState = useProxy(pencegahanKriminalitasState); const [link, setLink] = useState(''); const embedLink = convertYoutubeUrlToEmbed(link); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + kriminalitasState.create.form.judul?.trim() !== '' && + !isHtmlEmpty(kriminalitasState.create.form.deskripsiSingkat) && + !isHtmlEmpty(kriminalitasState.create.form.deskripsi) && + embedLink !== '' + ); + }; const resetForm = () => { kriminalitasState.create.form = { @@ -38,22 +56,29 @@ function CreatePencegahanKriminalitas() { }; const handleSubmit = async () => { - if (!embedLink) { - toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); - return; + try { + setIsSubmitting(true); + if (!embedLink) { + toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); + return; + } + + kriminalitasState.create.form.linkVideo = embedLink; + await kriminalitasState.create.create(); + resetForm(); + router.push('/admin/keamanan/pencegahan-kriminalitas'); + } catch (error) { + console.error('Gagal menambahkan pencegahan kriminalitas:', error); + toast.error('Gagal menambahkan pencegahan kriminalitas'); + } finally { + setIsSubmitting(false); } - - kriminalitasState.create.form.linkVideo = embedLink; - await kriminalitasState.create.create(); - resetForm(); - router.push('/admin/keamanan/pencegahan-kriminalitas'); }; return ( - + {/* Header Back Button + Title */} - - Tambah Pencegahan Kriminalitas @@ -82,7 +106,7 @@ function CreatePencegahanKriminalitas() { { kriminalitasState.create.form.judul = e.currentTarget.value; }} @@ -119,7 +143,7 @@ function CreatePencegahanKriminalitas() { setLink(e.currentTarget.value)} required /> @@ -143,17 +167,31 @@ function CreatePencegahanKriminalitas() { {/* Button Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx index be9eefc9..a39f4e71 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/page.tsx @@ -17,15 +17,14 @@ import { TableTr, Text, Title, - Tooltip, } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; -import HeaderSearch from '../../_com/header'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; -import pencegahanKriminalitasState from '../../_state/keamanan/pencegahan-kriminalitas'; -import { useShallowEffect } from '@mantine/hooks'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../_com/header'; +import pencegahanKriminalitasState from '../../_state/keamanan/pencegahan-kriminalitas'; function PencegahanKriminalitas() { const [search, setSearch] = useState(""); @@ -47,8 +46,9 @@ function PencegahanKriminalitas() { } function ListPencegahanKriminalitas({ search }: { search: string }) { - const kriminalitasState = useProxy(pencegahanKriminalitasState) + const kriminalitasState = useProxy(pencegahanKriminalitasState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -59,45 +59,54 @@ function ListPencegahanKriminalitas({ search }: { search: string }) { } = kriminalitasState.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); - + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( - + - ) + ); } return ( - - + + {/* Judul + Tombol Tambah */} - - Daftar Pencegahan Kriminalitas - - - + + + Daftar Pencegahan Kriminalitas + + - {/* Tabel */} - - + {/* Desktop Table */} + +
- Nama Pencegahan - Deskripsi - Deskripsi Singkat - Aksi + Nama Pencegahan + Deskripsi + Deskripsi Singkat + Aksi @@ -105,35 +114,29 @@ function ListPencegahanKriminalitas({ search }: { search: string }) { data.map((item) => ( - - + {item.judul} - - - - + - - - + @@ -151,7 +156,7 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
- + Tidak ada data pencegahan kriminalitas yang cocok
@@ -161,6 +166,71 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + {data.length > 0 ? ( + data.map((item) => ( + + + + + Nama Pencegahan + + + {item.judul} + + + + + Deskripsi Singkat + + + + + + Deskripsi + + + + + + + + + )) + ) : ( +
+ + Tidak ada data pencegahan kriminalitas yang cocok + +
+ )} +
{/* Pagination */} @@ -178,8 +248,8 @@ function ListPencegahanKriminalitas({ search }: { search: string }) { radius="md" />
-
+
); } -export default PencegahanKriminalitas; +export default PencegahanKriminalitas; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx deleted file mode 100644 index bf826e3d..00000000 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/edit/page.tsx +++ /dev/null @@ -1,401 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -"use client"; - -import colors from "@/con/colors"; -import { - Box, - Button, - Card, - Group, - Modal, - Paper, - Select, - Stack, - Text, - TextInput, - Title, - Tooltip, -} from "@mantine/core"; -import { IconArrowBack } from "@tabler/icons-react"; -import { useParams, useRouter } from "next/navigation"; -import { useProxy } from "valtio/utils"; -import { useEffect, useState } from "react"; -import { toast } from "react-toastify"; -import polsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat"; - -function EditPolsekTerdekat() { - const polsekState = useProxy(polsekTerdekat); - const params = useParams(); - const router = useRouter(); - - const [layananOptions, setLayananOptions] = useState< - { value: string; label: string }[] - >([]); - const [modalOpen, setModalOpen] = useState(false); - const [modalUpdateOpen, setModalUpdateOpen] = useState(false); - const [namaLayananBaru, setNamaLayananBaru] = useState(""); - const [selectedLayananId, setSelectedLayananId] = useState( - null - ); - const [namaLayananUpdate, setNamaLayananUpdate] = useState(""); - const [formData, setFormData] = useState({ - nama: "", - jarakKeDesa: "", - alamat: "", - nomorTelepon: "", - jamOperasional: "", - embedMapUrl: "", - namaTempatMaps: "", - alamatMaps: "", - linkPetunjukArah: "", - layananPolsekId: "", - }); - - // load data untuk form edit - useEffect(() => { - const loadPolsekTerdekat = async () => { - const id = params?.id as string; - if (!id) return; - - try { - const data = await polsekState.edit.load(id); - if (data) { - setFormData({ - nama: data.nama || "", - jarakKeDesa: data.jarakKeDesa || "", - alamat: data.alamat || "", - nomorTelepon: data.nomorTelepon || "", - jamOperasional: data.jamOperasional || "", - embedMapUrl: data.embedMapUrl || "", - namaTempatMaps: data.namaTempatMaps || "", - alamatMaps: data.alamatMaps || "", - linkPetunjukArah: data.linkPetunjukArah || "", - layananPolsekId: data.layananPolsekId || "", - }); - } - } catch (error) { - console.error("Error loading polsek terdekat:", error); - toast.error("Gagal memuat data polsek terdekat"); - } - }; - - loadPolsekTerdekat(); - }, [params?.id]); - - const fetchLayanan = async () => { - try { - const res = await fetch("/api/keamanan/layanan-polsek/find-many"); - const data = await res.json(); - - if (data.success) { - const options = data.data.map((item: any) => ({ - value: item.id, - label: item.nama, - })); - setLayananOptions(options); - } - } catch { - toast.error("Gagal memuat layanan polsek"); - } - }; - - const handleTambahLayanan = async () => { - if (!namaLayananBaru.trim()) - return toast.warn("Nama layanan tidak boleh kosong"); - - try { - const res = await fetch("/api/keamanan/layanan-polsek/create", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ nama: namaLayananBaru }), - }); - const data = await res.json(); - - if (data.success) { - const newLayanan = { - value: data.data.id, - label: data.data.nama, - }; - setLayananOptions((prev) => [...prev, newLayanan]); - await fetchLayanan(); - polsekState.create.form.layananPolsekId = data.data.id; - toast.success("Layanan baru ditambahkan!"); - setModalOpen(false); - setNamaLayananBaru(""); - } else { - toast.error(data.message || "Gagal menambah layanan"); - } - } catch { - toast.error("Error menambah layanan"); - } - }; - - const handleUpdateLayanan = async (id: string, namaBaru: string) => { - if (!namaBaru.trim()) - return toast.warn("Nama layanan tidak boleh kosong"); - - try { - const res = await fetch(`/api/keamanan/layanan-polsek/update/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ nama: namaBaru }), - }); - const data = await res.json(); - - if (data.success) { - await fetchLayanan(); - toast.success("Layanan berhasil diupdate!"); - setModalUpdateOpen(false); - setNamaLayananUpdate(""); - } else { - toast.error(data.message || "Gagal mengupdate layanan"); - } - } catch { - toast.error("Error mengupdate layanan"); - } - }; - - const handleDeleteLayanan = async (id: string) => { - const confirmDelete = confirm("Yakin ingin menghapus layanan ini?"); - if (!confirmDelete) return; - - try { - const res = await fetch(`/api/keamanan/layanan-polsek/del/${id}`, { - method: "DELETE", - }); - const data = await res.json(); - - if (data.success) { - await fetchLayanan(); - setLayananOptions((prev) => - prev.filter((layanan) => layanan.value !== id) - ); - toast.success("Layanan berhasil dihapus!"); - } else { - toast.error(data.message || "Gagal menghapus layanan"); - } - } catch { - toast.error("Error menghapus layanan"); - } - }; - - useEffect(() => { - fetchLayanan(); - }, []); - - const handleChange = (field: string, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })); - }; - - const handleSubmit = async () => { - try { - polsekState.edit.form = { ...formData }; // update global state hanya di sini - await polsekState.edit.update(); - toast.success("Polsek terdekat berhasil diperbarui!"); - router.push("/admin/keamanan/polsek-terdekat"); - } catch (error) { - console.error("Error updating polsek terdekat:", error); - toast.error("Gagal memperbarui data polsek terdekat"); - } - }; - - return ( - - {/* Modal Tambah */} - setModalOpen(false)} - title="Tambah Layanan Polsek" - centered - > - - setNamaLayananBaru(e.currentTarget.value)} - /> - - - - - {/* Modal Update */} - setModalUpdateOpen(false)} - title="Update Layanan Polsek" - centered - > - - setNamaLayananUpdate(e.currentTarget.value)} - /> - - - - - {/* Header */} - - - - - - Edit Polsek Terdekat - - - - {/* Form utama */} - - - {/* Input fields */} - handleChange("nama", e.currentTarget.value)} - label="Nama Polsek Terdekat" - placeholder="Masukkan nama Polsek Terdekat" - required - /> - handleChange("jarakKeDesa", e.currentTarget.value)} - label="Jarak Polsek Terdekat" - /> - handleChange("alamat", e.currentTarget.value)} - label="Alamat Polsek Terdekat" - /> - handleChange("nomorTelepon", e.currentTarget.value)} - label="Nomor Telepon" - /> - handleChange("jamOperasional", e.currentTarget.value)} - label="Jam Operasional" - /> - handleChange("embedMapUrl", e.currentTarget.value)} - label="Embed Map URL" - /> - handleChange("namaTempatMaps", e.currentTarget.value)} - label="Nama Tempat Maps" - /> - handleChange("alamatMaps", e.currentTarget.value)} - label="Alamat Maps" - /> - handleChange("linkPetunjukArah", e.currentTarget.value)} - label="Link Petunjuk Arah" - /> - - Layanan Polsek} - placeholder="Pilih layanan polsek" - data={layananOptions} - value={polsekState.create.form.layananPolsekId} - onChange={(val) => (polsekState.create.form.layananPolsekId = val || "")} - /> - - - {/* Tombol Submit */} - - - - - - - ); -} - -export default CreatePolsekTerdekat; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx new file mode 100644 index 00000000..29f2a4fd --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/edit/page.tsx @@ -0,0 +1,293 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import statePolsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat"; +import colors from "@/con/colors"; +import { + Box, + Button, + Group, + Loader, + MultiSelect, + Paper, + Stack, + TextInput, + Title +} from "@mantine/core"; +import { IconArrowBack } from "@tabler/icons-react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { useProxy } from "valtio/utils"; + + +type FormData = { + nama: string; + jarakKeDesa: string; + alamat: string; + nomorTelepon: string; + jamOperasional: string; + embedMapUrl: string; + namaTempatMaps: string; + alamatMaps: string; + linkPetunjukArah: string; + layananPolsekId: string[]; +}; + +function EditPolsekTerdekat() { + const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState); + const params = useParams(); + const router = useRouter(); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: [] + }); + + const [originalData, setOriginalData] = useState({ + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: [] + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.jarakKeDesa?.trim() !== '' && + formData.alamat?.trim() !== '' && + formData.nomorTelepon?.trim() !== '' && + formData.layananPolsekId.length > 0 + ); + }; + + useEffect(() => { + statePolsekTerdekat.layananPolsek.findManyAll.load(); + }, []); + + // load data untuk form edit + useEffect(() => { + const loadPolsekTerdekat = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await polsekState.edit.load(id); + if (data) { + setFormData({ + nama: data.nama || "", + jarakKeDesa: data.jarakKeDesa || "", + alamat: data.alamat || "", + nomorTelepon: data.nomorTelepon || "", + jamOperasional: data.jamOperasional || "", + embedMapUrl: data.embedMapUrl || "", + namaTempatMaps: data.namaTempatMaps || "", + alamatMaps: data.alamatMaps || "", + linkPetunjukArah: data.linkPetunjukArah || "", + layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [], + }); + + setOriginalData({ + nama: data.nama || "", + jarakKeDesa: data.jarakKeDesa || "", + alamat: data.alamat || "", + nomorTelepon: data.nomorTelepon || "", + jamOperasional: data.jamOperasional || "", + embedMapUrl: data.embedMapUrl || "", + namaTempatMaps: data.namaTempatMaps || "", + alamatMaps: data.alamatMaps || "", + linkPetunjukArah: data.linkPetunjukArah || "", + layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [], + }); + } + } catch (error) { + console.error("Error loading polsek terdekat:", error); + toast.error("Gagal memuat data polsek terdekat"); + } + }; + + loadPolsekTerdekat(); + }, [params?.id]); + + + const handleChange = (key: keyof FormData, value: any) => { + setFormData((prev) => ({ ...prev, [key]: value })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + jarakKeDesa: originalData.jarakKeDesa, + alamat: originalData.alamat, + nomorTelepon: originalData.nomorTelepon, + jamOperasional: originalData.jamOperasional, + embedMapUrl: originalData.embedMapUrl, + namaTempatMaps: originalData.namaTempatMaps, + alamatMaps: originalData.alamatMaps, + linkPetunjukArah: originalData.linkPetunjukArah, + layananPolsekId: (originalData as any)?.LayananToPolsek?.map((l: any) => l.layananId) || [], + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + await polsekState.edit.update(); + toast.success("Polsek terdekat berhasil diperbarui!"); + router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat"); + } catch (error) { + console.error("Error updating polsek terdekat:", error); + toast.error("Gagal memperbarui data polsek terdekat"); + } finally { + setIsSubmitting(false); + } +}; + + return ( + + {/* Header */} + + + + Edit Polsek Terdekat + + + + {/* Form utama */} + + + {/* Input fields */} + handleChange("nama", e.currentTarget.value)} + label="Nama Polsek Terdekat" + placeholder="Masukkan nama Polsek Terdekat" + required + /> + handleChange("jarakKeDesa", e.currentTarget.value)} + label="Jarak Polsek Terdekat" + /> + handleChange("alamat", e.currentTarget.value)} + label="Alamat Polsek Terdekat" + /> + handleChange("nomorTelepon", e.currentTarget.value)} + label="Nomor Telepon" + /> + handleChange("jamOperasional", e.currentTarget.value)} + label="Jam Operasional" + /> + handleChange("embedMapUrl", e.currentTarget.value)} + label="Embed Map URL" + /> + handleChange("namaTempatMaps", e.currentTarget.value)} + label="Nama Tempat Maps" + /> + handleChange("alamatMaps", e.currentTarget.value)} + label="Alamat Maps" + /> + handleChange("linkPetunjukArah", e.currentTarget.value)} + label="Link Petunjuk Arah" + /> + + handleChange('layananPolsekId', val)} + data={ + statePolsekTerdekat.layananPolsek.findManyAll.data?.map((v) => ({ + value: v.id, + label: v.nama, + })) || [] + } + clearable + searchable + required + error={!formData.layananPolsekId.length ? 'Pilih minimal satu layanan polsek' : undefined} + /> + + {/* Submit */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default EditPolsekTerdekat; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/page.tsx similarity index 76% rename from src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx rename to src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/page.tsx index 8cc1ec2a..5d8ac062 100644 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/[id]/page.tsx @@ -1,17 +1,17 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import { ModalKonfirmasiHapus } from '../../../../_com/modalKonfirmasiHapus'; +import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat'; function DetailPolsekTerdekat() { const router = useRouter(); - const polsekState = useProxy(polsekTerdekat); + const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState); const [selectedId, setSelectedId] = useState(null); const [modalHapus, setModalHapus] = useState(false); const params = useParams(); @@ -25,7 +25,7 @@ function DetailPolsekTerdekat() { polsekState.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/keamanan/polsek-terdekat"); + router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat"); } }; @@ -40,7 +40,7 @@ function DetailPolsekTerdekat() { const data = polsekState.findUnique.data; return ( - + {/* Tombol Back */} - + - - - +
diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx new file mode 100644 index 00000000..3f38a0d5 --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/create/page.tsx @@ -0,0 +1,251 @@ +'use client' +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + MultiSelect, + Paper, + Stack, + Text, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat'; + +function CreatePolsekTerdekat() { + const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + polsekState.create.form.nama?.trim() !== '' && + polsekState.create.form.jarakKeDesa?.trim() !== '' && + polsekState.create.form.alamat?.trim() !== '' && + polsekState.create.form.nomorTelepon?.trim() !== '' && + polsekState.create.form.layananPolsekId.length > 0 + ); + }; + + useEffect(() => { + statePolsekTerdekat.layananPolsek.findManyAll.load(); + }, []); + + const resetForm = () => { + polsekState.create.form = { + nama: "", + jarakKeDesa: "", + alamat: "", + nomorTelepon: "", + jamOperasional: "", + embedMapUrl: "", + namaTempatMaps: "", + alamatMaps: "", + linkPetunjukArah: "", + layananPolsekId: [], + }; + }; + + const isValidGoogleMapsEmbed = (url: string): boolean => { + try { + const u = new URL(url); + return ( + u.hostname === 'www.google.com' && + u.pathname === '/maps/embed' && + u.searchParams.has('pb') + ); + } catch { + return false; + } + }; + + const handleSubmit = async () => { + const { embedMapUrl } = polsekState.create.form; + + // ✅ Validasi Google Maps Embed URL (jika diisi) + if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) { + toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps."); + return; + } + + try { + setIsSubmitting(true); + await polsekState.create.create(); + resetForm(); + router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat"); + } catch (error) { + console.error(error); + toast.error("Gagal menambah polsek terdekat"); + } finally { + setIsSubmitting(false); + } + }; + + const extractEmbedUrl = (input: string): string => { + // Jika sudah berupa URL embed yang valid + if (input.startsWith('https://www.google.com/maps/embed?')) { + return input.trim(); + } + + // Coba parse sebagai HTML string (iframe) + const iframeRegex = /]*src=["']([^"']*)["'][^>]*>/i; + const match = input.match(iframeRegex); + if (match && match[1]?.startsWith('https://www.google.com/maps/embed?')) { + return match[1].trim(); + } + + // Jika tidak cocok, kembalikan input asli (atau string kosong) + return input.trim(); + }; + + return ( + + {/* Header */} + + + + Tambah Polsek Terdekat + + + + {/* Form */} + + + (polsekState.create.form.nama = val.target.value)} + label={Nama Polsek Terdekat} + placeholder="Masukkan nama Polsek Terdekat" + required + /> + (polsekState.create.form.jarakKeDesa = val.target.value)} + label={Jarak Polsek Terdekat} + placeholder="Masukkan jarak Polsek Terdekat" + required + /> + (polsekState.create.form.alamat = val.target.value)} + label={Alamat Polsek Terdekat} + placeholder="Masukkan alamat Polsek Terdekat" + required + /> + (polsekState.create.form.nomorTelepon = val.target.value)} + label={Nomor Telepon Polsek Terdekat} + placeholder="Masukkan nomor telepon Polsek Terdekat" + required + /> + (polsekState.create.form.jamOperasional = val.target.value)} + label={Jam Operasional Polsek Terdekat} + placeholder="Masukkan jam operasional Polsek Terdekat" + /> + { + const rawValue = e.currentTarget.value; + const cleanUrl = extractEmbedUrl(rawValue); + polsekState.create.form.embedMapUrl = cleanUrl; + }} + description="Contoh: https://www.google.com/maps/embed?pb=..." + label={Embed Map URL} + placeholder="Paste iframe dari Google Maps atau URL embed langsung" + /> + (polsekState.create.form.namaTempatMaps = val.target.value)} + label={Nama Tempat Maps} + placeholder="Masukkan nama tempat maps" + /> + (polsekState.create.form.alamatMaps = val.target.value)} + label={Alamat Maps} + placeholder="Masukkan alamat maps" + /> + (polsekState.create.form.linkPetunjukArah = val.target.value)} + label={Link Petunjuk Arah} + placeholder="Masukkan link petunjuk arah" + /> + ({ + value: v.id, + label: v.nama, + })) || []} + value={polsekState.create.form.layananPolsekId} + onChange={(val) => { + polsekState.create.form.layananPolsekId = val; + }} + searchable + clearable + nothingFoundMessage="Tidak ada layanan ditemukan" + required + error={polsekState.create.form.layananPolsekId?.length === 0 ? "Pilih minimal 1 layanan polsek" : undefined} + /> + {/* Tombol Submit */} + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreatePolsekTerdekat; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/page.tsx new file mode 100644 index 00000000..062be926 --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/daftar-polsek-terdekat/page.tsx @@ -0,0 +1,252 @@ +'use client' +import colors from '@/con/colors'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import statePolsekTerdekat from '../../../_state/keamanan/polsek-terdekat'; + + +function PolsekTerdekat() { + const [search, setSearch] = useState(""); + + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + + ); +} + +function ListPolsekTerdekat({ search }: { search: string }) { + const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); + + const { + data, + page, + totalPages, + loading, + load, + } = polsekState.findMany; + + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + const filteredData = data || []; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + + + + Daftar Polsek Terdekat + + + + + {/* Desktop Table */} + + + + + + + Nama Polsek + + + + + Jarak + + + + + Alamat + + + + + Aksi + + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.nama} + + + + + {item.jarakKeDesa} + + + + + {item.alamat} + + + + + + + )) + ) : ( + + +
+ + Tidak ada data Polsek yang cocok + +
+
+
+ )} +
+
+
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Polsek + + + {item.nama} + + + + + Jarak + + + {item.jarakKeDesa} + + + + + Alamat + + + {item.alamat} + + + + + + + + )) + ) : ( +
+ + Tidak ada data Polsek yang cocok + +
+ )} +
+
+
+ + {/* Pagination */} +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt={{ base: 'lg', md: 'xl' }} + mb={{ base: 'lg', md: 'xl' }} + color="blue" + radius="md" + /> +
+
+ ); +} + +export default PolsekTerdekat; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx new file mode 100644 index 00000000..5eaa948d --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/[id]/page.tsx @@ -0,0 +1,169 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' + +import statePolsekTerdekat from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditLayananPolsek() { + const editState = useProxy(statePolsekTerdekat.layananPolsek); + const router = useRouter(); + const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [originalData, setOriginalData] = useState({ + nama: '', + }); + + const [formData, setFormData] = useState({ + nama: '', + }); + + // Check if form is valid + const isFormValid = () => { + return formData.nama?.trim() !== ''; + }; + + useEffect(() => { + const loadLayananPolsek = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.update.load(id); + if (data) { + setFormData({ + nama: data.nama || '', + }); + setOriginalData({ + nama: data.nama || '', + }); + } + } catch (error) { + console.error('Error loading layanan polsek:', error); + toast.error('Gagal memuat data layanan polsek'); + } + }; + + loadLayananPolsek(); + }, [params?.id]); + + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ + ...prev, + [e.target.name]: e.target.value, + })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + }); + toast.info('Form dikembalikan ke data awal'); + }; + + const handleSubmit = async () => { + try { + setIsSubmitting(true); + // update global state hanya saat submit + editState.update.form = { + ...editState.update.form, + nama: formData.nama, + }; + + await editState.update.update(); + toast.success('Layanan Polsek berhasil diperbarui!'); + router.push('/admin/keamanan/polsek-terdekat/layanan-polsek'); + } catch (error) { + console.error('Error updating layanan polsek:', error); + toast.error('Terjadi kesalahan saat memperbarui layanan polsek'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Back Button + Title */} + + + + Edit Layanan Polsek + + + + {/* Form Wrapper */} + + + + + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default EditLayananPolsek; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx new file mode 100644 index 00000000..67d551f4 --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/create/page.tsx @@ -0,0 +1,119 @@ +'use client'; +import statePolsekTerdekat from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function CreateLayananPolsek() { + const createState = useProxy(statePolsekTerdekat.layananPolsek); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return createState.create.form.nama?.trim() !== ''; + }; + + const resetForm = () => { + createState.create.form = { + nama: '', + }; + }; + + const handleSubmit = async () => { + setIsSubmitting(true); + try { + await createState.create.create(); + resetForm(); + router.push('/admin/keamanan/polsek-terdekat/layanan-polsek'); + } catch (error) { + console.error('Error creating layanan polsek:', error); + toast.error('Gagal menambahkan layanan polsek'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Header dengan back button */} + + + + Tambah Layanan Polsek + + + + {/* Form utama */} + + + (createState.create.form.nama = e.target.value)} + required + /> + + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreateLayananPolsek; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/page.tsx new file mode 100644 index 00000000..df663acb --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layanan-polsek/page.tsx @@ -0,0 +1,265 @@ +'use client' +import colors from '@/con/colors'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import statePolsekTerdekat from '../../../_state/keamanan/polsek-terdekat'; + + +function LayananPolsek() { + const [search, setSearch] = useState(""); + + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + + ); +} + +function ListLayananPolsek({ search }: { search: string }) { + const layananState = useProxy(statePolsekTerdekat.layananPolsek); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); + + const { + data, + page, + totalPages, + loading, + load, + } = layananState.findMany; + + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + const handleDelete = () => { + if (selectedId) { + layananState.delete.delete(selectedId); + setModalHapus(false); + setSelectedId(null); + load(page, 10, search); + } + }; + + const filteredData = data || []; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + + + + Daftar Layanan Polsek + + + + + {/* Desktop Table */} + + + + + + + Nama Layanan Polsek + + + + Edit + + + Hapus + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.nama} + + + + + + + + + + )) + ) : ( + + +
+ + Tidak ada data Polsek yang cocok + +
+
+
+ )} +
+
+
+ + {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Layanan Polsek + + + {item.nama} + + + + + + + + + )) + ) : ( +
+ + Tidak ada data Layanan Polsek yang cocok + +
+ )} +
+
+
+ + {/* Pagination */} +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt={{ base: 'lg', md: 'xl' }} + mb={{ base: 'lg', md: 'xl' }} + color="blue" + radius="md" + /> +
+ + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleDelete} + text="Apakah anda yakin ingin menghapus layanan polsek ini?" + /> +
+ ); +} + +export default LayananPolsek; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layout.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layout.tsx new file mode 100644 index 00000000..5792a343 --- /dev/null +++ b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/layout.tsx @@ -0,0 +1,34 @@ +'use client' +import React from 'react'; +import LayoutPolsek from './_com/layoutPolsek'; +import { usePathname } from 'next/navigation'; +import { Box } from '@mantine/core'; + +function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +export default Layout; diff --git a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx b/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx deleted file mode 100644 index 22aa1d19..00000000 --- a/src/app/admin/(dashboard)/keamanan/polsek-terdekat/page.tsx +++ /dev/null @@ -1,165 +0,0 @@ -'use client' -import colors from '@/con/colors'; -import { - Box, - Button, - Center, - Group, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip, -} from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import HeaderSearch from '../../_com/header'; -import polsekTerdekat from '../../_state/keamanan/polsek-terdekat'; - -function PolsekTerdekat() { - const [search, setSearch] = useState(""); - - return ( - - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - - ); -} - -function ListPolsekTerdekat({ search }: { search: string }) { - const polsekState = useProxy(polsekTerdekat) - const router = useRouter(); - - const { - data, - page, - totalPages, - loading, - load, - } = polsekState.findMany; - - useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) - - const filteredData = data || [] - - if (loading || !data) { - return ( - - - - ) - } - - return ( - - - - Daftar Polsek Terdekat - - - - - - - - - - Nama Polsek - Jarak - Alamat - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.nama} - - - - {item.jarakKeDesa} - - - {item.alamat} - - - - - - - )) - ) : ( - - -
- - Tidak ada data Polsek yang cocok - -
-
-
- )} -
-
-
-
- - {/* Pagination */} -
- { - load(newPage, 10, search); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); -} - -export default PolsekTerdekat; diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx index 2de3db48..0616cddc 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/edit/page.tsx @@ -1,16 +1,17 @@ "use client"; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from "@mantine/core"; import { IconArrowBack, @@ -37,12 +38,35 @@ function EditTipsKeamanan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ judul: "", deskripsi: "", imageId: "", }); + const [originalData, setOriginalData] = useState({ + judul: "", + deskripsi: "", + imageId: "", + imageUrl: "", + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.judul?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data saat pertama kali useEffect(() => { const loadData = async () => { @@ -58,6 +82,13 @@ function EditTipsKeamanan() { imageId: data.imageId || "", }); + setOriginalData({ + judul: data.judul || "", + deskripsi: data.deskripsi || "", + imageId: data.imageId || "", + imageUrl: data.image?.link || "", + }) + if (data?.image?.link) { setPreviewImage(data.image.link); } @@ -74,6 +105,7 @@ function EditTipsKeamanan() { const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { @@ -98,23 +130,34 @@ function EditTipsKeamanan() { } catch (error) { console.error("Error updating tips keamanan:", error); toast.error("Terjadi kesalahan saat memperbarui tips keamanan"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + judul: originalData.judul, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + return ( - + {/* Header */} - - - + Edit Tips Keamanan @@ -170,14 +213,14 @@ function EditTipsKeamanan() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp {previewImage ? ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + ) : ( + {/* Tombol Batal */} + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx index 61bb32c9..2874b250 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; @@ -41,7 +41,7 @@ function DetailTipsKeamanan() { const data = stateKeamanan.findUnique.data; return ( - + - + - - - + diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx index 6f82636a..675544b2 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -27,6 +28,23 @@ function CreateKeamananLingkungan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateKeamanan.create.form.judul?.trim() !== '' && + !isHtmlEmpty(stateKeamanan.create.form.deskripsi) && + file !== null + ); + }; const resetForm = () => { stateKeamanan.create.form = { @@ -39,38 +57,44 @@ function CreateKeamananLingkungan() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + stateKeamanan.create.form.imageId = uploaded.id; + + await stateKeamanan.create.create(); + + resetForm(); + router.push('/admin/keamanan/tips-keamanan'); + } catch (error) { + console.error("Error creating tips keamanan:", error); + toast.error("Gagal menambahkan tips keamanan"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); - } - - stateKeamanan.create.form.imageId = uploaded.id; - - await stateKeamanan.create.create(); - - resetForm(); - router.push('/admin/keamanan/tips-keamanan'); }; return ( - + {/* Header Back + Title */} - - - + Tambah Tips Keamanan @@ -101,7 +125,7 @@ function CreateKeamananLingkungan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -122,7 +146,7 @@ function CreateKeamananLingkungan() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -138,7 +180,7 @@ function CreateKeamananLingkungan() { (stateKeamanan.create.form.judul = e.target.value)} required /> @@ -158,17 +200,31 @@ function CreateKeamananLingkungan() { {/* Submit Button */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx index 7305d94c..0d7e02e6 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/page.tsx @@ -17,13 +17,12 @@ import { TableTr, Text, Title, - Tooltip, } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; -import { useProxy } from 'valtio/utils'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../_com/header'; import tipsKeamananState from '../../_state/keamanan/tips-keamanan'; @@ -44,8 +43,9 @@ function TipsKeamanan() { } function ListTipsKeamanan({ search }: { search: string }) { - const stateKeamanan = useProxy(tipsKeamananState) + const stateKeamanan = useProxy(tipsKeamananState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -53,71 +53,94 @@ function ListTipsKeamanan({ search }: { search: string }) { totalPages, loading, load, - } = stateKeamanan.findMany + } = stateKeamanan.findMany; useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } return ( - + Daftar Tips Keamanan - - - + - - + + {/* Desktop Table */} + +
- Judul - Deskripsi - Aksi + + + Judul + + + + + Deskripsi + + + + + Aksi + + {filteredData.length > 0 ? ( filteredData.map((item) => ( - - - - {item.judul} - - + + + {item.judul} + - - - - + + - + @@ -125,8 +148,10 @@ function ListTipsKeamanan({ search }: { search: string }) { ) : ( -
- Tidak ada data tips keamanan yang cocok +
+ + Tidak ada data tips keamanan yang cocok +
@@ -134,13 +159,66 @@ function ListTipsKeamanan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Judul + + + {item.judul} + + + + + Deskripsi + + + + + + + + + + + )) + ) : ( +
+ + Tidak ada data tips keamanan yang cocok + +
+ )} +
+
{ - load(newPage, 10) - window.scrollTo({ top: 0, behavior: 'smooth' }) + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} mt="md" @@ -153,4 +231,4 @@ function ListTipsKeamanan({ search }: { search: string }) { ); } -export default TipsKeamanan; +export default TipsKeamanan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx index 8bcb314a..bd4c00e3 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/_lib/layoutTabs.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' import colors from '@/con/colors'; -import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core'; +import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; @@ -17,36 +17,31 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { label: "Presentase Kelahiran & Kematian", value: "presentasekelahiran&kematian", href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian", - icon: , - tooltip: "Lihat data kelahiran dan kematian" + icon: }, { - label: "Grafik Hasil Kepuasan Masyarakat", - value: "grafikhasilkepuasan", - href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan", - icon: , - tooltip: "Grafik kepuasan masyarakat terhadap pelayanan" + label: "Penderita Penyakit", + value: "penderitapenyakit", + href: "/admin/kesehatan/data-kesehatan-warga/penderita_penyakit", + icon: }, { label: "Fasilitas Kesehatan", value: "fasilitaskesehatan", href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan", - icon: , - tooltip: "Data fasilitas kesehatan desa" + icon: }, { label: "Jadwal Kegiatan", value: "jadwalkegiatan", href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan", - icon: , - tooltip: "Atur jadwal kegiatan kesehatan" + icon: }, { label: "Artikel Kesehatan", value: "artikelkesehatan", href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan", - icon: , - tooltip: "Artikel & informasi seputar kesehatan" + icon: }, ]; @@ -86,42 +81,76 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { keepMounted={false} > {/* ✅ Scroll horizontal wrapper */} - - - {tabs.map((tab, i) => ( - + + + + {tabs.map((tab, i) => ( {tab.label} - - ))} - - + ))} + + + + + + + + + {tabs.map((tab, i) => ( + + {tab.label} + + ))} + + + {tabs.map((tab, i) => ( @@ -139,7 +168,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { ))} - + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx index 227c8c1f..2076ab86 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/edit/page.tsx @@ -14,8 +14,7 @@ import { Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -148,14 +147,12 @@ function EditArtikelKesehatan() { ); return ( - + {/* Header */} - - - + Edit Artikel Kesehatan diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx index e03b527f..8ba15f0c 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/[id]/page.tsx @@ -10,8 +10,7 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -50,7 +49,7 @@ function DetailArtikelKesehatan() { const data = state.findUnique.data; return ( - + {/* Tombol Back */} - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx index 7289cd05..857426cf 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/create/page.tsx @@ -4,16 +4,17 @@ import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -27,6 +28,34 @@ function CreateArtikelKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateArtikelKesehatan.create.form.title?.trim() !== '' && + stateArtikelKesehatan.create.form.content?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.introduction.content) && + stateArtikelKesehatan.create.form.symptom.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.symptom.content) && + stateArtikelKesehatan.create.form.prevention.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.prevention.content) && + stateArtikelKesehatan.create.form.firstAid.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.firstAid.content) && + stateArtikelKesehatan.create.form.mythVsFact.title?.trim() !== '' && + !isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.mitos) && + !isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.fakta) && + !isHtmlEmpty(stateArtikelKesehatan.create.form.doctorSign.content) && + file !== null + ); + }; const resetForm = () => { stateArtikelKesehatan.create.form = { @@ -63,41 +92,115 @@ function CreateArtikelKesehatan() { const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); + + if (!stateArtikelKesehatan.create.form.title?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.content?.trim()) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.introduction.content)) { + toast.error('Pendahuluan wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.symptom.title?.trim()) { + toast.error('Judul gejala wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.symptom.content)) { + toast.error('Deskripsi gejala wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.prevention.title?.trim()) { + toast.error('Judul pencegahan wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.prevention.content)) { + toast.error('Deskripsi pencegahan wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.firstAid.title?.trim()) { + toast.error('Judul pertolongan pertama wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.firstAid.content)) { + toast.error('Deskripsi pertolongan pertama wajib diisi'); + return; + } + + if (!stateArtikelKesehatan.create.form.mythVsFact.title?.trim()) { + toast.error('Judul mitos vs fakta wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.mitos)) { + toast.error('Deskripsi mitos wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.mythVsFact.fakta)) { + toast.error('Deskripsi fakta wajib diisi'); + return; + } + + if (isHtmlEmpty(stateArtikelKesehatan.create.form.doctorSign.content)) { + toast.error('Deskripsi kapan harus ke dokter wajib diisi'); + return; + } + if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); + toast.error('Gambar wajib dipilih'); + return; } - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); + try { + setIsSubmitting(true); - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal mengunggah gambar, silakan coba lagi'); + } + + stateArtikelKesehatan.create.form.imageId = uploaded.id; + await stateArtikelKesehatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); + } catch (error) { + console.error('Error submitting form:', error); + toast.error('Gagal menyimpan data, silakan coba lagi'); + } finally { + setIsSubmitting(false); } - - stateArtikelKesehatan.create.form.imageId = uploaded.id; - await stateArtikelKesehatan.create.submit(); - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); }; return ( - + {/* Header */} - - - + Tambah Artikel Kesehatan @@ -127,7 +230,7 @@ function CreateArtikelKesehatan() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -148,7 +251,7 @@ function CreateArtikelKesehatan() { {previewImage && ( - + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -167,7 +288,7 @@ function CreateArtikelKesehatan() { { stateArtikelKesehatan.create.form.title = e.target.value; }} @@ -176,7 +297,7 @@ function CreateArtikelKesehatan() { { stateArtikelKesehatan.create.form.content = e.target.value; }} @@ -199,7 +320,7 @@ function CreateArtikelKesehatan() { label={"Judul Gejala"} required placeholder="Masukkan judul gejala penyakit" - defaultValue={stateArtikelKesehatan.create.form.symptom.title} + value={stateArtikelKesehatan.create.form.symptom.title} onChange={(e) => { stateArtikelKesehatan.create.form.symptom.title = e.target.value; }} @@ -223,7 +344,7 @@ function CreateArtikelKesehatan() { label={"Judul Pencegahan"} required placeholder="Masukkan judul" - defaultValue={stateArtikelKesehatan.create.form.prevention.title} + value={stateArtikelKesehatan.create.form.prevention.title} onChange={(e) => { stateArtikelKesehatan.create.form.prevention.title = e.target.value; }} @@ -244,7 +365,7 @@ function CreateArtikelKesehatan() { label={"Judul Pertolongan Pertama"} required placeholder="Masukkan judul" - defaultValue={stateArtikelKesehatan.create.form.firstAid.title} + value={stateArtikelKesehatan.create.form.firstAid.title} onChange={(e) => { stateArtikelKesehatan.create.form.firstAid.title = e.target.value; }} @@ -265,7 +386,7 @@ function CreateArtikelKesehatan() { label={"Judul Mitos dan Fakta"} required placeholder="Masukkan judul" - defaultValue={stateArtikelKesehatan.create.form.mythVsFact.title} + value={stateArtikelKesehatan.create.form.mythVsFact.title} onChange={(e) => { stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value; }} @@ -303,17 +424,32 @@ function CreateArtikelKesehatan() { {/* Submit Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx index 961c6630..2514ab14 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/artikel_kesehatan/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -51,10 +50,11 @@ function ListArtikelKesehatan({ search }: { search: string }) { const router = useRouter(); const { data, page, totalPages, loading, load } = stateArtikel.findMany; + const [debouncedSearch] = useDebouncedValue(search, 1000); useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; @@ -72,21 +72,21 @@ function ListArtikelKesehatan({ search }: { search: string }) { {/* Judul + Tombol Tambah */} Daftar Artikel Kesehatan - - - + {/* Tabel */} - +
Judul diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx index 8a25e7ba..01e6a6ff 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/edit/page.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; @@ -8,20 +7,23 @@ import { Box, Button, Group, + Loader, + MultiSelect, Paper, Stack, Text, TextInput, Title, - Tooltip, } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; -interface FasilitasKesehatanFormBase { +// Tipe form yang SESUAI dengan logika relasi (array ID) +interface EditFasilitasKesehatanForm { name: string; informasiUmum: { fasilitas: string; @@ -29,70 +31,160 @@ interface FasilitasKesehatanFormBase { jamOperasional: string; }; layananUnggulan: { content: string }; - dokterdanTenagaMedis: { - name: string; - specialist: string; - jadwal: string; - }; + dokterdanTenagaMedis: string[]; // ← ARRAY ID fasilitasPendukung: { content: string }; prosedurPendaftaran: { content: string }; - tarifDanLayanan: { - layanan: string; - tarif: string; - }; + tarifDanLayanan: string[]; // ← ARRAY ID } function EditFasilitasKesehatan() { const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan); + const dokterState = useProxy(fasilitasKesehatanState.dokter); + const tarifState = useProxy(fasilitasKesehatanState.tarif); const router = useRouter(); - const params = useParams(); + const params = useParams<{ id: string }>(); + const [isSubmitting, setIsSubmitting] = useState(false); - const [formData, setFormData] = useState({ + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.informasiUmum.fasilitas?.trim() !== '' && + formData.informasiUmum.alamat?.trim() !== '' && + formData.informasiUmum.jamOperasional?.trim() !== '' && + !isHtmlEmpty(formData.layananUnggulan.content) && + formData.dokterdanTenagaMedis.length > 0 && + !isHtmlEmpty(formData.fasilitasPendukung.content) && + !isHtmlEmpty(formData.prosedurPendaftaran.content) && + formData.tarifDanLayanan.length > 0 + ); + }; + + const [formData, setFormData] = useState({ name: '', informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' }, layananUnggulan: { content: '' }, - dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' }, + dokterdanTenagaMedis: [], fasilitasPendukung: { content: '' }, prosedurPendaftaran: { content: '' }, - tarifDanLayanan: { layanan: '', tarif: '' }, + tarifDanLayanan: [], }); - // Helper untuk update nested state - const updateForm = ( - key: K, - value: FasilitasKesehatanFormBase[K] - ) => setFormData(prev => ({ ...prev, [key]: value })); - - const updateNested = < - K extends keyof FasilitasKesehatanFormBase, - N extends keyof FasilitasKesehatanFormBase[K] - >(key: K, nestedKey: N, value: FasilitasKesehatanFormBase[K][N]) => - setFormData(prev => ({ - ...prev, - [key]: { ...prev[key] as object, [nestedKey]: value }, - })); - - // Load data - useEffect(() => { - const load = async () => { - const id = params?.id as string; + // Load data fasilitas & daftar dokter/tarif + useShallowEffect(() => { + const loadAll = async () => { + const id = params?.id; if (!id) return; - try { - await state.edit.load(id); - const form = state.edit.form; - if (form) setFormData(form as FasilitasKesehatanFormBase); - } catch (err) { - console.error(err); - toast.error('Gagal memuat data fasilitas kesehatan'); + + // Load dokter & tarif (untuk opsi MultiSelect) + await Promise.all([ + dokterState.findMany.load(), + tarifState.findMany.load(), + ]); + + // Load data fasilitas + await state.edit.load(id); + const loaded = state.edit.form; + if (loaded) { + setFormData({ + name: loaded.name, + informasiUmum: loaded.informasiUmum, + layananUnggulan: loaded.layananUnggulan, + dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [], + fasilitasPendukung: loaded.fasilitasPendukung, + prosedurPendaftaran: loaded.prosedurPendaftaran, + tarifDanLayanan: loaded.tarifDanLayanan || [], + }); } }; - load(); + + loadAll(); }, [params?.id]); - // Submit - const handleSubmit = async () => { + const updateForm = ( + field: K, + value: EditFasilitasKesehatanForm[K] + ) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const handleReset = () => { + const loaded = state.edit.form; + if (loaded) { + setFormData({ + name: loaded.name, + informasiUmum: loaded.informasiUmum, + layananUnggulan: loaded.layananUnggulan, + dokterdanTenagaMedis: loaded.dokterdanTenagaMedis || [], + fasilitasPendukung: loaded.fasilitasPendukung, + prosedurPendaftaran: loaded.prosedurPendaftaran, + tarifDanLayanan: loaded.tarifDanLayanan || [], + }); + toast.info('Form dikembalikan ke data awal'); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.name?.trim()) { + toast.error('Nama fasilitas kesehatan wajib diisi'); + return; + } + + if (!formData.informasiUmum.fasilitas?.trim()) { + toast.error('Fasilitas wajib diisi'); + return; + } + + if (!formData.informasiUmum.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (!formData.informasiUmum.jamOperasional?.trim()) { + toast.error('Jam operasional wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.layananUnggulan.content)) { + toast.error('Layanan unggulan wajib diisi'); + return; + } + + if (formData.dokterdanTenagaMedis.length === 0) { + toast.error('Dokter dan tenaga medis wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.fasilitasPendukung.content)) { + toast.error('Fasilitas pendukung wajib diisi'); + return; + } + + if (formData.tarifDanLayanan.length === 0) { + toast.error('Tarif dan layanan wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.prosedurPendaftaran.content)) { + toast.error('Prosedur pendaftaran wajib diisi'); + return; + } + try { - state.edit.form = { ...state.edit.form, ...formData }; + setIsSubmitting(true); + + // Update state Valtio + state.edit.form = { ...formData }; + const success = await state.edit.submit(); if (success) { toast.success('Fasilitas kesehatan berhasil diperbarui!'); @@ -100,19 +192,19 @@ function EditFasilitasKesehatan() { } } catch (err) { console.error(err); - toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan'); + toast.error('Gagal memperbarui data'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Fasilitas Kesehatan @@ -130,7 +222,7 @@ function EditFasilitasKesehatan() { updateForm('name', e.target.value)} required @@ -138,114 +230,118 @@ function EditFasilitasKesehatan() { {/* Informasi Umum */} - - Informasi Umum - + Informasi Umum updateNested('informasiUmum', 'fasilitas', e.target.value)} + onChange={(e) => + updateForm('informasiUmum', { + ...formData.informasiUmum, + fasilitas: e.target.value, + }) + } /> updateNested('informasiUmum', 'alamat', e.target.value)} + onChange={(e) => + updateForm('informasiUmum', { + ...formData.informasiUmum, + alamat: e.target.value, + }) + } /> updateNested('informasiUmum', 'jamOperasional', e.target.value)} + onChange={(e) => + updateForm('informasiUmum', { + ...formData.informasiUmum, + jamOperasional: e.target.value, + }) + } /> {/* Layanan Unggulan */} - - Layanan Unggulan - + Layanan Unggulan updateNested('layananUnggulan', 'content', v)} + onChange={(v) => updateForm('layananUnggulan', { content: v })} /> - {/* Dokter dan Tenaga Medis */} - - - Dokter dan Tenaga Medis - - updateNested('dokterdanTenagaMedis', 'name', e.target.value)} - /> - - updateNested('dokterdanTenagaMedis', 'specialist', e.target.value) - } - /> - updateNested('dokterdanTenagaMedis', 'jadwal', e.target.value)} - /> - + {/* Dokter & Tenaga Medis — MultiSelect */} + ({ + value: d.id, + label: `${d.name} (${d.specialist})`, + })) || [] + } + value={formData.dokterdanTenagaMedis} + onChange={(val) => updateForm('dokterdanTenagaMedis', val)} + searchable + clearable + required + /> {/* Fasilitas Pendukung */} - - Fasilitas Pendukung - + Fasilitas Pendukung updateNested('fasilitasPendukung', 'content', v)} + onChange={(v) => updateForm('fasilitasPendukung', { content: v })} /> {/* Prosedur Pendaftaran */} - - Prosedur Pendaftaran - + Prosedur Pendaftaran updateNested('prosedurPendaftaran', 'content', v)} + onChange={(v) => updateForm('prosedurPendaftaran', { content: v })} /> - {/* Tarif dan Layanan */} - - - Tarif dan Layanan - - updateNested('tarifDanLayanan', 'tarif', e.target.value)} - /> - updateNested('tarifDanLayanan', 'layanan', e.target.value)} - /> - + {/* Tarif & Layanan — MultiSelect */} + ({ + value: t.id, + label: `${t.layanan} - ${t.tarif}`, + })) || [] + } + value={formData.tarifDanLayanan} + onChange={(val) => updateForm('tarifDanLayanan', val)} + searchable + clearable + required + /> - {/* Tombol Simpan */} - + {/* Aksi */} + + @@ -254,4 +350,4 @@ function EditFasilitasKesehatan() { ); } -export default EditFasilitasKesehatan; +export default EditFasilitasKesehatan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx index d6640e65..d8b953c5 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/[id]/page.tsx @@ -9,8 +9,13 @@ import { Paper, Skeleton, Stack, - Text, - Tooltip, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; @@ -51,7 +56,7 @@ function DetailFasilitasKesehatan() { const data = state.findUnique.data; return ( - + {/* Tombol Back */}
+ + + Nama + Spesialis + Jadwal + Jam Operasional + + + + {data.dokterdantenagamedis.map((dokter) => ( + + + {dokter.name || '-'} + + + {dokter.specialist || '-'} + + + {dokter.jadwal || '-'} + + + {dokter.jamBukaOperasional} – {dokter.jamTutupOperasional} + {dokter.jadwalLibur && ( + <> +
+ + Libur: {dokter.jadwalLibur} ({dokter.jamBukaLibur}–{dokter.jamTutupLibur}) + + + )} +
+
+ ))} +
+
+
+ ) : ( + Tidak ada dokter atau tenaga medis terdaftar. + )}
- - Tarif & Layanan - Layanan - {data.tarifdanlayanan?.layanan || '-'} - Tarif - {data.tarifdanlayanan?.tarif || '-'} + + Tarif & Layanan + {Array.isArray(data.tarifdanlayanan) && data.tarifdanlayanan.length > 0 ? ( + + + + + Layanan + Tarif + + + + {data.tarifdanlayanan.map((tarif) => ( + + + {tarif.layanan || '-'} + + + {tarif.tarif || '-'} + + + ))} + +
+
+ ) : ( + Tidak ada tarif atau layanan terdaftar. + )}
{/* Aksi */} - - - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx index 530039a9..d414ef0d 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create/page.tsx @@ -6,22 +6,47 @@ import { Box, Button, Group, + Loader, + MultiSelect, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - function CreateFasilitasKesehatan() { const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateFasilitasKesehatan.create.form.name?.trim() !== '' && + stateFasilitasKesehatan.create.form.informasiUmum.fasilitas?.trim() !== '' && + stateFasilitasKesehatan.create.form.informasiUmum.alamat?.trim() !== '' && + stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional?.trim() !== '' && + !isHtmlEmpty(stateFasilitasKesehatan.create.form.layananUnggulan.content) && + stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.length > 0 && + !isHtmlEmpty(stateFasilitasKesehatan.create.form.fasilitasPendukung.content) && + stateFasilitasKesehatan.create.form.tarifDanLayanan.length > 0 && + !isHtmlEmpty(stateFasilitasKesehatan.create.form.prosedurPendaftaran.content) + ); + }; const resetForm = () => { stateFasilitasKesehatan.create.form = { @@ -32,48 +57,98 @@ function CreateFasilitasKesehatan() { jamOperasional: '', }, layananUnggulan: { - content: '', - }, - dokterdanTenagaMedis: { - name: '', - specialist: '', - jadwal: '', + content: '' }, + dokterdanTenagaMedis: [] as string[], fasilitasPendukung: { content: '', }, prosedurPendaftaran: { content: '', }, - tarifDanLayanan: { - layanan: '', - tarif: '', - }, + tarifDanLayanan: [] as string[], }; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await stateFasilitasKesehatan.create.submit(); - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); + + if (!stateFasilitasKesehatan.create.form.name?.trim()) { + toast.error('Nama fasilitas kesehatan wajib diisi'); + return; + } + + if (!stateFasilitasKesehatan.create.form.informasiUmum.fasilitas?.trim()) { + toast.error('Fasilitas wajib diisi'); + return; + } + + if (!stateFasilitasKesehatan.create.form.informasiUmum.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (!stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional?.trim()) { + toast.error('Jam operasional wajib diisi'); + return; + } + + if (isHtmlEmpty(stateFasilitasKesehatan.create.form.layananUnggulan.content)) { + toast.error('Layanan unggulan wajib diisi'); + return; + } + + if (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.length === 0) { + toast.error('Dokter dan tenaga medis wajib dipilih'); + return; + } + + if (isHtmlEmpty(stateFasilitasKesehatan.create.form.fasilitasPendukung.content)) { + toast.error('Fasilitas pendukung wajib diisi'); + return; + } + + if (stateFasilitasKesehatan.create.form.tarifDanLayanan.length === 0) { + toast.error('Layanan wajib dipilih'); + return; + } + + if (isHtmlEmpty(stateFasilitasKesehatan.create.form.prosedurPendaftaran.content)) { + toast.error('Prosedur pendaftaran wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + await stateFasilitasKesehatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data'); + } finally { + setIsSubmitting(false); + } }; + useShallowEffect(() => { + fasilitasKesehatanState.dokter.findMany.load(); + fasilitasKesehatanState.tarif.findMany.load(); + }, []); + return ( - + {/* Header */} - - - + Tambah Data Fasilitas Kesehatan @@ -92,7 +167,7 @@ function CreateFasilitasKesehatan() { (stateFasilitasKesehatan.create.form.name = e.target.value)} required /> @@ -103,21 +178,21 @@ function CreateFasilitasKesehatan() { (stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value)} required /> (stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value)} required /> (stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value)} required /> @@ -132,31 +207,25 @@ function CreateFasilitasKesehatan() { /> + {/* Dokter dan Tenaga Medis */} - - Dokter dan Tenaga Medis - (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)} - required - /> - (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)} - required - /> - (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)} - required - /> - + ({ + label: item.name, + value: item.id, + })) || [] + } + value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis} + onChange={(val: string[]) => { + stateFasilitasKesehatan.create.form.dokterdanTenagaMedis = val; + }} + searchable + clearable + required + /> {/* Fasilitas Pendukung */} @@ -167,6 +236,24 @@ function CreateFasilitasKesehatan() { /> + ({ + label: item.layanan, + value: item.id, + })) || [] + } + value={stateFasilitasKesehatan.create.form.tarifDanLayanan} // string[] + onChange={(val: string[]) => { + stateFasilitasKesehatan.create.form.tarifDanLayanan = val; + }} + searchable + clearable + required + /> + {/* Prosedur Pendaftaran */} Prosedur Pendaftaran @@ -176,38 +263,35 @@ function CreateFasilitasKesehatan() { /> - {/* Tarif dan Layanan */} - - Tarif dan Layanan - (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)} - required - /> - (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)} - required - /> - {/* Submit */} - + + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx index 69da2f21..b21142cf 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/edit/page.tsx @@ -1,11 +1,298 @@ -import React from 'react'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; + +import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; +import colors from '@/con/colors'; +import { + ActionIcon, + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { TimeInput } from '@mantine/dates'; +import { IconArrowBack, IconClock } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + + +function EditDokterTenagaMedis() { + const state = useProxy(fasilitasKesehatanState.dokter); + const router = useRouter(); + const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.specialist?.trim() !== '' && + formData.jadwal?.trim() !== '' && + formData.jadwalLibur?.trim() !== '' && + formData.jamBukaOperasional !== '' && + formData.jamTutupOperasional !== '' && + formData.jamBukaLibur !== '' && + formData.jamTutupLibur !== '' + ); + }; + + const [formData, setFormData] = useState({ + name: '', + specialist: '', + jadwal: '', + jadwalLibur: '', + jamBukaOperasional: '', + jamTutupOperasional: '', + jamBukaLibur: '', + jamTutupLibur: '', + }); + + const [originalData, setOriginalData] = useState({ + name: '', + specialist: '', + jadwal: '', + jadwalLibur: '', + jamBukaOperasional: '', + jamTutupOperasional: '', + jamBukaLibur: '', + jamTutupLibur: '', + }); + + // Load data + useEffect(() => { + const load = async () => { + const id = params?.id as string; + if (!id) return; + + try { + await state.update.load(id); + const loadedData = state.update.form; + + if (!loadedData) { + toast.error('Data tidak ditemukan'); + return; + } + + setFormData(loadedData); + setOriginalData(loadedData); + } catch (err) { + console.error(err); + toast.error('Gagal memuat data fasilitas kesehatan'); + } + }; + + load(); + }, [params?.id]); + + const handleResetForm = () => { + setFormData({ + name: originalData.name, + specialist: originalData.specialist, + jadwal: originalData.jadwal, + jadwalLibur: originalData.jadwalLibur, + jamBukaOperasional: originalData.jamBukaOperasional, + jamTutupOperasional: originalData.jamTutupOperasional, + jamBukaLibur: originalData.jamBukaLibur, + jamTutupLibur: originalData.jamTutupLibur, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const refBuka = useRef(null); + const refTutup = useRef(null); + const refBukaLibur = useRef(null); + const refTutupLibur = useRef(null); + + const picker = (ref: any) => ( + ref.current?.showPicker()}> + + + ); + + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + // Submit + const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Nama dokter wajib diisi'); + return; + } + + if (!formData.specialist?.trim()) { + toast.error('Specialist wajib diisi'); + return; + } + + if (!formData.jadwal?.trim()) { + toast.error('Jadwal wajib diisi'); + return; + } + + if (!formData.jadwalLibur?.trim()) { + toast.error('Jadwal libur wajib diisi'); + return; + } + + if (!formData.jamBukaOperasional) { + toast.error('Jam buka operasional wajib diisi'); + return; + } + + if (!formData.jamTutupOperasional) { + toast.error('Jam tutup operasional wajib diisi'); + return; + } + + if (!formData.jamBukaLibur) { + toast.error('Jam buka hari libur wajib diisi'); + return; + } + + if (!formData.jamTutupLibur) { + toast.error('Jam tutup hari libur wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + state.update.form = { ...state.update.form, ...formData }; + const success = await state.update.submit(); + if (success) { + toast.success('Data berhasil diperbarui!'); + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis'); + } + } catch (err) { + console.error(err); + toast.error('Terjadi kesalahan saat memperbarui data'); + } finally { + setIsSubmitting(false); + } + }; -function Page() { return ( -
- Page -
+ + {/* Header */} + + + + Edit Fasilitas Kesehatan + + + + {/* Form */} + + + handleChange("name", e.target.value)} + required + /> + + handleChange("jadwal", e.target.value)} + required + /> + + handleChange("jadwalLibur", e.target.value)} + required + /> + + handleChange("jamBukaOperasional", e.target.value)} + required + /> + + handleChange("jamTutupOperasional", e.target.value)} + required + /> + + handleChange("jamBukaLibur", e.target.value)} + required + /> + + handleChange("jamTutupLibur", e.target.value)} + required + /> + {/* Tombol Simpan */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + ); } -export default Page; +export default EditDokterTenagaMedis; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx index 69da2f21..173a4156 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/[id]/page.tsx @@ -1,11 +1,165 @@ -import React from 'react'; +'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Paper, + Skeleton, + Stack, + Text +} from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + +function DetailDokterTenagaMedis() { + const params = useParams(); + const router = useRouter(); + const state = useProxy(fasilitasKesehatanState.dokter); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); + + const handleHapus = () => { + if (selectedId) { + state.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push( + '/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis' + ); + } + }; + + if (!state.findUnique.data) { + return ( + + + + ); + } + + const data = state.findUnique.data; -function Page() { return ( -
- Page -
+ + {/* Tombol Back */} + + + {/* Wrapper Detail */} + + + + Detail Dokter & Tenaga Medis + + + + + + Nama Dokter + {data.name || '-'} + + + + Specialist + {data.specialist || '-'} + + + + Jadwal + + + + + Jadwal Libur + + + + + Jam Buka Operasional + + + + + Jam Tutup Operasional + + + + + Jam Buka Libur + + + + + Jam Tutup Libur + + + + {/* Aksi */} + + + + + + + + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus dokter & tenaga medis ini?" + /> + ); } -export default Page; +export default DetailDokterTenagaMedis; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx index 71bf934b..4b5f8666 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/create/page.tsx @@ -1,71 +1,243 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' -import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; +import { ActionIcon, Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core'; +import { TimeInput } from '@mantine/dates'; +import { IconArrowBack, IconClock } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useRef, useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateDokter() { - const params = useParams() const createState = useProxy(fasilitasKesehatanState.dokter) const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.create.form.name?.trim() !== '' && + createState.create.create.form.specialist?.trim() !== '' && + createState.create.create.form.jadwal?.trim() !== '' && + createState.create.create.form.jadwalLibur?.trim() !== '' && + createState.create.create.form.jamBukaOperasional !== '' && + createState.create.create.form.jamTutupOperasional !== '' && + createState.create.create.form.jamBukaLibur !== '' && + createState.create.create.form.jamTutupLibur !== '' + ); + }; const resetForm = () => { createState.create.create.form = { name: "", specialist: "", jadwal: "", + jadwalLibur: "", + jamBukaOperasional: "", + jamTutupOperasional: "", + jamBukaLibur: "", + jamTutupLibur: "", }; }; - const handleSubmit = async () => { - await createState.create.create.create(); - resetForm(); - router.push(`/admin/kesehatan/fasilitas-kesehatan/${params?.id}/dokter-tenaga-medis`) + const refBuka = useRef(null); + const refTutup = useRef(null); + const refBukaLibur = useRef(null); + const refTutupLibur = useRef(null); + + const picker = (ref: any) => ( + ref.current?.showPicker()}> + + + ); + + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); // Prevent default form submission + + if (!createState.create.create.form.name?.trim()) { + toast.error('Nama dokter wajib diisi'); + return; + } + + if (!createState.create.create.form.specialist?.trim()) { + toast.error('Specialist wajib diisi'); + return; + } + + if (!createState.create.create.form.jadwal?.trim()) { + toast.error('Jadwal wajib diisi'); + return; + } + + if (!createState.create.create.form.jadwalLibur?.trim()) { + toast.error('Jadwal libur wajib diisi'); + return; + } + + if (!createState.create.create.form.jamBukaOperasional) { + toast.error('Jam buka operasional wajib diisi'); + return; + } + + if (!createState.create.create.form.jamTutupOperasional) { + toast.error('Jam tutup operasional wajib diisi'); + return; + } + + if (!createState.create.create.form.jamBukaLibur) { + toast.error('Jam buka hari libur wajib diisi'); + return; + } + + if (!createState.create.create.form.jamTutupLibur) { + toast.error('Jam tutup hari libur wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + await createState.create.create.create(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis`) + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data'); + } finally { + setIsSubmitting(false); + } }; return ( - - - - + + Tambah Data Dokter & Tenaga Medis + +
- - - Create Dokter + {/* Form */} + + Nama Dokter} - placeholder="masukkan nama dokter" - defaultValue={createState.create.create.form.name} - onChange={(e) => { - createState.create.create.form.name = e.target.value; - }} + label={"Nama Dokter"} + placeholder="Masukkan nama dokter" + value={createState.create.create.form.name} + onChange={(e) => (createState.create.create.form.name = e.target.value)} + required /> - Specialist + + {/* Informasi Umum */} + Specialist} - placeholder="masukkan specialist" - defaultValue={createState.create.create.form.specialist} - onChange={(e) => { - createState.create.create.form.specialist = e.target.value; - }} + label="Specialist" + placeholder="Masukkan specialist" + value={createState.create.create.form.specialist} + onChange={(e) => (createState.create.create.form.specialist = e.target.value)} + required /> - - Jadwal - { - createState.create.create.form.jadwal = htmlContent; + + (createState.create.create.form.jadwal = e.target.value)} + required + /> + + (createState.create.create.form.jadwalLibur = e.target.value)} + required + /> + + (createState.create.create.form.jamBukaOperasional = e.target.value)} + required + /> + + (createState.create.create.form.jamTutupOperasional = e.target.value)} + required + /> + + (createState.create.create.form.jamBukaLibur = e.target.value)} + required + /> + + (createState.create.create.form.jamTutupLibur = e.target.value)} + required + /> + + {/* Submit */} + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + > + {isSubmitting ? : 'Simpan'} + +
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx index 94a1ad90..28f3aba9 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis/page.tsx @@ -1,24 +1,22 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; -import JudulList from '@/app/admin/(dashboard)/_com/judulList'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import { useState } from 'react'; - function DokterTenagaMedis() { const [search, setSearch] = useState(""); const router = useRouter(); return ( - - @@ -45,68 +43,163 @@ function ListDokterTenagaMedis({ search }: { search: string }) { totalPages } = stateFasilitasKesehatan.findMany + const [debouncedSearch] = useDebouncedValue(search, 1000); + useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch) + }, [page, debouncedSearch]) const filteredData = data || [] if (loading || !data) { return ( - + ) } + return ( - - - - - - - - - Fasilitas Kesehatan - Alamat - Jam Operasional - Detail - - - - {filteredData.map((item) => ( + + + {/* Judul + Tombol Tambah */} + + Daftar Dokter dan Tenaga Medis + Daftar Dokter dan Tenaga Medis + + + + {/* Desktop Table */} + +
+ + + Nama Dokter + Spesialis + Jadwal + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( - {item.name} - {item.specialist} - + + {item.name} + - - ))} - -
-
-
+ )) + ) : ( + + +
+ + Tidak ada fasilitas kesehatan yang cocok + +
+
+
+ )} + + +
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + Nama Dokter + {item.name} + + + Spesialis + {item.specialist || '-'} + + + Jadwal + + + + + )) + ) : ( +
+ + Tidak ada fasilitas kesehatan yang cocok + +
+ )} +
+ + {/* Pagination */}
load(newPage)} // ini penting! + onChange={(newPage) => { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} total={totalPages} mt="md" mb="md" + color="blue" + radius="md" />
) } -export default DokterTenagaMedis; +export default DokterTenagaMedis; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx index 8c554c6a..93e96eb7 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/page.tsx @@ -1,12 +1,13 @@ 'use client' import colors from '@/con/colors'; import { + ActionIcon, Box, Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -16,96 +17,168 @@ import { TableThead, TableTr, Text, + TextInput, Title, - Tooltip, + Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconCoin, IconDeviceImacCog, IconPlus, IconReportMedical, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import HeaderSearch from '../../../_com/header'; import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; - function FasilitasKesehatan() { - const router = useRouter(); const [search, setSearch] = useState(""); - + const router = useRouter(); return ( - {/* Tombol Back */} - - - + + + + Fasilitas Kesehatan + + + Fasilitas Kesehatan + + + + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')} + size="lg" + radius="xl" + color="green.6" + > + + + + + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')} + size="lg" + radius="xl" + color="blue.6" + > + + + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + fz={{ base: 'xs', md: 'sm' }} + px="sm" + py="xs" + /> + + - {/* Header Search */} - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> + + + Fasilitas Kesehatan + + + Fasilitas Kesehatan + + + + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis')} + size="lg" + radius="xl" + color="green.6" + > + + + + + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan')} + size="lg" + radius="xl" + color="blue.6" + > + + + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + fz={{ base: 'xs', md: 'sm' }} + px="sm" + py="xs" + /> + + + ); } - function ListFasilitasKesehatan({ search }: { search: string }) { - const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) + const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } return ( - - - {/* Judul + Tombol Tambah */} + + - Daftar Fasilitas Kesehatan - - - + + Daftar Fasilitas Kesehatan + + + Daftar Fasilitas Kesehatan + + - {/* Tabel */} - - + {/* Desktop Table */} + +
Fasilitas Kesehatan - Dokter - Layanan + Jumlah Dokter + Jumlah Layanan Aksi @@ -114,23 +187,23 @@ function ListFasilitasKesehatan({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - {item.dokterdantenagamedis?.name || '-'} - + + {item.dokterdantenagamedis?.length + ? `${item.dokterdantenagamedis.length} dokter` + : '-'} + - - - {item.tarifdanlayanan?.layanan || '-'} - - + + {item.tarifdanlayanan?.length + ? `${item.tarifdanlayanan.length} layanan` + : '-'} +
+ + {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Fasilitas Kesehatan + + + {item.name} + + + + + Jumlah Dokter + + + {item.dokterdantenagamedis?.length + ? `${item.dokterdantenagamedis.length} dokter` + : '-'} + + + + + Jumlah Layanan + + + {item.tarifdanlayanan?.length + ? `${item.tarifdanlayanan.length} layanan` + : '-'} + + + + + )) + ) : ( +
+ + Tidak ada fasilitas kesehatan yang cocok + +
+ )} +
{/* Pagination */} @@ -180,7 +315,7 @@ function ListFasilitasKesehatan({ search }: { search: string }) { />
- ) + ); } -export default FasilitasKesehatan; +export default FasilitasKesehatan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx new file mode 100644 index 00000000..4f291b8d --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/[id]/page.tsx @@ -0,0 +1,194 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' + +import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditTarifLayanan() { + const editState = useProxy(fasilitasKesehatanState.tarif); + const router = useRouter(); + const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.layanan?.trim() !== '' && + formData.tarif?.trim() !== '' + ); + }; + + const [originalData, setOriginalData] = useState({ + tarif: '', + layanan: '' + }); + + const [formData, setFormData] = useState({ + tarif: '', + layanan: '' + }); + + useEffect(() => { + const loadTarifLayanan = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.update.load(id); + if (data) { + setFormData({ + tarif: data.tarif || '', + layanan: data.layanan || '', + }); + setOriginalData({ + tarif: data.tarif || '', + layanan: data.layanan || '', + }); + } + } catch (error) { + console.error('Error loading tarif layanan:', error); + toast.error('Gagal memuat data tarif layanan'); + } + }; + + loadTarifLayanan(); + }, [params?.id]); + + const handleChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData({ + tarif: originalData.tarif, + layanan: originalData.layanan, + }); + toast.info('Form dikembalikan ke data awal'); + }; + + const handleSubmit = async () => { + if (!formData.layanan?.trim()) { + toast.error('Layanan wajib diisi'); + return; + } + + if (!formData.tarif?.trim()) { + toast.error('Tarif wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + // update global state hanya saat submit + editState.update.form = { + ...editState.update.form, + tarif: formData.tarif, + layanan: formData.layanan, + }; + + await editState.update.submit(); + toast.success('Tarif Layanan berhasil diperbarui!'); + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan'); + } catch (error) { + console.error('Error updating tarif layanan:', error); + toast.error('Terjadi kesalahan saat memperbarui tarif layanan'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Back Button + Title */} + + + + Edit Tarif Layanan + + + + {/* Form Wrapper */} + + + handleChange('layanan', e.target.value)} + required + /> + + handleChange('tarif', e.target.value)} + required + /> + + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default EditTarifLayanan; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx new file mode 100644 index 00000000..94aaee98 --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/create/page.tsx @@ -0,0 +1,140 @@ +'use client'; +import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function CreateTarifLayanan() { + const createState = useProxy(fasilitasKesehatanState.tarif); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.form.layanan?.trim() !== '' && + createState.create.form.tarif?.trim() !== '' + ); + }; + + const resetForm = () => { + createState.create.form = { + tarif: '', + layanan: '', + }; + }; + + const handleSubmit = async () => { + if (!createState.create.form.layanan?.trim()) { + toast.error('Layanan wajib diisi'); + return; + } + + if (!createState.create.form.tarif?.trim()) { + toast.error('Tarif wajib diisi'); + return; + } + + setIsSubmitting(true); + try { + await createState.create.create(); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan'); + } catch (error) { + console.error('Error creating tarif layanan:', error); + toast.error('Gagal menambahkan tarif layanan'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Header dengan back button */} + + + + Tambah Tarif Layanan + + + + {/* Form utama */} + + + (createState.create.form.layanan = e.target.value)} + required + /> + (createState.create.form.tarif = e.target.value)} + required + /> + + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreateTarifLayanan; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx index eb82a5f9..93e71fd6 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan/page.tsx @@ -1,29 +1,28 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; -import JudulList from '@/app/admin/(dashboard)/_com/judulList'; +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import { useState } from 'react'; - function TarifLayanan() { const [search, setSearch] = useState(""); const router = useRouter(); return ( - - } value={search} @@ -35,78 +34,236 @@ function TarifLayanan() { } function ListTarifLayanan({ search }: { search: string }) { - const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.dokter) + const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.tarif); const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [debouncedSearch] = useDebouncedValue(search, 1000); + const { data, loading, load, page, totalPages - } = stateFasilitasKesehatan.findMany + } = stateFasilitasKesehatan.findMany; useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const handleDelete = () => { + if (selectedId) { + stateFasilitasKesehatan.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + load(page, 10, debouncedSearch); + } + }; + + const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } + return ( - - - - - - - - - Fasilitas Kesehatan - Alamat - Jam Operasional - Detail - - - - {filteredData.map((item) => ( + + + {/* Judul + Tombol Tambah */} + + + Daftar Tarif dan Layanan + + + + + {/* Desktop Table */} + +
+ + + + + Layanan + + + + + Tarif + + + + + Edit + + + + + Hapus + + + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( - {item.name} - {item.specialist} - + + {item.layanan || '-'} + - + + + - ))} - -
-
-
+ )) + ) : ( + + +
+ + Tidak ada fasilitas kesehatan yang cocok + +
+
+
+ )} + + +
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Layanan + + + {item.layanan || '-'} + + + + + Tarif + + + {item.tarif} + + + + + + + + )) + ) : ( +
+ + Tidak ada fasilitas kesehatan yang cocok + +
+ )} +
+ + {/* Pagination */}
load(newPage)} // ini penting! + onChange={(newPage) => { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} total={totalPages} mt="md" mb="md" + color="blue" + radius="md" />
+ + setModalHapus(false)} + onConfirm={handleDelete} + text="Apakah anda yakin ingin menghapus tarif layanan ini?" + />
- ) + ); } -export default TarifLayanan; +export default TarifLayanan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx deleted file mode 100644 index f6501312..00000000 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/edit/page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' - -import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; -import colors from '@/con/colors'; -import { - Box, - Button, - Group, - Paper, - Stack, - TextInput, - Title, - Tooltip -} from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { useProxy } from 'valtio/utils'; - -function EditGrafikHasilKepuasan() { - const editState = useProxy(grafikkepuasan); - const router = useRouter(); - const params = useParams(); - - const [formData, setFormData] = useState({ - nama: '', - tanggal: '', - jenisKelamin: '', - alamat: '', - penyakit: '', - }); - - // Load data once - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) return; - - try { - const data = await editState.update.load(id); - if (data) setFormData({ - nama: data.nama || '', - tanggal: data.tanggal || '', - jenisKelamin: data.jenisKelamin || '', - alamat: data.alamat || '', - penyakit: data.penyakit || '', - }); - } catch (err) { - console.error("Error loading grafik hasil kepuasan:", err); - toast.error("Gagal memuat data grafik hasil kepuasan"); - } - }; - - loadData(); - }, [params?.id]); - - // Generic handler for controlled inputs - const handleChange = (field: keyof typeof formData, value: string) => { - setFormData((prev) => ({ ...prev, [field]: value })); - }; - - const handleSubmit = async () => { - try { - editState.update.form = { ...editState.update.form, ...formData }; - await editState.update.submit(); - toast.success('Grafik hasil kepuasan berhasil diperbarui!'); - router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan'); - } catch (err) { - console.error('Error updating grafik hasil kepuasan:', err); - toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan'); - } - }; - - return ( - - {/* Header */} - - - - - - Edit Grafik Hasil Kepuasan - - - - {/* Form */} - - - {(['nama','tanggal','jenisKelamin','alamat','penyakit'] as const).map((field) => ( - handleChange(field, e.target.value)} - type={field === 'tanggal' ? 'date' : 'text'} - label={field === 'jenisKelamin' ? 'Jenis Kelamin' : field.charAt(0).toUpperCase() + field.slice(1)} - placeholder={`Masukkan ${field}`} - required - /> - ))} - - - - - - - - ); -} - -export default EditGrafikHasilKepuasan; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx deleted file mode 100644 index 1897aba3..00000000 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/page.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client' -import colors from '@/con/colors'; -import { - Box, - Button, - Center, - Group, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip -} from '@mantine/core'; -import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; -import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; -import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { Bar, BarChart, Tooltip as ChartTooltip, Legend, XAxis, YAxis } from 'recharts'; -import { useProxy } from 'valtio/utils'; -import HeaderSearch from '../../../_com/header'; -import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; - -function GrafikHasilKepuasanMasyarakat() { - const [search, setSearch] = useState(""); - - return ( - - {/* Header Search */} - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - - ); -} - -function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { - type PDKMGrafik = { - id: string; - nama: string; - tanggal: string | Date; - jenisKelamin: string; - alamat: string; - penyakit: string; - }; - - const stateGrafikKepuasan = useProxy(grafikkepuasan); - const [chartData, setChartData] = useState([]); - const [mounted, setMounted] = useState(false); - const isTablet = useMediaQuery('(max-width: 1024px)'); - const isMobile = useMediaQuery('(max-width: 768px)'); - const router = useRouter(); - - const { data, page, totalPages, loading, load } = stateGrafikKepuasan.findMany; - - useShallowEffect(() => { - setMounted(true); - load(page, 10, search); - }, [page, search]); - - useEffect(() => { - if (data) { - setChartData(data.map((item) => ({ - ...item, - tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal - }))); - } - }, [data]); - - const processDiseaseData = (data: PDKMGrafik[]) => { - const diseaseCount: Record = {}; - data.forEach(item => { - const penyakit = item.penyakit.trim(); - if (penyakit) { - diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1; - } - }); - return Object.entries(diseaseCount).map(([name, count]) => ({ name, count })); - }; - - const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]); - - useEffect(() => { - if (data && data.length > 0) { - setDiseaseChartData(processDiseaseData(data)); - } - }, [data]); - - const filteredData = data || []; - - if (loading || !data) { - return ( - - - - ); - } - - return ( - - - {/* Judul + Tombol Tambah */} - - Daftar Grafik Hasil Kepuasan Masyarakat - - - - - - {/* Tabel */} - - - - - Nama - Tanggal - Jenis Kelamin - Penyakit - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - {item.nama} - - - - - {new Date(item.tanggal).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric', - })} - - - - - {item.jenisKelamin} - - - - - {item.penyakit} - - - - - - - )) - ) : ( - - -
- - Tidak ada data kepuasan masyarakat yang cocok - -
-
-
- )} -
-
-
-
- - {/* Pagination */} -
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
- - {/* Chart */} - - - Grafik Hasil Kepuasan Masyarakat - {mounted && diseaseChartData.length > 0 ? ( -
- - - - - - - -
- ) : ( - Belum ada data untuk ditampilkan dalam grafik - )} -
-
-
- ); -} - -export default GrafikHasilKepuasanMasyarakat; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx index de1b5922..e04e0763 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/edit/page.tsx @@ -4,7 +4,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -39,7 +39,29 @@ function EditJadwalKegiatan() { const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState(emptyForm()); + const [originalData, setOriginalData] = useState(emptyForm()); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.content?.trim() !== '' && + formData.informasiJadwalKegiatan.name?.trim() !== '' && + formData.informasiJadwalKegiatan.tanggal?.trim() !== '' && + formData.informasiJadwalKegiatan.waktu?.trim() !== '' && + formData.informasiJadwalKegiatan.lokasi?.trim() !== '' && + !isHtmlEmpty(formData.deskripsiJadwalKegiatan.deskripsi) + ); + }; + // Helper untuk update nested state const updateNested = < @@ -85,6 +107,19 @@ function EditJadwalKegiatan() { syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' }, dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' }, }); + setOriginalData({ + content: form.content || '', + informasiJadwalKegiatan: { + name: form.informasiJadwalKegiatan?.name || '', + tanggal: form.informasiJadwalKegiatan?.tanggal || '', + waktu: form.informasiJadwalKegiatan?.waktu || '', + lokasi: form.informasiJadwalKegiatan?.lokasi || '', + }, + deskripsiJadwalKegiatan: { deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: form.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' }, + }); } } catch (error) { console.error("Error loading jadwal kegiatan:", error); @@ -94,8 +129,26 @@ function EditJadwalKegiatan() { loadJadwalKegiatan(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + content: originalData.content || '', + informasiJadwalKegiatan: { + name: originalData.informasiJadwalKegiatan?.name || '', + tanggal: originalData.informasiJadwalKegiatan?.tanggal || '', + waktu: originalData.informasiJadwalKegiatan?.waktu || '', + lokasi: originalData.informasiJadwalKegiatan?.lokasi || '', + }, + deskripsiJadwalKegiatan: { deskripsi: originalData.deskripsiJadwalKegiatan?.deskripsi || '' }, + layananJadwalKegiatan: { content: originalData.layananJadwalKegiatan?.content || '' }, + syaratKetentuanJadwalKegiatan: { content: originalData.syaratKetentuanJadwalKegiatan?.content || '' }, + dokumenJadwalKegiatan: { content: originalData.dokumenJadwalKegiatan?.content || '' }, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); stateJadwalKegiatan.edit.form = { ...stateJadwalKegiatan.edit.form, ...formData }; const success = await stateJadwalKegiatan.edit.submit(); if (success) { @@ -105,18 +158,18 @@ function EditJadwalKegiatan() { } catch (error) { console.error("Error updating jadwal kegiatan:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan"); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Jadwal Kegiatan @@ -192,17 +245,31 @@ function EditJadwalKegiatan() { {/* Submit */} + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx index 383cc54c..abf361ab 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/[id]/page.tsx @@ -2,7 +2,7 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -40,7 +40,7 @@ function DetailJadwalKegiatan() { const data = stateJadwalKegiatan.findUnique.data return ( - + {/* Tombol Back */} - + - - - +
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx index 55121d4a..824ed933 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create/page.tsx @@ -6,21 +6,42 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateJadwalKegiatan() { const stateJadwalKegiatan = useProxy(jadwalKegiatanState); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + stateJadwalKegiatan.create.form.content?.trim() !== '' && + stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name?.trim() !== '' && + stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal?.trim() !== '' && + stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu?.trim() !== '' && + stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi?.trim() !== '' && + !isHtmlEmpty(stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi) + ); + }; const resetForm = () => { stateJadwalKegiatan.create.form = { @@ -48,27 +69,32 @@ function CreateJadwalKegiatan() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await stateJadwalKegiatan.create.submit(); - - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); + try { + setIsSubmitting(true); + await stateJadwalKegiatan.create.submit(); + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); + } catch (error) { + console.error(error); + toast.error('Gagal menyimpan data jadwal kegiatan'); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + Tambah Jadwal Kegiatan @@ -87,7 +113,7 @@ function CreateJadwalKegiatan() { { stateJadwalKegiatan.create.form.content = e.target.value; }} @@ -110,7 +136,7 @@ function CreateJadwalKegiatan() { label="Nama" required placeholder="Masukkan nama" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name = e.target.value; }} @@ -119,7 +145,7 @@ function CreateJadwalKegiatan() { type="date" required label="Tanggal" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal = e.target.value; }} @@ -128,7 +154,7 @@ function CreateJadwalKegiatan() { label="Waktu" required placeholder="Masukkan waktu" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu = e.target.value; }} @@ -137,7 +163,7 @@ function CreateJadwalKegiatan() { label="Lokasi" required placeholder="Masukkan lokasi" - defaultValue={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi} + value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi} onChange={(e) => { stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi = e.target.value; }} @@ -175,17 +201,32 @@ function CreateJadwalKegiatan() { {/* Save Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx index 2114f56f..73003da1 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/jadwal_kegiatan/page.tsx @@ -5,8 +5,8 @@ import { Button, Center, Group, - Paper, Pagination, + Paper, Skeleton, Stack, Table, @@ -16,30 +16,21 @@ import { TableThead, TableTr, Text, - Title, - Tooltip, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../../_com/header'; import jadwalKegiatanState from '../../../_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; -import { useState } from 'react'; function JadwalKegiatan() { - const router = useRouter(); const [search, setSearch] = useState(""); return ( - {/* Tombol Back */} - - - - {/* Header Search */} { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - + {/* Judul + Tombol Tambah */} Daftar Jadwal Kegiatan - - - + - {/* Tabel */} - - + {/* Desktop Table */} + +
- Nama - Tanggal - Waktu - Lokasi - Aksi + Nama + Tanggal + Waktu + Lokasi + Aksi @@ -111,35 +103,31 @@ function ListJadwalKegiatan({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.informasijadwalkegiatan.name} - - + + {item.informasijadwalkegiatan.name} + - - {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( - 'id-ID', - { - day: '2-digit', - month: 'long', - year: 'numeric', - } - )} - + + {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( + 'id-ID', + { + day: '2-digit', + month: 'long', + year: 'numeric', + } + )} + - + {item.informasijadwalkegiatan.waktu} - + - - - {item.informasijadwalkegiatan.lokasi} - - + + {item.informasijadwalkegiatan.lokasi} + @@ -160,8 +150,8 @@ function ListJadwalKegiatan({ search }: { search: string }) { ) : ( -
- +
+ Tidak ada jadwal kegiatan yang cocok
@@ -171,6 +161,72 @@ function ListJadwalKegiatan({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + + {item.informasijadwalkegiatan.name} + + + + Tanggal + + {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString( + 'id-ID', + { + day: '2-digit', + month: 'long', + year: 'numeric', + } + )} + + + + Waktu + + {item.informasijadwalkegiatan.waktu} + + + + Lokasi + + {item.informasijadwalkegiatan.lokasi} + + + + + + + + )) + ) : ( +
+ + Tidak ada jadwal kegiatan yang cocok + +
+ )} +
{/* Pagination */} @@ -192,4 +248,4 @@ function ListJadwalKegiatan({ search }: { search: string }) { ); } -export default JadwalKegiatan; +export default JadwalKegiatan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx index 9ebad0e4..db66e722 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/layout.tsx @@ -1,9 +1,29 @@ 'use client' +import { usePathname } from "next/navigation"; import LayoutTabs from "./_lib/layoutTabs" +import { Box } from "@mantine/core"; -export default function Layout({children} : {children: React.ReactNode}) { +export default function Layout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + + // Contoh path: + // - /darmasaba/desa/berita/semua → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list + // - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail + + const segments = pathname.split('/').filter(Boolean); + const isDetailPage = segments.length >= 5; + + if (isDetailPage) { + // Tampilkan tanpa tab menu + return ( + + {children} + + ); + } return ( {children} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx new file mode 100644 index 00000000..ef9e30b4 --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/edit/page.tsx @@ -0,0 +1,221 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' + +import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; +import colors from '@/con/colors'; +import { + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title +} from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import { convertToISODate } from '../../../persentase_data_kelahiran_kematian/lib/dateUtils'; + +function EditGrafikHasilKepuasan() { + const editState = useProxy(grafikkepuasan); + const router = useRouter(); + const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.tanggal !== '' && + formData.jenisKelamin?.trim() !== '' && + formData.alamat?.trim() !== '' && + formData.penyakit?.trim() !== '' + ); + }; + + const [formData, setFormData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyakit: '', + }); + + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyakit: '', + }); + + // Load data once + useEffect(() => { + const loadData = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await editState.update.load(id); + if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + + setFormData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyakit: data.penyakit || '', + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyakit: data.penyakit || '', + }); + } + } catch (err) { + console.error("Error loading penderita penyakit:", err); + toast.error("Gagal memuat data penderita penyakit"); + } + }; + + loadData(); + }, [params?.id]); + + // Generic handler for controlled inputs + const handleChange = (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + penyakit: originalData.penyakit, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + if (!formData.nama?.trim()) { + toast.error('Nama wajib diisi'); + return; + } + + if (!formData.tanggal) { + toast.error('Tanggal wajib diisi'); + return; + } + + if (!formData.jenisKelamin?.trim()) { + toast.error('Jenis kelamin wajib diisi'); + return; + } + + if (!formData.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (!formData.penyakit?.trim()) { + toast.error('Penyakit wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + editState.update.form = { ...editState.update.form, ...formData }; + await editState.update.submit(); + toast.success('penderita penyakit berhasil diperbarui!'); + router.push('/admin/kesehatan/data-kesehatan-warga/penderita_penyakit'); + } catch (err) { + console.error('Error updating penderita penyakit:', err); + toast.error('Terjadi kesalahan saat memperbarui penderita penyakit'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + {/* Header */} + + + + Edit Penderita Penyakit + + + + {/* Form */} + + + {(['nama', 'tanggal', 'jenisKelamin', 'alamat', 'penyakit'] as const).map((field) => ( + handleChange(field, e.target.value)} + type={field === 'tanggal' ? 'date' : 'text'} + label={field === 'jenisKelamin' ? 'Jenis Kelamin' : field.charAt(0).toUpperCase() + field.slice(1)} + placeholder={`Masukkan ${field}`} + required + /> + ))} + + + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default EditGrafikHasilKepuasan; diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/page.tsx similarity index 71% rename from src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx rename to src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/page.tsx index c80da5f8..89b9c8f6 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; @@ -26,7 +26,7 @@ function DetailGrafikHasilKepuasan() { state.delete.byId(selectedId); setModalHapus(false); setSelectedId(null); - router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); + router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit"); } }; @@ -41,7 +41,7 @@ function DetailGrafikHasilKepuasan() { const data = state.findUnique.data; return ( - + {/* Tombol Back */} - + - - - +
diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx similarity index 51% rename from src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx rename to src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx index e33b6e1a..caf68c3e 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/create/page.tsx @@ -7,22 +7,34 @@ import { Box, Button, Group, + Loader, Paper, Stack, - Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateGrafikHasilKepuasanMasyarakat() { const stateGrafikKepuasan = useProxy(grafikkepuasan); const [chartData, setChartData] = useState([]); const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateGrafikKepuasan.create.form.nama?.trim() !== '' && + stateGrafikKepuasan.create.form.tanggal !== '' && + stateGrafikKepuasan.create.form.jenisKelamin?.trim() !== '' && + stateGrafikKepuasan.create.form.alamat?.trim() !== '' && + stateGrafikKepuasan.create.form.penyakit?.trim() !== '' + ); + }; const resetForm = () => { stateGrafikKepuasan.create.form = { @@ -35,27 +47,58 @@ function CreateGrafikHasilKepuasanMasyarakat() { }; const handleSubmit = async () => { - await stateGrafikKepuasan.create.create(); - resetForm(); - router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); + if (!stateGrafikKepuasan.create.form.nama?.trim()) { + toast.error('Nama wajib diisi'); + return; + } + + if (!stateGrafikKepuasan.create.form.tanggal) { + toast.error('Tanggal wajib diisi'); + return; + } + + if (!stateGrafikKepuasan.create.form.jenisKelamin?.trim()) { + toast.error('Jenis kelamin wajib diisi'); + return; + } + + if (!stateGrafikKepuasan.create.form.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (!stateGrafikKepuasan.create.form.penyakit?.trim()) { + toast.error('Penyakit wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + await stateGrafikKepuasan.create.create(); + resetForm(); + router.push("/admin/kesehatan/data-kesehatan-warga/penderita_penyakit"); + } catch (error) { + console.error("Error creating grafik kepuasan:", error); + toast.error("Terjadi kesalahan saat membuat grafik kepuasan"); + } finally { + setIsSubmitting(false); + } }; return ( - + {/* Header */} - - - + - Tambah Grafik Hasil Kepuasan Masyarakat + Tambah Penderita Penyakit @@ -72,7 +115,7 @@ function CreateGrafikHasilKepuasanMasyarakat() { (stateGrafikKepuasan.create.form.nama = e.target.value)} required /> @@ -80,44 +123,58 @@ function CreateGrafikHasilKepuasanMasyarakat() { type="date" label="Tanggal" placeholder="Masukkan tanggal" - defaultValue={stateGrafikKepuasan.create.form.tanggal} + value={stateGrafikKepuasan.create.form.tanggal} onChange={(e) => (stateGrafikKepuasan.create.form.tanggal = e.target.value)} required /> (stateGrafikKepuasan.create.form.jenisKelamin = e.target.value)} required /> (stateGrafikKepuasan.create.form.alamat = e.target.value)} required /> (stateGrafikKepuasan.create.form.penyakit = e.target.value)} required /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/page.tsx new file mode 100644 index 00000000..c27ed736 --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/penderita_penyakit/page.tsx @@ -0,0 +1,342 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; +import colors from '@/con/colors'; +import { + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title, +} from '@mantine/core'; +import { useDebouncedValue, useMediaQuery, useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { + Bar, + BarChart, + Tooltip as ChartTooltip, + Legend, + XAxis, + YAxis, +} from 'recharts'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; + +function GrafikHasilKepuasanMasyarakat() { + const [search, setSearch] = useState(''); + + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + + ); +} + +function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { + type PDKMGrafik = { + id: string; + nama: string; + tanggal: string | Date; + jenisKelamin: string; + alamat: string; + penyakit: string; + }; + + const stateGrafikKepuasan = useProxy(grafikkepuasan); + const [debouncedSearch] = useDebouncedValue(search, 1000); + const [chartData, setChartData] = useState([]); + const [mounted, setMounted] = useState(false); + const isMobile = useMediaQuery('(max-width: 768px)'); + const router = useRouter(); + + const { data, page, totalPages, loading, load } = stateGrafikKepuasan.findMany; + + useShallowEffect(() => { + setMounted(true); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + useEffect(() => { + if (data) { + setChartData( + data.map((item) => ({ + ...item, + tanggal: + item.tanggal instanceof Date + ? item.tanggal.toISOString() + : item.tanggal, + })) + ); + } + }, [data]); + + const processDiseaseData = (data: PDKMGrafik[]) => { + const diseaseCount: Record = {}; + data.forEach((item) => { + const penyakit = item.penyakit.trim(); + if (penyakit) { + diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1; + } + }); + return Object.entries(diseaseCount).map(([name, count]) => ({ name, count })); + }; + + const [diseaseChartData, setDiseaseChartData] = useState<{ name: string; count: number }[]>([]); + + useEffect(() => { + if (data && data.length > 0) { + setDiseaseChartData(processDiseaseData(data)); + } + }, [data]); + + const filteredData = data || []; + + if (loading || !data) { + return ( + + + + ); + } + + return ( + + {/* Daftar Penderita Penyakit */} + + + + Daftar Penderita Penyakit + + + + + {/* Desktop Table */} + + + + + Nama + Tanggal + Jenis Kelamin + Penyakit + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + {item.nama} + + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + {item.jenisKelamin} + + + {item.penyakit} + + + + + + )) + ) : ( + + +
+ + Tidak ada data kepuasan masyarakat yang cocok + +
+
+
+ )} +
+
+
+ + {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + + + {item.nama} + + + + Tanggal + + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + Jenis Kelamin + + + {item.jenisKelamin} + + + + Penyakit + + + {item.penyakit} + + + + + + )) + ) : ( +
+ + Tidak ada data kepuasan masyarakat yang cocok + +
+ )} +
+
+ + {/* Pagination */} +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+ + {/* Chart Section */} + + + Penderita Penyakit + + + {mounted && diseaseChartData.length > 0 ? ( +
+ + + + + + + +
+ ) : ( +
+ + Belum ada data untuk ditampilkan dalam grafik + +
+ )} +
+
+ ); +} + +export default GrafikHasilKepuasanMasyarakat; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx index ac5e2805..de7c964e 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/edit/page.tsx @@ -7,22 +7,34 @@ import { Box, Button, Group, + Loader, Paper, Stack, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +import { convertToISODate } from '../../../lib/dateUtils'; function EditKelahiran() { const editState = useProxy(persentaseKelahiranKematian.kelahiran); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.tanggal !== '' && + formData.jenisKelamin?.trim() !== '' && + formData.alamat?.trim() !== '' + ); + }; const [formData, setFormData] = useState({ nama: '', @@ -31,26 +43,44 @@ function EditKelahiran() { alamat: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + }); + // Load data saat mount atau params.id berubah useEffect(() => { const loadKelahiran = async () => { const id = params?.id as string; if (!id) return; - + try { const data = await editState.edit.load(id); - if (data) setFormData({ - nama: data.nama || '', - tanggal: data.tanggal || '', - jenisKelamin: data.jenisKelamin || '', - alamat: data.alamat || '' - }); + if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + + setFormData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '' + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '' + }); + } } catch (error) { console.error('Error loading data kelahiran:', error); toast.error('Gagal memuat data kelahiran'); } }; - + loadKelahiran(); }, [params?.id]); @@ -58,8 +88,40 @@ function EditKelahiran() { setFormData((prev) => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + }); + toast.info("Form dikembalikan ke data awal"); + }; + + const handleSubmit = async () => { + if (!formData.nama?.trim()) { + toast.error('Nama wajib diisi'); + return; + } + + if (!formData.tanggal) { + toast.error('Tanggal wajib diisi'); + return; + } + + if (!formData.jenisKelamin?.trim()) { + toast.error('Jenis kelamin wajib diisi'); + return; + } + + if (!formData.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + try { + setIsSubmitting(true); // Update global state hanya saat submit editState.edit.form = { ...editState.edit.form, ...formData }; await editState.edit.update(); @@ -68,18 +130,18 @@ function EditKelahiran() { } catch (error) { console.error('Error updating data kelahiran:', error); toast.error('Terjadi kesalahan saat memperbarui data kelahiran'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Data Kelahiran @@ -126,17 +188,31 @@ function EditKelahiran() { /> + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx index 18a2e587..3ec0a1bf 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -13,151 +13,147 @@ import colors from '@/con/colors'; function DetailKelahiran() { - const state = useProxy(persentaseKelahiranKematian.kelahiran); - const [modalHapus, setModalHapus] = useState(false); - const [selectedId, setSelectedId] = useState(null); - const params = useParams(); - const router = useRouter(); + const state = useProxy(persentaseKelahiranKematian.kelahiran); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const params = useParams(); + const router = useRouter(); - useShallowEffect(() => { - state.findUnique.load(params?.id as string); - }, []); + useShallowEffect(() => { + state.findUnique.load(params?.id as string); + }, []); - const handleHapus = () => { - if (selectedId) { - state.delete.byId(selectedId); - setModalHapus(false); - setSelectedId(null); - router.push( - "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran" - ); - } - }; + const handleHapus = () => { + if (selectedId) { + state.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push( + "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran" + ); + } + }; - if (!state.findUnique.data) { - return ( - - - - ); - } + if (!state.findUnique.data) { + return ( + + + + ); + } - const data = state.findUnique.data; + const data = state.findUnique.data; - return ( - - {/* Tombol Back */} - + return ( + + {/* Tombol Back */} + - {/* Wrapper Detail */} - - - - Detail Data Kelahiran - + {/* Wrapper Detail */} + + + + Detail Data Kelahiran + - - - - Nama - {data.nama || '-'} - + + + + Nama + {data.nama || '-'} + - - Tanggal - - {new Date(data.tanggal).toLocaleDateString("id-ID", { - day: "2-digit", - month: "long", - year: "numeric", - })} - - + + Tanggal + + {new Date(data.tanggal).toLocaleDateString("id-ID", { + day: "2-digit", + month: "long", + year: "numeric", + })} + + - - Jenis Kelamin - {data.jenisKelamin || '-'} - + + Jenis Kelamin + {data.jenisKelamin || '-'} + - - Alamat - {data.alamat || '-'} - + + Alamat + {data.alamat || '-'} + - {/* Aksi */} - - - - + {/* Aksi */} + + - - - - - - - - + + + + + + - {/* Modal Konfirmasi Hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah anda yakin ingin menghapus data ini?" - /> - - ); + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah anda yakin ingin menghapus data ini?" + /> + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx index b8476a66..5c2d2d5e 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create/page.tsx @@ -2,124 +2,176 @@ import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, + Box, + Button, + Group, + Loader, + Paper, + Stack, + Text, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; function CreateKelahiran() { - const createState = useProxy(persentaseKelahiranKematian.kelahiran); - const router = useRouter(); + const createState = useProxy(persentaseKelahiranKematian.kelahiran); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.form.nama?.trim() !== '' && + createState.create.form.tanggal !== '' && + createState.create.form.jenisKelamin?.trim() !== '' && + createState.create.form.alamat?.trim() !== '' + ); + }; + + const resetForm = () => { + createState.create.form = { + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + }; + }; - const resetForm = () => { - createState.create.form = { - nama: '', - tanggal: '', - jenisKelamin: '', - alamat: '', - }; - }; + const handleSubmit = async () => { + if (!createState.create.form.nama?.trim()) { + toast.error('Nama wajib diisi'); + return; + } + + if (!createState.create.form.tanggal) { + toast.error('Tanggal wajib diisi'); + return; + } + + if (!createState.create.form.jenisKelamin?.trim()) { + toast.error('Jenis kelamin wajib diisi'); + return; + } + + if (!createState.create.form.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' + ); + } catch (error) { + console.error('Error creating kelahiran:', error); + toast.error('Gagal menambahkan data kelahiran'); + } finally { + setIsSubmitting(false); + } + }; - const handleSubmit = async () => { - await createState.create.create(); - resetForm(); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran' - ); - }; + return ( + + {/* Header */} + + + + Tambah Data Kelahiran + + - return ( - - {/* Header */} - - - - - - Tambah Data Kelahiran - - + {/* Form */} + + + Nama} + placeholder="Masukkan nama" + value={createState.create.form.nama} + onChange={(e) => (createState.create.form.nama = e.target.value)} + required + /> + Tanggal} + placeholder="Masukkan tanggal" + value={createState.create.form.tanggal} + onChange={(e) => (createState.create.form.tanggal = e.target.value)} + required + /> + Jenis Kelamin} + placeholder="Masukkan jenis kelamin" + value={createState.create.form.jenisKelamin} + onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)} + required + /> + Alamat} + placeholder="Masukkan alamat" + value={createState.create.form.alamat} + onChange={(e) => (createState.create.form.alamat = e.target.value)} + required + /> - {/* Form */} - - - Nama} - placeholder="Masukkan nama" - defaultValue={createState.create.form.nama} - onChange={(e) => (createState.create.form.nama = e.target.value)} - required - /> - Tanggal} - placeholder="Masukkan tanggal" - defaultValue={createState.create.form.tanggal} - onChange={(e) => (createState.create.form.tanggal = e.target.value)} - required - /> - Jenis Kelamin} - placeholder="Masukkan jenis kelamin" - defaultValue={createState.create.form.jenisKelamin} - onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)} - required - /> - Alamat} - placeholder="Masukkan alamat" - defaultValue={createState.create.form.alamat} - onChange={(e) => (createState.create.form.alamat = e.target.value)} - required - /> + + - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx index 255ed68d..1892c2e9 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/page.tsx @@ -3,25 +3,24 @@ import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Center, - Group, - Paper, - Pagination, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip, + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -29,179 +28,224 @@ import { useProxy } from 'valtio/utils'; function Kelahiran() { - const router = useRouter(); - const [search, setSearch] = useState(""); + const router = useRouter(); + const [search, setSearch] = useState(""); + return ( + + {/* Tombol Back */} + + + - return ( - - {/* Tombol Back */} - - - + {/* Header Search */} + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> - - {/* Header Search */} - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - - - ); + + + ); } function ListKelahiran({ search }: { search: string }) { - const statePersentase = useProxy(persentasekelahiran.kelahiran); - const router = useRouter(); + const statePersentase = useProxy(persentasekelahiran.kelahiran); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay + const { data, page, totalPages, loading, load } = statePersentase.findMany; - const { data, page, totalPages, loading, load } = statePersentase.findMany; + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + const filteredData = data || []; - useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + if (loading || !data) { + return ( + + + + ); + } + return ( + + + {/* Judul + Tombol Tambah */} + + + Daftar Data Kelahiran + + + - const filteredData = data || []; + {/* Desktop Table */} + + + + + Nama + Tanggal + Jenis Kelamin + Alamat + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.nama} + + + + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + + {item.jenisKelamin} + + + + + {item.alamat} + + + + + + + )) + ) : ( + + +
+ + Tidak ada data kelahiran yang cocok + +
+
+
+ )} +
+
+
+ {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + {item.nama} + + + Tanggal + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + Jenis Kelamin + {item.jenisKelamin} + + + Alamat + {item.alamat} + + + + + + + )) + ) : ( +
+ + Tidak ada data kelahiran yang cocok + +
+ )} +
+
- if (loading || !data) { - return ( - - - - ); - } - - - return ( - - - {/* Judul + Tombol Tambah */} - - Daftar Data Kelahiran - - - - - - - {/* Tabel */} - - - - - Nama - Tanggal - Jenis Kelamin - Alamat - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.nama} - - - - - - {new Date(item.tanggal).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric', - })} - - - - - {item.jenisKelamin} - - - - - - {item.alamat} - - - - - - - - )) - ) : ( - - -
- - Tidak ada data kelahiran yang cocok - -
-
-
- )} -
-
-
-
- - - {/* Pagination */} -
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); + {/* Pagination */} +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+
+ ); } - export default Kelahiran; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx index 9f7ada02..3e58c773 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/edit/page.tsx @@ -8,23 +8,43 @@ import { Box, Button, Group, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +import { convertToISODate } from '../../../lib/dateUtils'; function EditKematian() { const editState = useProxy(persentaseKelahiranKematian.kematian); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.nama?.trim() !== '' && + formData.tanggal !== '' && + formData.jenisKelamin?.trim() !== '' && + formData.alamat?.trim() !== '' && + !isHtmlEmpty(formData.penyebab) + ); + }; const [formData, setFormData] = useState({ nama: '', @@ -34,6 +54,14 @@ function EditKematian() { penyebab: '', }); + const [originalData, setOriginalData] = useState({ + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyebab: '', + }); + // Load data saat mount useEffect(() => { const loadData = async () => { @@ -43,12 +71,22 @@ function EditKematian() { try { const data = await editState.edit.load(id); if (data) { + const formattedTanggal = convertToISODate(data.tanggal); + setFormData({ nama: data.nama || '', - tanggal: data.tanggal || '', + tanggal: formattedTanggal, jenisKelamin: data.jenisKelamin || '', alamat: data.alamat || '', - penyebab: data.penyebab || '', + penyebab: data.penyebab || '' + }); + + setOriginalData({ + nama: data.nama || '', + tanggal: formattedTanggal, + jenisKelamin: data.jenisKelamin || '', + alamat: data.alamat || '', + penyebab: data.penyebab || '' }); } } catch (error) { @@ -64,8 +102,45 @@ function EditKematian() { setFormData(prev => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + nama: originalData.nama, + tanggal: originalData.tanggal, + jenisKelamin: originalData.jenisKelamin, + alamat: originalData.alamat, + penyebab: originalData.penyebab, + }); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { + if (!formData.nama?.trim()) { + toast.error('Nama wajib diisi'); + return; + } + + if (!formData.tanggal) { + toast.error('Tanggal wajib diisi'); + return; + } + + if (!formData.jenisKelamin?.trim()) { + toast.error('Jenis kelamin wajib diisi'); + return; + } + + if (!formData.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.penyebab)) { + toast.error('Penyebab wajib diisi'); + return; + } + try { + setIsSubmitting(true); // Update global state saat submit editState.edit.form = { ...editState.edit.form, ...formData }; await editState.edit.update(); @@ -76,18 +151,18 @@ function EditKematian() { } catch (error) { console.error('Error updating data kematian:', error); toast.error('Terjadi kesalahan saat memperbarui data kematian'); + } finally { + setIsSubmitting(false); } }; return ( - + {/* Header */} - - - + Edit Data Kematian @@ -147,17 +222,31 @@ function EditKematian() { + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx index 41b4fa96..d31b938b 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/[id]/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -48,7 +48,7 @@ function DetailKematian() { return ( - + {/* Tombol kembali */} - + - - - + @@ -155,7 +151,7 @@ function DetailKematian() { onConfirm={handleHapus} text="Apakah Anda yakin ingin menghapus data ini?" /> - + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx index 2f1c92b7..c742c297 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create/page.tsx @@ -3,143 +3,194 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Group, - Paper, - Stack, - TextInput, - Title, - Tooltip + Box, + Button, + Group, + Loader, + Paper, + Stack, + TextInput, + Title } from '@mantine/core'; import { IconArrowBack } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; - - - - - function CreateKematian() { - const createState = useProxy(persentaseKelahiranKematian.kematian); - const router = useRouter(); + const createState = useProxy(persentaseKelahiranKematian.kematian); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.form.nama?.trim() !== '' && + createState.create.form.tanggal !== '' && + createState.create.form.jenisKelamin?.trim() !== '' && + createState.create.form.alamat?.trim() !== '' && + !isHtmlEmpty(createState.create.form.penyebab) + ); + }; + + const resetForm = () => { + createState.create.form = { + nama: '', + tanggal: '', + jenisKelamin: '', + alamat: '', + penyebab: '', + }; + }; - const resetForm = () => { - createState.create.form = { - nama: '', - tanggal: '', - jenisKelamin: '', - alamat: '', - penyebab: '', - }; - }; + const handleSubmit = async () => { + if (!createState.create.form.nama?.trim()) { + toast.error('Nama wajib diisi'); + return; + } + + if (!createState.create.form.tanggal) { + toast.error('Tanggal wajib diisi'); + return; + } + + if (!createState.create.form.jenisKelamin?.trim()) { + toast.error('Jenis kelamin wajib diisi'); + return; + } + + if (!createState.create.form.alamat?.trim()) { + toast.error('Alamat wajib diisi'); + return; + } + + if (isHtmlEmpty(createState.create.form.penyebab)) { + toast.error('Penyebab wajib diisi'); + return; + } + + try { + setIsSubmitting(true); + await createState.create.create(); + resetForm(); + router.push( + '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' + ); + } catch (error) { + console.error('Error creating data kematian:', error); + toast.error('Gagal menambahkan data kematian'); + } finally { + setIsSubmitting(false); + } + }; - const handleSubmit = async () => { - if (!createState.create.form.nama) { - return toast.warn('Nama wajib diisi'); - } - if (!createState.create.form.tanggal) { - return toast.warn('Tanggal wajib diisi'); - } + return ( + + {/* Header */} + + + + Tambah Data Kematian + + - await createState.create.create(); - resetForm(); - router.push( - '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian' - ); - }; + {/* Form Card */} + + + (createState.create.form.nama = e.target.value)} + required + /> + (createState.create.form.tanggal = e.target.value)} + required + /> + (createState.create.form.jenisKelamin = e.target.value)} + required + /> + (createState.create.form.alamat = e.target.value)} + required + /> + + + Penyebab + + { + createState.create.form.penyebab = htmlContent; + }} + /> + - return ( - - {/* Header */} - - - - - - Tambah Data Kematian - - + + - - {/* Form Card */} - - - (createState.create.form.nama = e.target.value)} - required - /> - (createState.create.form.tanggal = e.target.value)} - required - /> - (createState.create.form.jenisKelamin = e.target.value)} - required - /> - (createState.create.form.alamat = e.target.value)} - required - /> - - - Penyebab - - { - createState.create.form.penyebab = htmlContent; - }} - /> - - - - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx index b9ab8ec7..da0cb664 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/page.tsx @@ -3,203 +3,239 @@ import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; import { - Box, - Button, - Center, - Group, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip, + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; - function Kematian() { - const [search, setSearch] = useState(""); - const router = useRouter(); + const [search, setSearch] = useState(""); + const router = useRouter(); + return ( + + {/* Tombol Back */} + + + - return ( - - {/* Tombol Back */} - - - + {/* Header dengan Search */} + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> - - {/* Header dengan Search */} - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - - - ); + + + ); } - function ListKematian({ search }: { search: string }) { - const statePersentase = useProxy(persentasekelahiran.kematian); - const router = useRouter(); + const statePersentase = useProxy(persentasekelahiran.kematian); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); + const { data, page, totalPages, loading, load } = statePersentase.findMany; - const { data, page, totalPages, loading, load } = statePersentase.findMany; + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + const filteredData = data || []; - useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + if (loading || !data) { + return ( + + + + ); + } + return ( + + + + Daftar Data Kematian + Daftar Data Kematian + + - const filteredData = data || []; + {/* Tabel untuk desktop */} + + + + + Nama + Tanggal + Jenis Kelamin + Alamat + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.nama} + + + + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + {item.jenisKelamin} + + + + {item.alamat} + + + + + + + )) + ) : ( + + +
+ + Tidak ada data kematian yang cocok + +
+
+
+ )} +
+
+
+ {/* Card untuk mobile */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Nama + {item.nama} + + + Tanggal + + {new Date(item.tanggal).toLocaleDateString('id-ID', { + day: '2-digit', + month: 'long', + year: 'numeric', + })} + + + + Jenis Kelamin + {item.jenisKelamin} + + + Alamat + {item.alamat} + + + + + + + )) + ) : ( +
+ + Tidak ada data kematian yang cocok + +
+ )} +
+
- if (loading || !data) { - return ( - - - - ); - } - - - return ( - - - - Daftar Data Kematian - - - - - - - - - - - Nama - Tanggal - Jenis Kelamin - Alamat - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.nama} - - - - - - {new Date(item.tanggal).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'long', - year: 'numeric', - })} - - - - - {item.jenisKelamin} - - - - - - {item.alamat} - - - - - - - - )) - ) : ( - - -
- - Tidak ada data kematian yang cocok - -
-
-
- )} -
-
-
-
- - - {/* Pagination */} -
- { - load(newPage, 10, search); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); + {/* Pagination */} +
+ { + load(newPage, 10, search); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + color="blue" + radius="md" + /> +
+
+ ); } - export default Kematian; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx new file mode 100644 index 00000000..3ce629bc --- /dev/null +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/lib/dateUtils.tsx @@ -0,0 +1,24 @@ +export const convertToISODate = (dateString: string): string => { + if (!dateString) return ''; + + // Jika format dd/mm/yyyy + const parts = dateString.split('/'); + if (parts.length === 3 && parts[0].length === 2 && parts[1].length === 2 && parts[2].length === 4) { + const [day, month, year] = parts; + return `${year}-${month}-${day}`; + } + + // Jika sudah format YYYY-MM-DD, biarkan + if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + return dateString; + } + + // Jika format lain, coba parse dengan Date + const date = new Date(dateString); + if (!isNaN(date.getTime())) { + return date.toISOString().split('T')[0]; // YYYY-MM-DD + } + + console.warn(`Format tanggal tidak dikenali: ${dateString}`); + return ''; +}; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx index 97f198bd..a7e123dc 100644 --- a/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/page.tsx @@ -3,275 +3,334 @@ 'use client' import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import colors from '@/con/colors'; -import { ActionIcon, Badge, Box, Center, Flex, Tooltip as MantineTooltip, Paper, Select, Skeleton, Stack, Table, Text, Title } from '@mantine/core'; +import { ActionIcon, Badge, Box, Center, Flex, Group, Paper, Select, Skeleton, Stack, Table, Text, Title, Tooltip as MantineTooltip } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconBabyCarriage, IconGrave2 } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from 'recharts'; +import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip as RechartsTooltip, TooltipProps, XAxis, YAxis } from 'recharts'; import { useProxy } from 'valtio/utils'; - type TooltipPayload = { - name: string; - value: number; - payload: any; - color: string; - dataKey: string; + name: string; + value: number; + payload: any; + color: string; + dataKey: string; }; - type CustomTooltipProps = TooltipProps & { - active?: boolean; - payload?: TooltipPayload[]; - label?: string; + active?: boolean; + payload?: TooltipPayload[]; + label?: string; }; - function PersentaseDataKelahiranKematian() { - return ( - - - - ); + return ( + + + + ); } - function GrafikPersentaseKelahiranKematian() { - const router = useRouter(); + const router = useRouter(); + + type DataTahunan = { + tahun: string; + totalKelahiran: number; + totalKematian: number; + data: Array<{ + id: string; + bulan: string; + kelahiran: number; + kematian: number; + }>; + }; + + // ✅ Fungsi hitung tahunan + bulanan + const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => { + const dataTahunan: Record = {}; + + const namaBulan = [ + 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', + 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' + ]; + + kelahiran?.forEach((item: any) => { + const date = new Date(item.tanggal); + const tahun = date.getFullYear().toString(); + const bulanIndex = date.getMonth(); + + if (!dataTahunan[tahun]) { + dataTahunan[tahun] = { + tahun, + totalKelahiran: 0, + totalKematian: 0, + data: namaBulan.map((nama, idx) => ({ + id: `${tahun}-${idx + 1}`, + bulan: nama, + kelahiran: 0, + kematian: 0 + })) + }; + } + + dataTahunan[tahun].totalKelahiran += 1; + dataTahunan[tahun].data[bulanIndex].kelahiran += 1; + }); + + kematian?.forEach((item: any) => { + const date = new Date(item.tanggal); + const tahun = date.getFullYear().toString(); + const bulanIndex = date.getMonth(); + + if (!dataTahunan[tahun]) { + dataTahunan[tahun] = { + tahun, + totalKelahiran: 0, + totalKematian: 0, + data: namaBulan.map((nama, idx) => ({ + id: `${tahun}-${idx + 1}`, + bulan: nama, + kelahiran: 0, + kematian: 0 + })) + }; + } + + dataTahunan[tahun].totalKematian += 1; + dataTahunan[tahun].data[bulanIndex].kematian += 1; + }); + + return Object.values(dataTahunan).sort((a, b) => parseInt(a.tahun) - parseInt(b.tahun)); + }; + + const statePersentase = useProxy(persentasekelahiran); + const [chartData, setChartData] = useState([]); + const [selectedYear, setSelectedYear] = useState(null); + + const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num); + + const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { + if (active && payload && payload.length) { + return ( + + Tahun {label} + Kelahiran: {formatNumber(payload[0].value)} + Kematian: {formatNumber(payload[1].value)} + + ); + } + return null; + }; + + useShallowEffect(() => { + statePersentase.kelahiran.findMany.load(1, 1000); + statePersentase.kematian.findMany.load(1, 1000); + }, []); + + useEffect(() => { + if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) { + const hasil = countByYearAndMonth( + statePersentase.kelahiran.findMany.data, + statePersentase.kematian.findMany.data + ); + + setChartData(hasil); + setSelectedYear(hasil[0]?.tahun || null); + } + }, [statePersentase.kelahiran.findMany.data, statePersentase.kematian.findMany.data]); + + if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) { + return ; + } + + const selectedYearData = chartData.find(d => d.tahun === selectedYear); + + return ( + + + + + Statistik Kelahiran & Kematian + + + + router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}> + + + + + router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')}> + + + + + + + {chartData.length === 0 ? ( +
+ + Belum ada data untuk ditampilkan + +
+ ) : ( + <> + + ({ value: item.tahun, label: item.tahun }))} - value={selectedYear} - onChange={(value) => setSelectedYear(value || null)} - size="sm" - radius="md" - /> - - - - - - - - - } /> - - - - - - - - - {selectedYearData && ( - - - Rincian Tahun {selectedYear} - {formatNumber(selectedYearData.totalKelahiran)} kelahiran - {formatNumber(selectedYearData.totalKematian)} kematian - - - - - Bulan - Kelahiran - Kematian - - - - {selectedYearData.data.length > 0 ? ( - <> - {selectedYearData.data.map((item) => ( - - {item.bulan} - {formatNumber(item.kelahiran)} - {formatNumber(item.kematian)} - - ))} - - Total - {formatNumber(selectedYearData.totalKelahiran)} - {formatNumber(selectedYearData.totalKematian)} - - - ) : ( - - Tidak ada rincian bulanan - - )} - -
-
- )} - - )} -
-
- ); + {/* Total row mobile */} + {selectedYearData.data.length > 0 && ( + + Total + + Kelahiran + + {formatNumber(selectedYearData.totalKelahiran)} + + + + Kematian + + {formatNumber(selectedYearData.totalKematian)} + + + + )} + +
+ + )} + + )} + + + ); } - export default PersentaseDataKelahiranKematian; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx index 6d751dd5..34c07fa7 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/edit/page.tsx @@ -5,21 +5,22 @@ import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wab import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useEffect, useState, ChangeEvent } from 'react'; +import { ChangeEvent, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; @@ -27,16 +28,41 @@ function EditInfoWabahPenyakit() { const infoWabahPenyakitState = useProxy(infoWabahPenyakit); const router = useRouter(); const params = useParams(); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsiSingkat: '', - deskripsi: '', + deskripsiLengkap: '', imageId: '', }); + + const [originalData, setOriginalData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsiLengkap: '', + imageId: '', + imageUrl: '' + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsiSingkat) && + !isHtmlEmpty(formData.deskripsiLengkap) + ); + }; + // Helper untuk update field formData const updateField = (field: keyof typeof formData, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); @@ -54,9 +80,16 @@ function EditInfoWabahPenyakit() { setFormData({ name: data.name || '', deskripsiSingkat: data.deskripsiSingkat || '', - deskripsi: data.deskripsiLengkap || '', + deskripsiLengkap: data.deskripsiLengkap || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsiLengkap: data.deskripsiLengkap || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data.image?.link) setPreviewImage(data.image.link); } @@ -71,6 +104,7 @@ function EditInfoWabahPenyakit() { const handleSubmit = async () => { try { + setIsSubmitting(true); let uploadedImageId = formData.imageId; // Upload file kalau ada @@ -87,7 +121,7 @@ function EditInfoWabahPenyakit() { ...infoWabahPenyakitState.edit.form, name: formData.name, deskripsiSingkat: formData.deskripsiSingkat, - deskripsiLengkap: formData.deskripsi, + deskripsiLengkap: formData.deskripsiLengkap, imageId: uploadedImageId, }; @@ -97,9 +131,23 @@ function EditInfoWabahPenyakit() { } catch (error) { console.error(error); toast.error('Terjadi kesalahan saat memperbarui info wabah penyakit'); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsiLengkap: originalData.deskripsiLengkap, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleDrop = (files: File[]) => { const selectedFile = files[0]; if (selectedFile) { @@ -109,14 +157,12 @@ function EditInfoWabahPenyakit() { }; return ( - + {/* Header */} - - - + Edit Info Wabah Penyakit @@ -152,11 +198,11 @@ function EditInfoWabahPenyakit() { - Deskripsi + Deskripsi Lengkap updateField('deskripsi', val)} + value={formData.deskripsiLengkap} + onChange={(val) => updateField('deskripsiLengkap', val)} /> @@ -168,7 +214,7 @@ function EditInfoWabahPenyakit() { onDrop={handleDrop} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -193,7 +239,7 @@ function EditInfoWabahPenyakit() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx index 03fda087..8f2f268f 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/[id]/page.tsx @@ -3,21 +3,20 @@ import colors from '@/con/colors'; import { Box, Button, - Paper, - Stack, - Text, - Skeleton, - Tooltip, Group, Image, + Paper, + Skeleton, + Stack, + Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; function DetailInfoWabahPenyakit() { const state = useProxy(infoWabahPenyakit); @@ -50,7 +49,7 @@ function DetailInfoWabahPenyakit() { const data = state.findUnique.data; return ( - + {/* Tombol Back */} - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx index 06a0d39c..801dd945 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,30 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; -import { Dropzone } from '@mantine/dropzone'; function CreateInfoWabahPenyakit() { const router = useRouter(); const infoWabahPenyakitState = useProxy(infoWabahPenyakit) const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + infoWabahPenyakitState.create.form.name?.trim() !== '' && + !isHtmlEmpty(infoWabahPenyakitState.create.form.deskripsiSingkat) && + !isHtmlEmpty(infoWabahPenyakitState.create.form.deskripsiLengkap) && + file !== null + ); + }; const resetForm = () => { infoWabahPenyakitState.create.form = { @@ -40,41 +59,47 @@ function CreateInfoWabahPenyakit() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + infoWabahPenyakitState.create.form.imageId = uploaded.id; + await infoWabahPenyakitState.create.create(); + + resetForm(); + router.push("/admin/kesehatan/info-wabah-penyakit") + } catch (error) { + console.error("Error creating info wabah penyakit:", error); + toast.error("Gagal menambahkan info wabah penyakit"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - infoWabahPenyakitState.create.form.imageId = uploaded.id; - await infoWabahPenyakitState.create.create(); - - resetForm(); - router.push("/admin/kesehatan/info-wabah-penyakit") }; return ( - + {/* Header */} - - - + Tambah Info Wabah Penyakit @@ -91,7 +116,7 @@ function CreateInfoWabahPenyakit() { > { infoWabahPenyakitState.create.form.name = val.target.value; }} @@ -132,7 +157,7 @@ function CreateInfoWabahPenyakit() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -157,7 +182,7 @@ function CreateInfoWabahPenyakit() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx index e4215822..55fa211f 100644 --- a/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/info-wabah-penyakit/page.tsx @@ -17,9 +17,8 @@ import { TableTr, Text, Title, - Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -28,7 +27,7 @@ import HeaderSearch from '../../_com/header'; import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; function InfoWabahPenyakit() { - const [search, setSearch] = useState(""); + const [search, setSearch] = useState(''); return ( {/* Header Search */} @@ -45,8 +44,9 @@ function InfoWabahPenyakit() { } function ListInfoWabahPenyakit({ search }: { search: string }) { - const infoWabahPenyakitState = useProxy(infoWabahPenyakit) - const router = useRouter() + const infoWabahPenyakitState = useProxy(infoWabahPenyakit); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, @@ -57,40 +57,45 @@ function ListInfoWabahPenyakit({ search }: { search: string }) { } = infoWabahPenyakitState.findMany; useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } return ( - - + + {/* Judul + Tombol Tambah */} - - Daftar Info Wabah Penyakit - - - + + + Daftar Info Wabah Penyakit + + - {/* Tabel */} - - + {/* Desktop Table */} + +
Judul @@ -103,16 +108,19 @@ function ListInfoWabahPenyakit({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - - + )) ) : ( - -
- + +
+ Tidak ada data info wabah penyakit yang cocok
@@ -140,6 +150,53 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
+ + {/* Mobile Card List */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Judul + + + {item.name} + + + + Deskripsi Singkat + + + + + + + )) + ) : ( +
+ + Tidak ada data info wabah penyakit yang cocok + +
+ )} +
{/* Pagination */} @@ -147,8 +204,8 @@ function ListInfoWabahPenyakit({ search }: { search: string }) { { - load(newPage, 10) - window.scrollTo({ top: 0, behavior: 'smooth' }) + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} mt="md" @@ -158,7 +215,7 @@ function ListInfoWabahPenyakit({ search }: { search: string }) { />
- ) + ); } -export default InfoWabahPenyakit; +export default InfoWabahPenyakit; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx index 8ede9489..39d3035e 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/edit/page.tsx @@ -1,19 +1,21 @@ +/* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -29,14 +31,38 @@ function EditKontakDarurat() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsi: '', imageId: '', whatsapp: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + whatsapp: '', + imageUrl: '', + }); const [loading, setLoading] = useState(true); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.whatsapp?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data sekali saat mount useEffect(() => { const loadKontakDarurat = async () => { @@ -52,6 +78,13 @@ function EditKontakDarurat() { imageId: data.imageId || '', whatsapp: data.whatsapp || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + whatsapp: data.whatsapp || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -63,10 +96,11 @@ function EditKontakDarurat() { }; loadKontakDarurat(); - }, [params?.id, kontakDaruratState.edit]); + }, [params?.id]); const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; // Upload file baru jika ada @@ -90,19 +124,31 @@ function EditKontakDarurat() { } catch (error) { console.error("Error updating kontak darurat:", error); toast.error("Terjadi kesalahan saat memperbarui kontak darurat"); + } finally { + setIsSubmitting(false); } }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + whatsapp: originalData.whatsapp, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + if (loading) return Loading...; return ( - + - - - + Edit Kontak Darurat @@ -129,8 +175,8 @@ function EditKontakDarurat() { setFormData(prev => ({ ...prev, whatsapp: e.target.value }))} - label="Whatsapp" - placeholder="Masukkan whatsapp" + label="Telepon" + placeholder="Masukkan telepon" required /> @@ -154,7 +200,7 @@ function EditKontakDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -175,7 +221,7 @@ function EditKontakDarurat() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx index 628338cb..ce9c9a32 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; @@ -40,7 +40,7 @@ function DetailKontakDarurat() { const data = state.findUnique.data; return ( - + {/* Tombol Back */} - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx index 65a84277..c1af6df8 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, @@ -25,13 +27,30 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat'; -import { Dropzone } from '@mantine/dropzone'; function CreateKontakDarurat() { const router = useRouter(); const kontakDaruratState = useProxy(kontakDarurat); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + kontakDaruratState.create.form.name?.trim() !== '' && + kontakDaruratState.create.form.whatsapp?.trim() !== '' && + !isHtmlEmpty(kontakDaruratState.create.form.deskripsi) && + file !== null + ); + }; const resetForm = () => { kontakDaruratState.create.form = { @@ -45,42 +64,48 @@ function CreateKontakDarurat() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + kontakDaruratState.create.form.imageId = uploaded.id; + + await kontakDaruratState.create.create(); + + resetForm(); + router.push('/admin/kesehatan/kontak-darurat'); + } catch (error) { + console.error('Error creating kontak darurat:', error); + toast.error('Gagal menambahkan kontak darurat'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - kontakDaruratState.create.form.imageId = uploaded.id; - - await kontakDaruratState.create.create(); - - resetForm(); - router.push('/admin/kesehatan/kontak-darurat'); }; return ( - + {/* Header */} - - - + Tambah Kontak Darurat @@ -97,7 +122,7 @@ function CreateKontakDarurat() { > { kontakDaruratState.create.form.name = val.target.value; }} @@ -107,13 +132,13 @@ function CreateKontakDarurat() { /> { kontakDaruratState.create.form.whatsapp = val.target.value; }} - label={Whatsapp} - placeholder="Masukkan whatsapp" + label={Telepon} + placeholder="Masukkan telepon" required /> @@ -139,7 +164,7 @@ function CreateKontakDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx index 2c341cf0..9c3fc7c4 100644 --- a/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/kontak-darurat/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useShallowEffect, useDebouncedValue } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -47,48 +46,47 @@ function KontakDarurat() { } function ListKontakDarurat({ search }: { search: string }) { - const kontakDaruratState = useProxy(kontakDarurat) + const kontakDaruratState = useProxy(kontakDarurat); const router = useRouter(); const { data, page, totalPages, loading, load } = kontakDaruratState.findMany; + const [debouncedSearch] = useDebouncedValue(search, 1000); useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); - const filteredData = data || [] + const filteredData = data || []; if (loading || !data) { return ( - + - ) + ); } return ( - - + + {/* Judul + Tombol Tambah */} - - - Daftar Kontak Darurat - - - - - + + Daftar Kontak Darurat + + - {/* Tabel */} - - + {/* Desktop Table */} + +
Judul @@ -100,35 +98,31 @@ function ListKontakDarurat({ search }: { search: string }) { {filteredData.length > 0 ? ( filteredData.map((item) => ( - - - - {item.name} - - + + + {item.name} + - - - - + + - + )) ) : ( - +
- Tidak ada data kontak darurat yang cocok + Tidak ada data kontak darurat yang cocok
@@ -136,6 +130,40 @@ function ListKontakDarurat({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Judul + {item.name} + + + Deskripsi + + + + + + )) + ) : ( +
+ Tidak ada data kontak darurat yang cocok +
+ )} +
{/* Pagination */} @@ -154,7 +182,7 @@ function ListKontakDarurat({ search }: { search: string }) { />
- ) + ); } -export default KontakDarurat; +export default KontakDarurat; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx index 3d1d0da1..a637af41 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/edit/page.tsx @@ -6,16 +6,17 @@ import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penangan import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -28,6 +29,7 @@ function EditPenangananDarurat() { const penangananDaruratState = useProxy(penangananDarurat) const router = useRouter(); const params = useParams() + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', @@ -35,10 +37,32 @@ function EditPenangananDarurat() { imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsi: '', + imageId: '', + imageUrl: '', + }); + const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); const [loading, setLoading] = useState(true); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) + ); + }; + // Load data satu kali saat component mount useEffect(() => { const loadData = async () => { @@ -54,6 +78,13 @@ function EditPenangananDarurat() { imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); + if (data.image?.link) { setPreviewImage(data.image.link); } @@ -81,8 +112,20 @@ function EditPenangananDarurat() { setPreviewImage(URL.createObjectURL(selected)); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; if (file) { @@ -108,20 +151,20 @@ function EditPenangananDarurat() { } catch (err) { console.error("Error updating penanganan darurat:", err); toast.error("Gagal memperbarui data penanganan darurat"); + } finally { + setIsSubmitting(false); } }; if (loading) return Loading...; return ( - + {/* Header */} - - - + Edit Penanganan Darurat @@ -159,7 +202,7 @@ function EditPenangananDarurat() { onDrop={handleDrop} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -184,7 +227,7 @@ function EditPenangananDarurat() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx index 36967755..db808007 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/[id]/page.tsx @@ -1,11 +1,11 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core'; -import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; -import { useRouter, useParams } from 'next/navigation'; -import React, { useState } from 'react'; -import { useProxy } from 'valtio/utils'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat'; @@ -40,7 +40,7 @@ function DetailPenangananDarurat() { const data = state.findUnique.data; return ( - + {/* Tombol Back */} - + - - - +
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx index f28e0a92..174a396c 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { @@ -31,8 +32,25 @@ function CreatePenangananDarurat() { const router = useRouter(); const penangananDaruratState = useProxy(penangananDarurat); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [file, setFile] = useState(null); + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + penangananDaruratState.create.form.name?.trim() !== '' && + !isHtmlEmpty(penangananDaruratState.create.form.deskripsi) && + file !== null + ); + }; + const resetForm = () => { penangananDaruratState.create.form = { name: '', @@ -44,42 +62,48 @@ function CreatePenangananDarurat() { }; const handleSubmit = async () => { - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + penangananDaruratState.create.form.imageId = uploaded.id; + + await penangananDaruratState.create.create(); + + resetForm(); + router.push('/admin/kesehatan/penanganan-darurat'); + } catch (error) { + console.error(error); + toast.error('Gagal menambahkan penanganan darurat'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - penangananDaruratState.create.form.imageId = uploaded.id; - - await penangananDaruratState.create.create(); - - resetForm(); - router.push('/admin/kesehatan/penanganan-darurat'); }; return ( - + {/* Header */} - - - + Tambah Penanganan Darurat @@ -99,7 +123,7 @@ function CreatePenangananDarurat() { Judul} placeholder="Masukkan judul" - defaultValue={penangananDaruratState.create.form.name} + value={penangananDaruratState.create.form.name} onChange={(val) => { penangananDaruratState.create.form.name = val.target.value; }} @@ -131,7 +155,7 @@ function CreatePenangananDarurat() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -193,17 +235,31 @@ function CreatePenangananDarurat() { {/* Button Simpan */} + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx index 7d397e4d..a465616e 100644 --- a/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/penanganan-darurat/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -48,49 +47,55 @@ function PenangananDarurat() { function ListPenangananDarurat({ search }: { search: string }) { const state = useProxy(penangananDarurat); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000) const { data, page, totalPages, loading, load } = state.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - + + {/* Judul + Tombol Tambah */} - - Daftar Penanganan Darurat - - - + + + Daftar Penanganan Darurat + + - {/* Tabel */} - - + {/* Desktop Table */} + +
- Judul - Deskripsi - Aksi + Judul + Deskripsi + Aksi @@ -98,22 +103,19 @@ function ListPenangananDarurat({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - - )) ) : ( - +
- Tidak ada data penanganan darurat + + Tidak ada data penanganan darurat +
@@ -141,6 +145,56 @@ function ListPenangananDarurat({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Judul + + {item.name} + + + + Deskripsi + + + + + + + + )) + ) : ( +
+ + Tidak ada data penanganan darurat + +
+ )} +
+
{/* Pagination */} @@ -162,4 +216,4 @@ function ListPenangananDarurat({ search }: { search: string }) { ); } -export default PenangananDarurat; +export default PenangananDarurat; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx index 1a47aefa..69b7e102 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/edit/page.tsx @@ -6,16 +6,17 @@ import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/pos import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -31,6 +32,26 @@ function EditPosyandu() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.nomor?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + !isHtmlEmpty(formData.jadwalPelayanan) && + (file !== null || originalData.imageId !== '') // Either a new file is selected or an existing image exists + ); + }; + const [formData, setFormData] = useState({ name: '', nomor: '', @@ -38,6 +59,14 @@ function EditPosyandu() { imageId: '', jadwalPelayanan: '', }); + const [originalData, setOriginalData] = useState({ + name: "", + nomor: "", + deskripsi: "", + imageId: "", + jadwalPelayanan: "", + imageUrl: "" + }); // Load data posyandu useEffect(() => { @@ -55,6 +84,14 @@ function EditPosyandu() { imageId: data.imageId || '', jadwalPelayanan: data.jadwalPelayanan || '', }); + setOriginalData({ + name: data.name || '', + nomor: data.nomor || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + jadwalPelayanan: data.jadwalPelayanan || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } } catch (error) { @@ -66,8 +103,33 @@ function EditPosyandu() { }, [params?.id]); const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Nama posyandu wajib diisi'); + return; + } + + if (!formData.nomor?.trim()) { + toast.error('Nomor telepon wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.jadwalPelayanan)) { + toast.error('Jadwal pelayanan wajib diisi'); + return; + } + + if (!file && !originalData.imageId) { + toast.error('Gambar wajib dipilih'); + return; + } + try { - // Update global state hanya saat submit + setIsSubmitting(true); const updatedForm = { ...statePosyandu.edit.form, ...formData }; // Upload file jika ada @@ -87,18 +149,31 @@ function EditPosyandu() { } catch (error) { console.error('Error updating posyandu:', error); toast.error('Terjadi kesalahan saat memperbarui posyandu'); + } finally { + setIsSubmitting(false); } }; + const resetForm = () => { + setFormData({ + name: originalData.name, + nomor: originalData.nomor, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + jadwalPelayanan: originalData.jadwalPelayanan, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + return ( - + {/* Tombol Back */} - - - + Edit Posyandu @@ -129,7 +204,7 @@ function EditPosyandu() { }} onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > @@ -148,25 +223,45 @@ function EditPosyandu() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
{previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} @@ -212,17 +307,31 @@ function EditPosyandu() { {/* Tombol Submit */} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx index 75af2397..2aa9fe3e 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/[id]/page.tsx @@ -1,177 +1,175 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core'; -import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react'; -import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import { useProxy } from 'valtio/utils'; -import posyanduState from '../../../_state/kesehatan/posyandu/posyandu'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import posyanduState from '../../../_state/kesehatan/posyandu/posyandu'; function DetailPosyandu() { - const statePosyandu = useProxy(posyanduState); - const params = useParams(); - const router = useRouter(); - const [modalHapus, setModalHapus] = useState(false); - const [selectedId, setSelectedId] = useState(null); + const statePosyandu = useProxy(posyanduState); + const params = useParams(); + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); - useShallowEffect(() => { - statePosyandu.findUnique.load(params?.id as string); - }, []); + useShallowEffect(() => { + statePosyandu.findUnique.load(params?.id as string); + }, []); - const handleHapus = () => { - if (selectedId) { - statePosyandu.delete.byId(selectedId); - setModalHapus(false); - setSelectedId(null); - router.push("/admin/kesehatan/posyandu"); - } - }; + const handleHapus = () => { + if (selectedId) { + statePosyandu.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push("/admin/kesehatan/posyandu"); + } + }; - if (!statePosyandu.findUnique.data) { - return ( - - - - ); - } + if (!statePosyandu.findUnique.data) { + return ( + + + + ); + } - const data = statePosyandu.findUnique.data; + const data = statePosyandu.findUnique.data; - return ( - - {/* Tombol kembali */} - + return ( + + {/* Tombol kembali */} + - {/* Card utama */} - - - - Detail Posyandu - + {/* Card utama */} + + + + Detail Posyandu + - - - - Nama Posyandu - {data.name || '-'} - + + + + Nama Posyandu + {data.name || '-'} + - - Nomor Posyandu - {data.nomor || '-'} - + + Nomor Posyandu + {data.nomor || '-'} + - - Deskripsi - - + + Deskripsi + + + + - - Jadwal Pelayanan - - + + Jadwal Pelayanan + + - - Gambar - {data.image?.link ? ( - {data.name - ) : ( - Tidak ada gambar - )} - + + Gambar + {data.image?.link ? ( + {data.name + ) : ( + Tidak ada gambar + )} + - {/* Aksi */} - - - - + {/* Aksi */} + + - - - - - - - - + + + + + + - {/* Modal konfirmasi hapus */} - setModalHapus(false)} - onConfirm={handleHapus} - text="Apakah Anda yakin ingin menghapus posyandu ini?" - /> - - ); + {/* Modal konfirmasi hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text="Apakah Anda yakin ingin menghapus posyandu ini?" + /> + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx index 9542eefe..d58cabc9 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { - Box, - Button, - Group, - Image, - Paper, - Stack, - Text, - TextInput, - Title, - Tooltip, + ActionIcon, + Box, + Button, + Group, + Image, + Loader, + Paper, + Stack, + Text, + TextInput, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -24,191 +25,260 @@ import posyandustate from '../../../_state/kesehatan/posyandu/posyandu'; function CreatePosyandu() { - const statePosyandu = useProxy(posyandustate); - const router = useRouter(); - const [file, setFile] = useState(null); - const [previewImage, setPreviewImage] = useState(null); + const statePosyandu = useProxy(posyandustate); + const router = useRouter(); + const [file, setFile] = useState(null); + const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + statePosyandu.create.form.name?.trim() !== '' && + statePosyandu.create.form.nomor?.trim() !== '' && + !isHtmlEmpty(statePosyandu.create.form.deskripsi) && + !isHtmlEmpty(statePosyandu.create.form.jadwalPelayanan) && + file !== null + ); + }; + + const resetForm = () => { + statePosyandu.create.form = { + name: '', + nomor: '', + deskripsi: '', + imageId: '', + jadwalPelayanan: '', + }; + setFile(null); + setPreviewImage(null); + }; - const resetForm = () => { - statePosyandu.create.form = { - name: '', - nomor: '', - deskripsi: '', - imageId: '', - jadwalPelayanan: '', - }; - setFile(null); - setPreviewImage(null); - }; + const handleSubmit = async () => { + if (!statePosyandu.create.form.name?.trim()) { + toast.error('Nama posyandu wajib diisi'); + return; + } + + if (!statePosyandu.create.form.nomor?.trim()) { + toast.error('Nomor telepon wajib diisi'); + return; + } + + if (isHtmlEmpty(statePosyandu.create.form.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (isHtmlEmpty(statePosyandu.create.form.jadwalPelayanan)) { + toast.error('Jadwal pelayanan wajib diisi'); + return; + } + + if (!file) { + toast.error('Gambar wajib dipilih'); + return; + } + + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Silakan pilih file gambar terlebih dahulu'); + } + // Upload gambar dulu + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + statePosyandu.create.form.imageId = uploaded.id; + await statePosyandu.create.create(); + resetForm(); + router.push('/admin/kesehatan/posyandu'); + } catch (error) { + console.error('Error creating posyandu:', error); + toast.error('Gagal menambahkan posyandu'); + } finally { + setIsSubmitting(false); + } + }; + return ( + + {/* Header */} + + + + Tambah Posyandu + + - const handleSubmit = async () => { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); - } + + + {/* Upload Gambar */} + + + Gambar Posyandu + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file (maks 5MB) + + - // Upload gambar dulu - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); + {previewImage && ( + + Preview Gambar + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } + {/* Input Form */} + (statePosyandu.create.form.name = e.target.value)} + required + /> + (statePosyandu.create.form.nomor = e.target.value)} + required + /> + + + Deskripsi Posyandu + + { + statePosyandu.create.form.deskripsi = htmlContent; + }} + /> + + + + Jadwal Pelayanan + + { + statePosyandu.create.form.jadwalPelayanan = htmlContent; + }} + /> + - statePosyandu.create.form.imageId = uploaded.id; + {/* Button */} + + - - await statePosyandu.create.create(); - - - resetForm(); - router.push('/admin/kesehatan/posyandu'); - }; - - - return ( - - {/* Header */} - - - - - - Tambah Posyandu - - - - - - - {/* Upload Gambar */} - - - Gambar Posyandu - - { - const selectedFile = files[0]; - if (selectedFile) { - setFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret gambar atau klik untuk memilih file (maks 5MB) - - - - - {previewImage && ( - - Preview Gambar - - )} - - - - {/* Input Form */} - (statePosyandu.create.form.name = e.target.value)} - required - /> - (statePosyandu.create.form.nomor = e.target.value)} - required - /> - - - Deskripsi Posyandu - - { - statePosyandu.create.form.deskripsi = htmlContent; - }} - /> - - - - Jadwal Pelayanan - - { - statePosyandu.create.form.jadwalPelayanan = htmlContent; - }} - /> - - - - {/* Button */} - - - - - - - ); + {/* Tombol Simpan */} + + + + + + ); } diff --git a/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx b/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx index b8528787..e64902ed 100644 --- a/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/posyandu/page.tsx @@ -1,177 +1,232 @@ 'use client' import colors from '@/con/colors'; import { - Box, - Button, - Center, - Group, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableTbody, - TableTd, - TableTh, - TableThead, - TableTr, - Text, - Title, - Tooltip + Box, + Button, + Center, + Group, + Pagination, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; -import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import HeaderSearch from '../../_com/header'; import posyandustate from '../../_state/kesehatan/posyandu/posyandu'; - function Posyandu() { - const [search, setSearch] = useState(""); - return ( - - } - value={search} - onChange={(e) => setSearch(e.currentTarget.value)} - /> - - - ); + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); } - function ListPosyandu({ search }: { search: string }) { - const statePosyandu = useProxy(posyandustate) - const router = useRouter(); + const statePosyandu = useProxy(posyandustate); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay + const { + data, + page, + totalPages, + loading, + load, + } = statePosyandu.findMany; - const { - data, - page, - totalPages, - loading, - load, - } = statePosyandu.findMany; + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + const filteredData = data || []; - useShallowEffect(() => { - load(page, 10, search) - }, [page, search]) + if (loading || !data) { + return ( + + + + ); + } + return ( + + + + + Daftar Posyandu + + + - const filteredData = data || []; + {/* Desktop Table */} + + + + + Nama Posyandu + Nomor Posyandu + Deskripsi + Aksi + + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + {item.name} + + + + + {item.nomor || '-'} + + + + + + + + + + )) + ) : ( + + +
+ + Tidak ada data posyandu yang cocok + +
+
+
+ )} +
+
+
+ {/* Mobile Cards */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Posyandu + + + {item.name} + + + + + Nomor Posyandu + + + {item.nomor || '-'} + + + + + Deskripsi + + + + + + + )) + ) : ( +
+ + Tidak ada data posyandu yang cocok + +
+ )} +
+
+
- if (loading || !data) { - return ( - - - - ) - } - - - return ( - - - - Daftar Posyandu - - - - - - - - - Nama Posyandu - Nomor Posyandu - Deskripsi - Aksi - - - - {filteredData.length > 0 ? ( - filteredData.map((item) => ( - - - - - {item.name} - - - - - - - {item.nomor || '-'} - - - - - - - - - - - - - )) - ) : ( - - -
- Tidak ada data posyandu yang cocok -
-
-
- )} -
-
-
-
-
- { - load(newPage, 10); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }} - total={totalPages} - mt="md" - mb="md" - color="blue" - radius="md" - /> -
-
- ); +
+ { + load(newPage, 10); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + total={totalPages} + mt="md" + mb="md" + color="blue" + radius="md" + /> +
+
+ ); } - export default Posyandu; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx index 1f0dcbec..d3f48814 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/edit/page.tsx @@ -5,16 +5,17 @@ import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-k import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -30,12 +31,36 @@ function EditProgramKesehatan() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', deskripsiSingkat: '', deskripsi: '', imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + deskripsiSingkat: '', + deskripsi: '', + imageId: '', + imageUrl: '' + }); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + !isHtmlEmpty(formData.deskripsiSingkat) && + !isHtmlEmpty(formData.deskripsi) + ); + }; // Load data awal useEffect(() => { @@ -53,6 +78,13 @@ function EditProgramKesehatan() { deskripsi: data.deskripsi || '', imageId: data.imageId || '', }); + setOriginalData({ + name: data.name || '', + deskripsiSingkat: data.deskripsiSingkat || '', + deskripsi: data.deskripsi || '', + imageId: data.imageId || '', + imageUrl: data.image?.link || '', + }); if (data?.image?.link) setPreviewImage(data.image.link); } catch (err) { @@ -69,9 +101,22 @@ function EditProgramKesehatan() { setFormData((prev) => ({ ...prev, [key]: value })); }; + const handleResetForm = () => { + setFormData({ + name: originalData.name, + deskripsiSingkat: originalData.deskripsiSingkat, + deskripsi: originalData.deskripsi, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + // Submit form const handleSubmit = async () => { try { + setIsSubmitting(true); const updatedForm = { ...programKesehatanState.edit.form, ...formData }; // Upload file kalau ada @@ -90,17 +135,17 @@ function EditProgramKesehatan() { } catch (err) { console.error(err); toast.error('Terjadi kesalahan saat memperbarui program kesehatan'); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Program Kesehatan @@ -161,7 +206,7 @@ function EditProgramKesehatan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -186,7 +231,7 @@ function EditProgramKesehatan() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx index 5a36bca6..e493ea78 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; function DetailProgramKesehatan() { const state = useProxy(programKesehatan); @@ -40,7 +40,7 @@ function DetailProgramKesehatan() { const data = state.findUnique.data; return ( - + {/* Tombol kembali */} - + - - - + diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx index 35d642df..ba3f9ee4 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/create/page.tsx @@ -2,17 +2,19 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip + Title } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,13 +22,30 @@ import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import CreateEditor from '../../../_com/createEditor'; import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; -import { Dropzone } from '@mantine/dropzone'; function CreateProgramKesehatan() { const router = useRouter(); const programKesehatanState = useProxy(programKesehatan); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Helper function to check if HTML content is empty + const isHtmlEmpty = (html: string) => { + // Remove all HTML tags and check if there's any text content + const textContent = html.replace(/<[^>]*>/g, '').trim(); + return textContent === ''; + }; + + // Check if form is valid + const isFormValid = () => { + return ( + programKesehatanState.create.form.name?.trim() !== '' && + !isHtmlEmpty(programKesehatanState.create.form.deskripsiSingkat) && + !isHtmlEmpty(programKesehatanState.create.form.deskripsi) && + file !== null + ); + }; const resetForm = () => { programKesehatanState.create.form = { @@ -46,36 +65,42 @@ function CreateProgramKesehatan() { if (!programKesehatanState.create.form.deskripsiSingkat) { return toast.warn("Deskripsi singkat wajib diisi"); } - if (!file) { - return toast.warn("Pilih file gambar terlebih dahulu"); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file gambar terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error("Gagal upload gambar"); + } + + programKesehatanState.create.form.imageId = uploaded.id; + await programKesehatanState.create.create(); + + resetForm(); + router.push("/admin/kesehatan/program-kesehatan"); + } catch (error) { + console.error("Error creating program kesehatan:", error); + toast.error("Gagal menambahkan program kesehatan"); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error("Gagal upload gambar"); - } - - programKesehatanState.create.form.imageId = uploaded.id; - await programKesehatanState.create.create(); - - resetForm(); - router.push("/admin/kesehatan/program-kesehatan"); }; return ( - + {/* Header */} - - - + Tambah Program Kesehatan @@ -92,7 +117,7 @@ function CreateProgramKesehatan() { > { programKesehatanState.create.form.name = val.target.value; }} @@ -139,7 +164,7 @@ function CreateProgramKesehatan() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -164,7 +189,7 @@ function CreateProgramKesehatan() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + + + {/* Tombol Simpan */} diff --git a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx index 55ce2a2b..b96a03ba 100644 --- a/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/program-kesehatan/page.tsx @@ -16,10 +16,9 @@ import { TableThead, TableTr, Text, - Title, - Tooltip + Title } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -49,48 +48,59 @@ function ListProgramKesehatan({ search }: { search: string }) { const router = useRouter(); const { data, page, totalPages, loading, load } = stateProgram.findMany; + const [debouncedSearch] = useDebouncedValue(search, 1000); useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - + + {/* Header List + Tombol Tambah */} - - Daftar Program Kesehatan - - - + + + Daftar Program Kesehatan + + - {/* Tabel */} - - + {/* Desktop Table */} + +
- Judul - Deskripsi Singkat - Deskripsi - Aksi + + Judul + + + Deskripsi Singkat + + + Deskripsi + + + Aksi + @@ -98,28 +108,25 @@ function ListProgramKesehatan({ search }: { search: string }) { filteredData.map((item) => ( - + {item.name} - - - + - - - + @@ -128,7 +135,9 @@ function ListProgramKesehatan({ search }: { search: string }) {
- Tidak ada program kesehatan yang cocok + + Tidak ada program kesehatan yang cocok +
@@ -136,6 +145,52 @@ function ListProgramKesehatan({ search }: { search: string }) {
+ + {/* Mobile Card View */} + + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + Judul + {item.name} + + + Deskripsi Singkat + + + + + + Deskripsi + + + + + + + + )) + ) : ( +
+ + Tidak ada program kesehatan yang cocok + +
+ )} +
+
{/* Pagination */} @@ -157,4 +212,4 @@ function ListProgramKesehatan({ search }: { search: string }) { ); } -export default ProgramKesehatan; +export default ProgramKesehatan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx index 9dd41e85..7869b690 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/edit/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client' @@ -5,16 +6,17 @@ import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/p import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -53,6 +55,7 @@ function EditPuskesmas() { const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', alamat: '', @@ -60,6 +63,25 @@ function EditPuskesmas() { kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' }, imageId: '', }); + const [originalData, setOriginalData] = useState({ + name: '', + alamat: '', + jam: { workDays: '', weekDays: '', holiday: '' }, + kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' }, + imageId: '', + imageUrl: '' + }); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.alamat?.trim() !== '' && + formData.jam.workDays?.trim() !== '' && + formData.jam.weekDays?.trim() !== '' && + formData.jam.holiday?.trim() !== '' + ); + }; useEffect(() => { const loadPuskesmas = async () => { @@ -86,6 +108,24 @@ function EditPuskesmas() { }, imageId: form.imageId, }); + setOriginalData({ + name: form.name, + alamat: form.alamat, + jam: { + workDays: form.jam.workDays, + weekDays: form.jam.weekDays, + holiday: form.jam.holiday, + }, + kontak: { + kontakPuskesmas: form.kontak.kontakPuskesmas, + email: form.kontak.email, + facebook: form.kontak.facebook, + kontakUGD: form.kontak.kontakUGD, + }, + imageId: form.imageId, + imageUrl: (form as any).image?.link + + }); const formWithImage = form as PuskesmasFormData; if (formWithImage.image?.link) { @@ -100,8 +140,31 @@ function EditPuskesmas() { loadPuskesmas(); }, [params?.id]); + const handleResetForm = () => { + setFormData({ + name: originalData.name, + alamat: originalData.alamat, + jam: { + workDays: originalData.jam.workDays, + weekDays: originalData.jam.weekDays, + holiday: originalData.jam.holiday, + }, + kontak: { + kontakPuskesmas: originalData.kontak.kontakPuskesmas, + email: originalData.kontak.email, + facebook: originalData.kontak.facebook, + kontakUGD: originalData.kontak.kontakUGD, + }, + imageId: originalData.imageId, + }); + setPreviewImage(originalData.imageUrl || null); + setFile(null); + toast.info("Form dikembalikan ke data awal"); + }; + const handleSubmit = async () => { try { + setIsSubmitting(true); statePuskesmas.edit.form = { ...statePuskesmas.edit.form, name: formData.name, @@ -131,6 +194,8 @@ function EditPuskesmas() { } catch (error) { console.error("Error updating puskesmas:", error); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data puskesmas"); + } finally { + setIsSubmitting(false); } }; @@ -147,14 +212,12 @@ function EditPuskesmas() { }; return ( - + {/* Header dengan tombol back */} - - - + Edit Puskesmas @@ -255,7 +318,7 @@ function EditPuskesmas() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -279,7 +342,7 @@ function EditPuskesmas() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} + {/* Tombol Batal */} + + + {/* Tombol Simpan */}
diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx index 2384ae36..02afb6b8 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/[id]/page.tsx @@ -1,13 +1,13 @@ 'use client' import colors from '@/con/colors'; -import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import { useShallowEffect } from '@mantine/hooks'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas'; function DetailPuskesmas() { const params = useParams(); @@ -40,7 +40,7 @@ function DetailPuskesmas() { const data = statePuskesmas.findUnique.data; return ( - + {/* Tombol kembali */} - - -
diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx index 871b1235..a2c14d97 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/create/page.tsx @@ -2,16 +2,17 @@ import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, Box, Button, Group, Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, + Title } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; @@ -26,6 +27,16 @@ function CreatePuskesmas() { const router = useRouter(); const [file, setFile] = useState(null); const [previewImage, setPreviewImage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + statePuskesmas.create.form.name?.trim() !== '' && + statePuskesmas.create.form.alamat?.trim() !== '' && + file !== null + ); + }; const resetForm = () => { statePuskesmas.create.form = { @@ -51,37 +62,43 @@ function CreatePuskesmas() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!file) { - return toast.warn('Pilih file gambar terlebih dahulu'); + try { + setIsSubmitting(true); + if (!file) { + return toast.warn('Pilih file gambar terlebih dahulu'); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }); + + const uploaded = res.data?.data; + if (!uploaded?.id) { + return toast.error('Gagal upload gambar'); + } + + statePuskesmas.create.form.imageId = uploaded.id; + await statePuskesmas.create.submit(); + + toast.success('Data berhasil disimpan'); + resetForm(); + router.push('/admin/kesehatan/puskesmas'); + } catch (error) { + console.error('Error creating posyandu:', error); + toast.error('Terjadi kesalahan saat membuat posyandu'); + } finally { + setIsSubmitting(false); } - - const res = await ApiFetch.api.fileStorage.create.post({ - file, - name: file.name, - }); - - const uploaded = res.data?.data; - if (!uploaded?.id) { - return toast.error('Gagal upload gambar'); - } - - statePuskesmas.create.form.imageId = uploaded.id; - await statePuskesmas.create.submit(); - - toast.success('Data berhasil disimpan'); - resetForm(); - router.push('/admin/kesehatan/puskesmas'); }; return ( - + {/* Header */} - - - + Tambah Data Puskesmas @@ -100,40 +117,40 @@ function CreatePuskesmas() { (statePuskesmas.create.form.name = e.target.value)} required /> (statePuskesmas.create.form.alamat = e.target.value)} required /> (statePuskesmas.create.form.jam.workDays = e.target.value)} /> (statePuskesmas.create.form.jam.weekDays = e.target.value)} /> (statePuskesmas.create.form.jam.holiday = e.target.value)} /> (statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value) } @@ -141,19 +158,19 @@ function CreatePuskesmas() { (statePuskesmas.create.form.kontak.email = e.target.value)} /> (statePuskesmas.create.form.kontak.facebook = e.target.value)} /> (statePuskesmas.create.form.kontak.kontakUGD = e.target.value)} /> @@ -171,7 +188,7 @@ function CreatePuskesmas() { }} onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} > @@ -196,7 +213,7 @@ function CreatePuskesmas() { {previewImage && ( - + Preview + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} {/* Action Button */} + {/* Tombol Batal */} + + {/* Tombol Simpan */} + diff --git a/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx b/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx index 44e4b74c..9f3eb215 100644 --- a/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx +++ b/src/app/admin/(dashboard)/kesehatan/puskesmas/page.tsx @@ -17,9 +17,8 @@ import { TableTr, Text, Title, - Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -49,48 +48,66 @@ function Puskesmas() { function ListPuskesmas({ search }: { search: string }) { const statePuskesmas = useProxy(puskesmasState); const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, totalPages, loading, load } = statePuskesmas.findMany; useShallowEffect(() => { - load(page, 10, search); - }, [page, search]); + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); const filteredData = data || []; if (loading || !data) { return ( - + ); } return ( - - - + + + Daftar Puskesmas - - - + - - + {/* Desktop Table */} + +
- Nama Puskesmas - Alamat - Kontak - Aksi + + + Nama Puskesmas + + + + + Alamat + + + + + Kontak + + + + + Aksi + + @@ -98,34 +115,33 @@ function ListPuskesmas({ search }: { search: string }) { filteredData.map((item) => ( - - - {item.name} - - + + {item.name} + - - - {item.alamat} - - + + {item.alamat} + - - - {item.kontak.kontakPuskesmas} - - + + {item.kontak.kontakPuskesmas} + @@ -133,8 +149,10 @@ function ListPuskesmas({ search }: { search: string }) { ) : ( -
- Tidak ada data puskesmas yang cocok +
+ + Tidak ada data puskesmas yang cocok +
@@ -142,6 +160,61 @@ function ListPuskesmas({ search }: { search: string }) {
+ + {/* Mobile Cards */} + + {filteredData.length > 0 ? ( + filteredData.map((item) => ( + + + + + Nama Puskesmas + + + {item.name} + + + + + Alamat + + + {item.alamat} + + + + + Kontak + + + {item.kontak.kontakPuskesmas} + + + + + + )) + ) : ( +
+ + Tidak ada data puskesmas yang cocok + +
+ )} +
{/* Pagination */} @@ -153,8 +226,7 @@ function ListPuskesmas({ search }: { search: string }) { window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} - mt="md" - mb="md" + my="md" color="blue" radius="md" /> @@ -163,4 +235,4 @@ function ListPuskesmas({ search }: { search: string }) { ); } -export default Puskesmas; +export default Puskesmas; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx similarity index 59% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx index dd65bf5a..d4730468 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/edit/page.tsx @@ -5,19 +5,20 @@ import sdgsDesa from "@/app/admin/(dashboard)/_state/landing-page/sdgs-desa"; import colors from "@/con/colors"; import ApiFetch from "@/lib/api-fetch"; import { + ActionIcon, Box, Button, Group, + Image, + Loader, Paper, Stack, Text, TextInput, - Title, - Tooltip, - Image, + Title } from "@mantine/core"; import { Dropzone } from "@mantine/dropzone"; -import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; @@ -33,6 +34,24 @@ export default function EditKolaborasiInovasi() { jumlah: "", imageId: "", }); + + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.jumlah?.trim() !== '' && + (formData.imageId?.trim() !== '' || file !== null) + ); + }; + + const [originalData, setOriginalData] = useState({ + name: "", + jumlah: "", + imageId: "", + imageUrl: "", + }); const [previewImage, setPreviewImage] = useState(null); const [file, setFile] = useState(null); @@ -45,14 +64,21 @@ export default function EditKolaborasiInovasi() { try { const data = await sdgsState.edit.load(id); if (data) { - setFormData({ + // isi form awal + const newForm = { name: data.name || "", jumlah: data.jumlah || "", imageId: data.imageId || "", + }; + setFormData(newForm); + + // simpan juga versi original + setOriginalData({ + ...newForm, + imageUrl: data.image?.link || "", }); - if (data.image?.link) { - setPreviewImage(data.image.link); - } + + setPreviewImage(data.image?.link || null); } } catch (error) { console.error("Error loading sdgs desa:", error); @@ -63,12 +89,24 @@ export default function EditKolaborasiInovasi() { loadKolaborasi(); }, [params?.id]); + 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"); + }; + const handleInputChange = (field: keyof typeof formData, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; const handleSubmit = async () => { try { + setIsSubmitting(true); let imageId = formData.imageId; // Upload file baru jika ada @@ -84,21 +122,21 @@ export default function EditKolaborasiInovasi() { await sdgsState.edit.update(); toast.success("sdgs desa berhasil diperbarui!"); - router.push("/admin/landing-page/sdgs-desa"); + router.push("/admin/landing-page/SDGs"); } catch (error) { console.error("Error updating sdgs desa:", error); toast.error("Terjadi kesalahan saat memperbarui sdgs desa"); + } finally { + setIsSubmitting(false); } }; return ( - + - - - + Edit Sdgs Desa @@ -115,7 +153,7 @@ export default function EditKolaborasiInovasi() { - Gambar Sdgs Desa + Gambar Program Inovasi { @@ -125,15 +163,15 @@ export default function EditKolaborasiInovasi() { setPreviewImage(URL.createObjectURL(selectedFile)); } }} - onReject={() => toast.error("File tidak valid, gunakan format gambar")} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} maxSize={5 * 1024 ** 2} - accept={{ "image/*": [] }} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} radius="md" p="xl" > - + @@ -146,25 +184,49 @@ export default function EditKolaborasiInovasi() { Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + {/* ✅ Preview gambar + tombol X */} {previewImage && ( - + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + )} - + {/* Tombol Batal */} + + {/* Tombol Simpan */} +
diff --git a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx similarity index 88% rename from src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx rename to src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx index 59b8f825..fa4ac4d9 100644 --- a/src/app/admin/(dashboard)/landing-page/sdgs-desa/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/SDGs/[id]/page.tsx @@ -1,10 +1,10 @@ 'use client' -import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; +import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; @@ -27,7 +27,7 @@ function DetailSDGSDesa() { sdgsState.delete.byId(selectedId) setModalHapus(false) setSelectedId(null) - router.push("/admin/landing-page/sdgs-desa") + router.push("/admin/landing-page/SDGs") } } @@ -42,7 +42,7 @@ function DetailSDGSDesa() { const data = sdgsState.findUnique.data; return ( - + - - -
diff --git a/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx new file mode 100644 index 00000000..14044c4c --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/SDGs/create/page.tsx @@ -0,0 +1,232 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import colors from '@/con/colors'; +import ApiFetch from '@/lib/api-fetch'; +import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title, Loader, ActionIcon } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; +import sdgsDesa from '../../../_state/landing-page/sdgs-desa'; + + +function CreateSDGsDesa() { + const router = useRouter(); + const stateSDGSDesa = useProxy(sdgsDesa) + const [previewImage, setPreviewImage] = useState(null); + const [file, setFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + stateSDGSDesa.create.form.name?.trim() !== '' && + stateSDGSDesa.create.form.jumlah?.trim() !== '' && + file !== null + ); + }; + + useEffect(() => { + stateSDGSDesa.findMany.load(); + }, []); + + const resetForm = () => { + stateSDGSDesa.create.form = { + name: "", + jumlah: "", + imageId: "", + }; + setFile(null); + setPreviewImage(null); + }; + const handleSubmit = async () => { + try { + setIsSubmitting(true); + if (!file) { + return toast.warn("Pilih file image terlebih dahulu"); + } + + const res = await ApiFetch.api.fileStorage.create.post({ + file, + name: file.name, + }) + + const uploaded = res.data?.data; + + if (!uploaded?.id) { + return toast.error("Gagal mengupload file"); + } + + stateSDGSDesa.create.form.imageId = uploaded.id; + + await stateSDGSDesa.create.create(); + + resetForm(); + router.push("/admin/landing-page/SDGs") + } catch (error) { + console.error(error); + toast.error("Gagal menambahkan sdgs desa") + } finally { + setIsSubmitting(false); + } + } + return ( + + + + + Tambah Sdgs Desa + + + + + + + + Gambar Program Inovasi + + { + const selectedFile = files[0]; + if (selectedFile) { + setFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih file + + + Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp + + + + + + {/* ✅ Preview gambar + tombol X */} + {previewImage && ( + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + + + )} + + { + stateSDGSDesa.create.form.name = e.currentTarget.value; + }} + required + /> + + + Jumlah + + } + placeholder="Masukkan jumlah" + value={stateSDGSDesa.create.form.jumlah} + onChange={(val) => { + stateSDGSDesa.create.form.jumlah = val.target.value; + }} + required + min={0} + radius="md" + /> + + {/* Tombol Batal */} + + + {/* Tombol Simpan */} + + + + + + ); +} + +export default CreateSDGsDesa; diff --git a/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx b/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx new file mode 100644 index 00000000..bc0a1506 --- /dev/null +++ b/src/app/admin/(dashboard)/landing-page/SDGs/page.tsx @@ -0,0 +1,204 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../_com/header'; +import sdgsDesa from '../../_state/landing-page/sdgs-desa'; + +function SdgsDesa() { + const [search, setSearch] = useState(''); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListSdgsDesa({ search }: { search: string }) { + const listState = useProxy(sdgsDesa); + const router = useRouter(); + const [debouncedSearch] = useDebouncedValue(search, 1000); + + + const { + data, + page, + totalPages, + loading, + load, + } = listState.findMany; + + useShallowEffect(() => { + load(page, 10, debouncedSearch); + }, [page, debouncedSearch]); + + const filteredData = data || []; + + // Handle loading state + if (loading || !data) { + return ( + + + + ); + } + + const isEmpty = data.length === 0; + + return ( + + + + + Daftar Sdgs Desa + + + + + {/* Desktop Table */} + + + + + + + Nama Sdgs Desa + + + + + Jumlah + + + + + Aksi + + + + + + {isEmpty ? ( + + + + Tidak ada data Sdgs Desa + + + + ) : ( + filteredData.map((item) => ( + + + + {item.name} + + + + + {item.jumlah || '0'} + + + + + + + )) + )} + +
+
+ + {/* Mobile Cards */} + + {isEmpty ? ( +
+ + Tidak ada data Sdgs Desa + +
+ ) : ( + + {filteredData.map((item) => ( + + + + Nama SDGs Desa + + {item.name} + + + + Jumlah + + {item.jumlah || '0'} + + + + + + + + ))} + + )} +
+
+ + {!isEmpty && ( +
+ { + load(newPage, 10); + window.scrollTo(0, 0); + }} + total={Math.max(1, totalPages)} + withEdges + radius="md" + /> +
+ )} +
+ ); +} + +export default SdgsDesa; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx index 4327ad4e..2c84dda3 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/edit/page.tsx @@ -1,65 +1,132 @@ /* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + 'use client'; import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'; import colors from '@/con/colors'; import ApiFetch from '@/lib/api-fetch'; import { + ActionIcon, + Badge, Box, Button, Group, Image, + Loader, + NumberInput, Paper, + Select, Stack, + Table, Text, TextInput, Title, - Tooltip, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { + IconArrowBack, + IconFile, + IconPhoto, + IconPlus, + IconTrash, + IconX +} from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; +// Tipe untuk form item +type ItemForm = { + kode: string; + uraian: string; + anggaran: number; + realisasi: number; + level: number; + tipe: 'pendapatan' | 'belanja' | 'pembiayaan'; +}; + function EditAPBDes() { const apbdesState = useProxy(apbdes); const router = useRouter(); const params = useParams(); - const [formData, setFormData] = useState({ - name: '', - jumlah: '', - imageId: '', - fileId: '' - }); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Check if form is valid + const isFormValid = () => { + return ( + apbdesState.edit.form.items.length > 0 + ); + }; const [previewImage, setPreviewImage] = useState(null); const [previewDoc, setPreviewDoc] = useState(null); const [imageFile, setImageFile] = useState(null); const [docFile, setDocFile] = useState(null); - // Load data on mount - useEffect(() => { - const loadData = async () => { - const id = params?.id as string; - if (!id) return; + // Form input untuk item baru + const [newItem, setNewItem] = useState({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + // Simpan data original untuk reset form + const [originalData, setOriginalData] = useState({ + tahun: 0, + imageId: '', + fileId: '', + imageUrl: '', + fileUrl: '', + }); + + // Load data saat pertama kali + useEffect(() => { + const id = params?.id as string; + if (!id) return; + + const loadData = async () => { try { const data = await apbdesState.edit.load(id); - if (data) { - setFormData({ - name: data.name || '', - jumlah: data.jumlah || '', - imageId: data.imageId || '', - fileId: data.fileId || '' - }); - setPreviewImage(data.image?.link || null); - setPreviewDoc(data.file?.link || null); - } - } catch (err) { - console.error(err); + + if (!data) return; + + // Set preview dari data lama + setPreviewImage(data.image?.link || null); + setPreviewDoc(data.file?.link || null); + + // Simpan data original untuk reset + 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 || '', + fileId: data.fileId || '', + items: (data.items || []).map((item: any) => ({ + kode: item.kode, + uraian: item.uraian, + anggaran: item.anggaran, + realisasi: item.realisasi, + selisih: item.selisih, + persentase: item.persentase, + level: item.level, + tipe: item.tipe || 'pendapatan', + })), + }; + } catch (error) { + console.error('Error loading APBDes:', error); toast.error('Gagal memuat data APBDes'); } }; @@ -67,7 +134,6 @@ function EditAPBDes() { loadData(); }, [params?.id]); - // Generic Dropzone handler const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => { const file = files[0]; if (!file) return; @@ -81,145 +147,424 @@ function EditAPBDes() { } }; + const handleAddItem = () => { + const { kode, uraian, anggaran, realisasi, level, tipe } = newItem; + if (!kode || !uraian) { + return toast.warn('Kode dan uraian wajib diisi'); + } + + const finalTipe = level === 1 ? null : tipe; + const selisih = realisasi - anggaran; + const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; + + apbdesState.edit.addItem({ + kode, + uraian, + anggaran, + realisasi, + selisih, + persentase, + level, + tipe: finalTipe, // ✅ Tidak akan undefined + }); + + + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + }; + + const handleRemoveItem = (index: number) => { + apbdesState.edit.removeItem(index); + }; + const handleSubmit = async () => { + if (apbdesState.edit.form.items.length === 0) { + return toast.warn('Minimal harus ada 1 item APBDes'); + } + try { - // Update global state with local form data first - apbdesState.edit.form = { ...apbdesState.edit.form, ...formData }; + setIsSubmitting(true); - // Helper function for uploading file - const uploadFile = async (file: File | null) => { - if (!file) return null; - const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); - const uploaded = res.data?.data; - if (!uploaded?.id) throw new Error('Upload gagal'); - return uploaded.id; - }; + // Upload file baru jika ada perubahan + if (imageFile) { + // Hapus file lama dari form jika ada file baru + const res = await ApiFetch.api.fileStorage.create.post({ + file: imageFile, + name: imageFile.name, + }); + const imageId = res.data?.data?.id; + if (imageId) { + apbdesState.edit.form.imageId = imageId; + } + } - // Upload files if selected - const uploadedImageId = await uploadFile(imageFile); - const uploadedDocId = await uploadFile(docFile); + if (docFile) { + // Hapus file lama dari form jika ada file baru + const res = await ApiFetch.api.fileStorage.create.post({ + file: docFile, + name: docFile.name, + }); + const fileId = res.data?.data?.id; + if (fileId) { + apbdesState.edit.form.fileId = fileId; + } + } - if (uploadedImageId) apbdesState.edit.form.imageId = uploadedImageId; - if (uploadedDocId) apbdesState.edit.form.fileId = uploadedDocId; + // Jika tidak ada file baru, gunakan ID lama (sudah ada di form) + // Pastikan imageId dan fileId tetap ada + if (!apbdesState.edit.form.imageId) { + return toast.warn('Gambar wajib diunggah'); + } + if (!apbdesState.edit.form.fileId) { + return toast.warn('Dokumen wajib diunggah'); + } - await apbdesState.edit.update(); - toast.success('APBDes berhasil diperbarui!'); - router.push('/admin/landing-page/apbdes'); + const success = await apbdesState.edit.update(); + if (success) { + router.push('/admin/landing-page/apbdes'); + } } catch (err) { - console.error(err); - toast.error('Terjadi kesalahan saat memperbarui APBDes'); + console.error('Update error:', err); + toast.error('Gagal memperbarui APBDes'); + } finally { + setIsSubmitting(false); } }; + const handleReset = () => { + // Reset ke data original (tahun, imageId, fileId) + apbdesState.edit.form = { + tahun: originalData.tahun, + imageId: originalData.imageId, + fileId: originalData.fileId, + items: [...apbdesState.edit.form.items], // keep existing items + }; + + // Reset preview ke data original + setPreviewImage(originalData.imageUrl || null); + setPreviewDoc(originalData.fileUrl || null); + + // Reset file uploads + setImageFile(null); + setDocFile(null); + + // Reset new item form + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + + toast.info('Form dikembalikan ke data awal'); + }; + return ( - + - - - + Edit APBDes - + - {/* Controlled Inputs */} - setFormData({ ...formData, name: e.target.value })} + {/* Header Form */} + + (apbdesState.edit.form.tahun = Number(val) || new Date().getFullYear()) + } + min={2000} + max={2100} required /> - setFormData({ ...formData, jumlah: e.target.value })} - required - /> + {/* Gambar & Dokumen */} + + + + Gambar APBDes + + toast.error('File gambar tidak valid')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + {previewImage ? 'Ganti gambar' : 'Unggah gambar'} + + + + + {previewImage && ( + + Preview + { + setPreviewImage(null); + setImageFile(null); + }} + > + + + + )} + - {/* Image Dropzone */} - - Gambar APBDes - toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - Seret gambar atau klik untuk memilih file - Maksimal 5MB, format gambar wajib - + + + Dokumen APBDes + + toast.error('File dokumen tidak valid')} + maxSize={10 * 1024 ** 2} + accept={{ + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + }} + radius="md" + p="xl" + > + + + + + + + {previewDoc ? 'Ganti dokumen' : 'Unggah dokumen'} + + + + + {previewDoc && ( + + + { + setPreviewDoc(null); + setDocFile(null); + }} + > + + + + )} + + + + {/* Input Item Baru */} + + + Tambah Item Pendapatan/Belanja + + + + setNewItem({ ...newItem, kode: e.target.value })} + required + /> + setNewItem({ ...newItem, tipe: (val as any) || 'pendapatan' })} + /> - - {previewImage && ( - - Preview Gambar - - )} - - - {/* Document Dropzone */} - - Dokumen APBDes - toast.error('File tidak valid, gunakan format dokumen')} - maxSize={10 * 1024 ** 2} - accept={{ - 'application/pdf': ['.pdf'], - 'application/msword': ['.doc'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'application/vnd.ms-excel': ['.xls'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - }} - radius="md" - p="xl" - > - - - - - - Seret dokumen atau klik untuk memilih file - Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX - + setNewItem({ ...newItem, uraian: e.target.value })} + required + /> + + setNewItem({ ...newItem, anggaran: Number(val) || 0 })} + thousandSeparator + min={0} + /> + setNewItem({ ...newItem, realisasi: Number(val) || 0 })} + thousandSeparator + min={0} + /> - - {previewDoc && ( - - Dokumen terpilih: {docFile?.name || 'Dokumen'} - - - )} - + + + - + {/* Tabel Items */} + {apbdesState.edit.form.items.length > 0 && ( + + + Daftar Item ({apbdesState.edit.form.items.length}) + + + + + + + + + + + + + + + {apbdesState.edit.form.items.map((item, idx) => ( + + + + + + + + + + ))} + +
KodeUraianAnggaranRealisasiLevelTipeAksi
+ + {item.kode} + + {item.uraian}{item.anggaran.toLocaleString('id-ID')}{item.realisasi.toLocaleString('id-ID')} + + L{item.level} + + + {item.tipe ? ( + + {item.tipe} + + ) : ( + '-' + )} + + handleRemoveItem(idx)}> + + +
+
+ )} + + {/* Tombol Aksi */} + + @@ -228,4 +573,4 @@ function EditAPBDes() { ); } -export default EditAPBDes; +export default EditAPBDes; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx index ce342456..b967d428 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/[id]/page.tsx @@ -1,36 +1,53 @@ -'use client' +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; import { useProxy } from 'valtio/utils'; -import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { + Box, + Button, + Group, + Image, + Paper, + Skeleton, + Stack, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text +} from '@mantine/core'; import { IconArrowBack, IconEdit, IconFile, IconTrash } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import colors from '@/con/colors'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import apbdes from '../../../_state/landing-page/apbdes'; + + function DetailAPBDes() { - const apbdesState = useProxy(apbdes) - const [modalHapus, setModalHapus] = useState(false) - const [selectedId, setSelectedId] = useState(null) - const params = useParams() - const router = useRouter() - - useShallowEffect(() => { - apbdesState.findUnique.load(params?.id as string) - }, []) + const apbdesState = useProxy(apbdes); + const [modalHapus, setModalHapus] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const params = useParams(); + const router = useRouter(); + useEffect(() => { + if (!params?.id) return; + apbdesState.findUnique.load(params.id as string); + }, [params?.id]); const handleHapus = () => { if (selectedId) { - apbdesState.delete.byId(selectedId) - setModalHapus(false) - setSelectedId(null) - router.push("/admin/landing-page/apbdes") + apbdesState.delete.byId(selectedId); + setModalHapus(false); + setSelectedId(null); + router.push('/admin/landing-page/apbdes'); } - } + }; if (!apbdesState.findUnique.data) { return ( @@ -42,8 +59,13 @@ function DetailAPBDes() { const data = apbdesState.findUnique.data; + // Helper: indentasi berdasarkan level + const getIndent = (level: number) => ({ + paddingLeft: `${(level - 1) * 20}px`, + }); + return ( - + - + - - - +
+ + {/* Tabel Items */} + {data.items && data.items.length > 0 ? ( + + + Rincian Pendapatan & Belanja ({data.items.length} item) + + + + + + Uraian + Anggaran (Rp) + Realisasi (Rp) + Selisih (Rp) + Persentase (%) + + + + {[...data.items] // Create a new array before sorting + .sort((a, b) => a.kode.localeCompare(b.kode)) + .map((item) => ( + + + + {item.kode} + {item.uraian} + + + {item.anggaran.toLocaleString('id-ID')} + {item.realisasi.toLocaleString('id-ID')} + + = 0 ? 'green' : 'red'}> + {item.selisih.toLocaleString('id-ID')} + + + + {item.persentase.toFixed(2)}% + + + ))} + +
+
+
+ ) : ( + Belum ada data item + )} diff --git a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx index 1925e0a5..1ac3945a 100644 --- a/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx +++ b/src/app/admin/(dashboard)/landing-page/apbdes/create/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ 'use client'; import colors from '@/con/colors'; @@ -12,82 +13,162 @@ import { Text, TextInput, Title, - Tooltip, + Loader, + ActionIcon, + NumberInput, + Select, + Table, + Badge, } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; +import { IconArrowBack, IconFile, IconPhoto, IconUpload, IconX, IconPlus, IconTrash } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useProxy } from 'valtio/utils'; import apbdes from '../../../_state/landing-page/apbdes'; +// Tipe item untuk form +type ItemForm = { + kode: string; + uraian: string; + anggaran: number; + realisasi: number; + level: number; + tipe: 'pendapatan' | 'belanja' | 'pembiayaan'; +}; function CreateAPBDes() { const router = useRouter(); - const stateAPBDes = useProxy(apbdes) + const stateAPBDes = useProxy(apbdes); const [previewImage, setPreviewImage] = useState(null); const [previewDoc, setPreviewDoc] = useState(null); const [imageFile, setImageFile] = useState(null); const [docFile, setDocFile] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + imageFile !== null && + docFile !== null && + stateAPBDes.create.form.items.length > 0 + ); + }; + + // Form sementara untuk input item baru + const [newItem, setNewItem] = useState({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); useEffect(() => { stateAPBDes.findMany.load(); }, []); const resetForm = () => { - stateAPBDes.create.form = { - name: "", - jumlah: "", - imageId: "", - fileId: "", - }; + stateAPBDes.create.reset(); setImageFile(null); setDocFile(null); setPreviewImage(null); + setPreviewDoc(null); + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); }; + const handleSubmit = async () => { if (!imageFile || !docFile) { return toast.warn("Pilih gambar dan dokumen terlebih dahulu"); } - + if (stateAPBDes.create.form.items.length === 0) { + return toast.warn("Minimal tambahkan 1 item APBDes"); + } + try { + setIsSubmitting(true); const [uploadImageRes, uploadDocRes] = await Promise.all([ ApiFetch.api.fileStorage.create.post({ file: imageFile, name: imageFile.name }), ApiFetch.api.fileStorage.create.post({ file: docFile, name: docFile.name }), ]); - + const imageId = uploadImageRes?.data?.data?.id; const fileId = uploadDocRes?.data?.data?.id; - + if (!imageId || !fileId) { return toast.error("Gagal mengupload file"); } - + + // Update form dengan ID file stateAPBDes.create.form.imageId = imageId; stateAPBDes.create.form.fileId = fileId; - + await stateAPBDes.create.create(); - + toast.success("Berhasil menambahkan APBDes"); resetForm(); router.push("/admin/landing-page/apbdes"); } catch (error) { console.error("Gagal submit:", error); toast.error("Gagal menyimpan data"); + } finally { + setIsSubmitting(false); } }; - + + // Tambahkan item ke state + const handleAddItem = () => { + const { kode, uraian, anggaran, realisasi, level, tipe } = newItem; + if (!kode || !uraian) { + return toast.warn("Kode dan uraian wajib diisi"); + } + + const finalTipe = level === 1 ? null : tipe; + const selisih = realisasi - anggaran; + const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; + + stateAPBDes.create.addItem({ + kode, + uraian, + anggaran, + realisasi, + selisih, + persentase, + level, + tipe: finalTipe, + }); + + // Reset form input + setNewItem({ + kode: '', + uraian: '', + anggaran: 0, + realisasi: 0, + level: 1, + tipe: 'pendapatan', + }); + }; + + // Hapus item + const handleRemoveItem = (index: number) => { + stateAPBDes.create.removeItem(index); + }; + return ( - + - - - + Tambah APBDes @@ -102,147 +183,309 @@ function CreateAPBDes() { style={{ border: '1px solid #e0e0e0' }} > - {/* Gambar APBDes */} - - - Gambar APBDes - - { - const selectedFile = files[0]; - if (selectedFile) { - setImageFile(selectedFile); - setPreviewImage(URL.createObjectURL(selectedFile)); - } - }} - onReject={() => toast.error('File tidak valid, gunakan format gambar')} - maxSize={5 * 1024 ** 2} - accept={{ 'image/*': [] }} - radius="md" - p="xl" - > - - - - - - - - - - - - - Seret gambar atau klik untuk memilih file - - - Maksimal 5MB (format: JPEG, JPG, PNG, GIF, WEBP, SVG) - + {/* Gambar & Dokumen (dipendekkan untuk fokus pada items) */} + + {/* Gambar APBDes */} + + + Gambar APBDes + + { + const selectedFile = files[0]; + if (selectedFile) { + setImageFile(selectedFile); + setPreviewImage(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid, gunakan format gambar')} + maxSize={5 * 1024 ** 2} + accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret gambar atau klik untuk memilih + + + + + {previewImage && ( + + Preview Gambar + + {/* Tombol hapus (pojok kanan atas) */} + { + setPreviewImage(null); + setImageFile(null); + }} + style={{ + boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + }} + > + + - - + )} + - {previewImage && ( - - Preview Gambar - - )} - + {/* Dokumen APBDes */} + + + Dokumen APBDes + + { + const selectedFile = files[0]; + if (selectedFile) { + setDocFile(selectedFile); + setPreviewDoc(URL.createObjectURL(selectedFile)); + } + }} + onReject={() => toast.error('File tidak valid')} + maxSize={5 * 1024 ** 2} + accept={{ + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + }} + radius="md" + p="xl" + > + + + + + + + + + + + + + Seret dokumen atau klik untuk memilih + + + + + {previewDoc && ( + + + Pratinjau Dokumen + + + + - + - ) : null} + )} @@ -139,4 +205,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx b/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx index 1ccaa2f0..56e3cda8 100644 --- a/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx +++ b/src/app/darmasaba/(pages)/keamanan/polsek-terdekat/semua-polsek/page.tsx @@ -1,20 +1,19 @@ 'use client' -import polsekTerdekatState from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat'; +import statePolsekTerdekat from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat'; import colors from '@/con/colors'; -import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; -import { useShallowEffect } from '@mantine/hooks'; +import { Box, Button, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; +import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { IconNavigation, IconSearch } from '@tabler/icons-react'; -import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import BackButton from '../../../desa/layanan/_com/BackButto'; -import { useRouter } from 'next/navigation'; -import { useDebouncedValue } from '@mantine/hooks'; function Page() { - const state = useProxy(polsekTerdekatState); + const state = useProxy(statePolsekTerdekat.polsekTerdekatState); const [search, setSearch] = useState(''); - const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay - const router = useRouter() + const [debouncedSearch] = useDebouncedValue(search, 1000); + const router = useRouter(); const { data, @@ -25,71 +24,98 @@ function Page() { } = state.findMany; useShallowEffect(() => { - load(page, 3, debouncedSearch) - }, [page, debouncedSearch]) + load(page, 3, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( - ) + ); } return ( - + - + + - + Semua Polsek Terdekat - </Text> + setSearch(e.target.value)} leftSection={} - w={{ base: "50%", md: "100%" }} + w={{ base: '50%', md: '100%' }} /> - - - {data.map((v, k) => { - return ( - - - {v.nama} - Alamat: {v.alamat} - Jarak: {v.jarakKeDesa} - Telepon: {v.nomorTelepon} - Jam Operasional: {v.jamOperasional} - - - - - - - - - ) - })} + + + + {data.map((v, k) => ( + + + + {v.nama} + + + Alamat: {v.alamat} + + + Jarak: {v.jarakKeDesa} + + + Telepon: {v.nomorTelepon} + + + Jam Operasional: {v.jamOperasional} + + + + + + + + + + ))} +
load(newPage)} // ini penting! + onChange={(newPage) => load(newPage)} total={totalPages} mt="md" mb="md" @@ -99,4 +125,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx b/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx index e4709396..f72913c1 100644 --- a/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx +++ b/src/app/darmasaba/(pages)/keamanan/tips-keamanan/page.tsx @@ -1,6 +1,6 @@ 'use client' import colors from '@/con/colors'; -import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; +import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; import BackButton from '../../desa/layanan/_com/BackButto'; import { useProxy } from 'valtio/utils'; import tipsKeamananState from '@/app/admin/(dashboard)/_state/keamanan/tips-keamanan'; @@ -8,11 +8,10 @@ import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useState } from 'react'; import { IconSearch } from '@tabler/icons-react'; - function Page() { - const state = useProxy(tipsKeamananState) - const [search, setSearch] = useState('') - const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay + const state = useProxy(tipsKeamananState); + const [search, setSearch] = useState(''); + const [debouncedSearch] = useDebouncedValue(search, 1000); const { data, page, @@ -22,81 +21,119 @@ function Page() { } = state.findMany; useShallowEffect(() => { - load(page, 3, debouncedSearch) - }, [page, debouncedSearch]) + load(page, 3, debouncedSearch); + }, [page, debouncedSearch]); if (loading || !data) { return ( - + - ) + ); } return ( - + + - + - + Tips Keamanan - </Text> + setSearch(e.target.value)} leftSection={} - w={{ base: "50%", md: "100%" }} + w={'100%'} /> - - Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. + + + Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). + + + Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga. - - + + + - {data.map((v, k) => { - return ( - - -
- -
- - - - {v.judul} - - - - + pb="10" + cols={{ base: 1, md: 3 }} + > + {data.map((v, k) => ( + + +
+ + {v.judul} + +
+ + + + {v.judul} + + + -
-
- ) - })} +
+
+
+ ))}
+
load(newPage)} // ini penting! + onChange={(newPage) => load(newPage)} total={totalPages} my="md" /> @@ -105,4 +142,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx index 7fe14d54..52ff293a 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx @@ -2,7 +2,7 @@ import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import colors from '@/con/colors'; -import { Box, Divider, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { Box, Divider, Flex, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react'; import { useParams } from 'next/navigation'; @@ -37,25 +37,25 @@ function Page() { - + {state.findUnique.data.title || 'Detail Artikel Kesehatan'} - </Text> + - {state.findUnique.data.title} @@ -64,7 +64,7 @@ function Page() { - + {new Date(state.findUnique.data.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', @@ -74,50 +74,63 @@ function Page() { - - Pendahuluan + Pendahuluan - + + + - {state.findUnique.data.symptom?.title} + {state.findUnique.data.symptom?.title} - + + + - {state.findUnique.data.prevention?.title} + {state.findUnique.data.prevention?.title} - + + + - {state.findUnique.data.firstaid?.title} + {state.findUnique.data.firstaid?.title} - + + + - {state.findUnique.data.mythvsfact?.title} + {state.findUnique.data.mythvsfact?.title} - +
- Mitos - Fakta + Mitos + Fakta {state.findUnique.data?.mythvsfact ? ( - + + + - + + + ) : ( @@ -131,36 +144,35 @@ function Page() { - Kapan Harus ke Dokter? + Kapan Harus ke Dokter? - + - Segera bawa penderita ke fasilitas kesehatan jika mengalami: - - + Segera bawa penderita ke fasilitas kesehatan jika mengalami: + + + + - Kasus DBD di Wilayah Abiansemal - - - Informasi Lebih Lanjut + Informasi Lebih Lanjut - Hotline DBD: (0361) 123456 - WhatsApp Center: 081234567890 - Email: p2p@dinkes.badungkab.go.id + Hotline DBD: (0361) 123456 + WhatsApp Center: 081234567890 + Email: p2p@dinkes.badungkab.go.id - Referensi + Referensi - + Kementerian Kesehatan RI. (2024). Pedoman Pencegahan dan Pengendalian DBD. World Health Organization. (2024). Dengue Guidelines for Diagnosis, Treatment, Prevention and Control. Dinas Kesehatan Kabupaten Badung. (2025). Laporan Surveilans DBD Triwulan I 2025. @@ -176,4 +188,4 @@ function Page() { ); } -export default Page; +export default Page; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx index defb85c2..31e23ce5 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/page.tsx @@ -1,7 +1,7 @@ 'use client' import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import colors from '@/con/colors'; -import { Box, Button, Card, Divider, Group, Image, Loader, Paper, Stack, Text } from '@mantine/core'; +import { Box, Button, Card, Divider, Group, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; import { IconCalendar, IconChevronRight } from '@tabler/icons-react'; import { useRouter } from 'next/navigation'; @@ -17,9 +17,8 @@ function ArtikelKesehatanPage() { if (!state.findMany.data) { return ( - - - Memuat artikel kesehatan... + + ) } @@ -28,13 +27,13 @@ function ArtikelKesehatanPage() { - + Artikel Kesehatan - </Text> + {state.findMany.data.length === 0 ? ( - + Belum ada artikel kesehatan yang tersedia @@ -51,17 +50,26 @@ function ArtikelKesehatanPage() { onMouseLeave={(e) => (e.currentTarget.style.transform = 'translateY(0)')} > - + - {item.title} + + {item.title} + - - + + {new Date(item.createdAt).toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' })} • Dinas Kesehatan - + {item.content} @@ -85,4 +93,4 @@ function ArtikelKesehatanPage() { ); } -export default ArtikelKesehatanPage; +export default ArtikelKesehatanPage; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx index 6aa24dfd..eb981540 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/fasilitas-kesehatan-page/[id]/page.tsx @@ -16,7 +16,6 @@ interface Kontak { email: string; } - interface Lokasi { mapsEmbed: string; } @@ -35,16 +34,14 @@ function Page() { state.findUnique.load(params?.id as string); }, []); - const data = state.findUnique.data as any; // Temporary any to fix type issues + const data = state.findUnique.data as any; const nama = data?.name || 'Fasilitas Kesehatan'; const prosedur = data?.prosedurpendaftaran.content || ''; const alamat = data?.informasiumum?.alamat || '-'; const jam = data?.informasiumum?.jamOperasional || '-'; const layananUnggulan = data?.layananunggulan?.content || ''; - const tenaga = data?.dokterdantenagamedis || null; const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || ''; - const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null; const kontak = (data?.kontak as Kontak) || { telepon: '(0361) 123456', whatsapp: '6289647037426', @@ -113,11 +110,11 @@ function Page() { - {alamat} + {alamat} - {kontak.telepon} + {kontak.telepon} {({ copied, copy }) => ( @@ -128,7 +125,7 @@ function Page() { - {kontak.whatsapp} + {kontak.whatsapp} {({ copied, copy }) => ( @@ -139,7 +136,7 @@ function Page() { - {kontak.email} + {kontak.email} {({ copied, copy }) => ( @@ -165,31 +162,43 @@ function Page() { - Nama Fasilitas - {nama} + Nama Fasilitas + {nama} - Jam Operasional - {jam} + Jam Operasional + {jam} Layanan Unggulan {layananUnggulan ? ( - + + + ) : ( - Belum ada informasi fasilitas pendukung. + Belum ada informasi layanan unggulan. )} Peta Lokasi -