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/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 index e69de29b..21d07dbf 100644 --- a/QWEN.md +++ 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 a862a5a4..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/package.json b/package.json index 065dc1c1..61958b92 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start" + "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" @@ -59,6 +62,7 @@ "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": "^8.6.0", @@ -106,17 +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/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/schema.prisma b/prisma/schema.prisma index ac06a3a0..5a9168cd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -236,7 +236,7 @@ model PrestasiDesa { imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -245,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[] } @@ -263,7 +263,7 @@ model Responden { kelompokUmurId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -272,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[] } @@ -282,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[] } @@ -292,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[] } @@ -326,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[] @@ -345,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[] @@ -370,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) } @@ -381,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) } @@ -398,7 +400,7 @@ model ProfilePPID { imageId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -410,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) } @@ -431,7 +433,7 @@ model PermohonanInformasiPublik { caraMemperolehSalinanInformasiId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -440,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[] } @@ -450,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[] } @@ -460,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[] } @@ -474,7 +476,7 @@ model FormulirPermohonanKeberatan { alasan String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -531,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) } @@ -541,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) } @@ -551,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) } @@ -562,7 +564,7 @@ model MaskotDesa { images ProfileDesaImage[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) + deletedAt DateTime? isActive Boolean @default(true) } @@ -631,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[] } 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/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)/_state/landing-page/apbdes.ts b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts index df9cde1d..9b5cb438 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/apbdes.ts @@ -38,11 +38,9 @@ function normalizeItem(item: Partial>): z.infer const anggaran = item.anggaran ?? 0; const realisasi = item.realisasi ?? 0; - - // ✅ Formula yang benar - const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget + const selisih = realisasi - anggaran; // positif = sisa anggaran, negatif = over budget const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran return { diff --git a/src/app/admin/(dashboard)/_state/landing-page/profile.ts b/src/app/admin/(dashboard)/_state/landing-page/profile.ts index d4abaf81..152f16d7 100644 --- a/src/app/admin/(dashboard)/_state/landing-page/profile.ts +++ b/src/app/admin/(dashboard)/_state/landing-page/profile.ts @@ -55,10 +55,15 @@ const programInovasi = proxy({ programInovasi.findMany.load(); return toast.success("Sukses menambahkan"); } - console.log(res); + if (process.env.NODE_ENV === 'development') { + console.log(res); + } return toast.error("failed create"); } catch (error) { - console.log((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.error("Create error:", error); + } + toast.error("Gagal menambahkan data"); } finally { programInovasi.create.loading = false; } @@ -91,13 +96,17 @@ const programInovasi = proxy({ programInovasi.findMany.total = res.data.total || 0; programInovasi.findMany.totalPages = res.data.totalPages || 1; } else { - console.error("Failed to load pegawai:", res.data?.message); + if (process.env.NODE_ENV === 'development') { + console.error("Failed to load pegawai:", res.data?.message); + } programInovasi.findMany.data = []; programInovasi.findMany.total = 0; programInovasi.findMany.totalPages = 1; } } catch (error) { - console.error("Error loading pegawai:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading pegawai:", error); + } programInovasi.findMany.data = []; programInovasi.findMany.total = 0; programInovasi.findMany.totalPages = 1; @@ -112,19 +121,25 @@ const programInovasi = proxy({ image: true; }; }> | null, + loading: false, async load(id: string) { try { - const res = await fetch(`/api/landingpage/programinovasi/${id}`); - if (res.ok) { - const data = await res.json(); - programInovasi.findUnique.data = data.data ?? null; + programInovasi.findUnique.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get(); + if (res.data?.success) { + programInovasi.findUnique.data = res.data.data ?? null; + return res.data.data; } else { - console.error("Failed to fetch program inovasi:", res.statusText); + toast.error(res.data?.message || "Gagal memuat data program inovasi"); programInovasi.findUnique.data = null; + return null; } } catch (error) { console.error("Error fetching program inovasi:", error); programInovasi.findUnique.data = null; + return null; + } finally { + programInovasi.findUnique.loading = false; } }, }, @@ -135,27 +150,18 @@ const programInovasi = proxy({ try { programInovasi.delete.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)["del"][id].delete(); - const response = await fetch( - `/api/landingpage/programinovasi/del/${id}`, - { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - } - ); - - const result = await response.json(); - - if (response.ok && result?.success) { - toast.success(result.message || "Program inovasi berhasil dihapus"); - await programInovasi.findMany.load(); // refresh list + if (res.data?.success) { + toast.success(res.data.message || "Program inovasi berhasil dihapus"); + await programInovasi.findMany.load(); } else { - toast.error(result?.message || "Gagal menghapus program inovasi"); + toast.error(res.data?.message || "Gagal menghapus program inovasi"); } } catch (error) { - console.error("Gagal delete:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Gagal delete:", error); + } toast.error("Terjadi kesalahan saat menghapus program inovasi"); } finally { programInovasi.delete.loading = false; @@ -174,20 +180,11 @@ const programInovasi = proxy({ } try { - const response = await fetch(`/api/landingpage/programinovasi/${id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + programInovasi.update.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)[id].get(); - const result = await response.json(); - - if (result?.success) { - const data = result.data; + if (res.data?.success) { + const data = res.data.data; this.id = data.id; this.form = { name: data.name, @@ -197,13 +194,15 @@ const programInovasi = proxy({ }; return data; } else { - throw new Error( - result?.message || "Gagal mengambil data program inovasi" - ); + toast.error(res.data?.message || "Gagal mengambil data program inovasi"); + return null; } } catch (error) { - console.error((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading program inovasi:", error); + } toast.error("Terjadi kesalahan saat mengambil data program inovasi"); + return null; } finally { programInovasi.update.loading = false; } @@ -221,41 +220,25 @@ const programInovasi = proxy({ try { programInovasi.update.loading = true; + const res = await (ApiFetch.api.landingpage.programinovasi as any)[this.id].put({ + name: this.form.name, + description: this.form.description, + imageId: this.form.imageId, + link: this.form.link, + }); - const response = await fetch( - `/api/landingpage/programinovasi/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - description: this.form.description, - imageId: this.form.imageId, - link: this.form.link, - }), - } - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.message || `HTTP error! status: ${response.status}` - ); - } - - const result = await response.json(); - - if (result.success) { + if (res.data?.success) { toast.success("Berhasil update program inovasi"); - await programInovasi.findMany.load(); // refresh list + await programInovasi.findMany.load(); return true; } else { - throw new Error(result.message || "Gagal update program inovasi"); + toast.error(res.data?.message || "Gagal update program inovasi"); + return false; } } catch (error) { - console.error("Error updating program inovasi:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error updating program inovasi:", error); + } toast.error( error instanceof Error ? error.message @@ -443,7 +426,7 @@ const pejabatDesa = proxy({ const templateMediaSosial = z.object({ name: z.string().min(3, "Nama minimal 3 karakter"), imageId: z.string().nullable().optional(), - iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"), + iconUrl: z.string().optional(), // ✅ Optional - tidak selalu required icon: z.string().nullable().optional(), }); @@ -484,10 +467,15 @@ const mediaSosial = proxy({ mediaSosial.findMany.load(); return toast.success("Sukses menambahkan"); } - console.log(res); + if (process.env.NODE_ENV === 'development') { + console.log(res); + } return toast.error("failed create"); } catch (error) { - console.log((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.log((error as Error).message); + } + toast.error("Gagal menambahkan data"); } finally { mediaSosial.create.loading = false; } @@ -518,13 +506,17 @@ const mediaSosial = proxy({ mediaSosial.findMany.total = res.data.total || 0; mediaSosial.findMany.totalPages = res.data.totalPages || 1; } else { - console.error("Failed to load media sosial:", res.data?.message); + if (process.env.NODE_ENV === 'development') { + console.error("Failed to load media sosial:", res.data?.message); + } mediaSosial.findMany.data = []; mediaSosial.findMany.total = 0; mediaSosial.findMany.totalPages = 1; } } catch (error) { - console.error("Error loading media sosial:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading media sosial:", error); + } mediaSosial.findMany.data = []; mediaSosial.findMany.total = 0; mediaSosial.findMany.totalPages = 1; @@ -539,25 +531,32 @@ const mediaSosial = proxy({ image: true; }; }> | null, + loading: false, async load(id: string) { if (!id) { toast.warn("ID tidak valid"); return null; } - mediaSosial.update.loading = true; + mediaSosial.findUnique.loading = true; try { - const res = await fetch(`/api/landingpage/mediasosial/${id}`); - if (res.ok) { - const data = await res.json(); - mediaSosial.findUnique.data = data.data ?? null; + const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get(); + if (res.data?.success) { + mediaSosial.findUnique.data = res.data.data ?? null; + return res.data.data; } else { - console.error("Failed to fetch media sosial:", res.statusText); + toast.error(res.data?.message || "Gagal memuat data media sosial"); mediaSosial.findUnique.data = null; + return null; } } catch (error) { - console.error("Error fetching media sosial:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error fetching media sosial:", error); + } mediaSosial.findUnique.data = null; + return null; + } finally { + mediaSosial.findUnique.loading = false; } }, }, @@ -568,24 +567,18 @@ const mediaSosial = proxy({ try { mediaSosial.delete.loading = true; + const res = await (ApiFetch.api.landingpage.mediasosial as any)["del"][id].delete(); - const response = await fetch(`/api/landingpage/mediasosial/del/${id}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }); - - const result = await response.json(); - - if (response.ok && result?.success) { - toast.success(result.message || "Media Sosial berhasil dihapus"); - await mediaSosial.findMany.load(); // refresh list + if (res.data?.success) { + toast.success(res.data.message || "Media Sosial berhasil dihapus"); + await mediaSosial.findMany.load(); } else { - toast.error(result?.message || "Gagal menghapus media sosial"); + toast.error(res.data?.message || "Gagal menghapus media sosial"); } } catch (error) { - console.error("Gagal delete:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Gagal delete:", error); + } toast.error("Terjadi kesalahan saat menghapus media sosial"); } finally { mediaSosial.delete.loading = false; @@ -603,43 +596,32 @@ const mediaSosial = proxy({ return null; } - mediaSosial.update.loading = true; // ✅ Tambahkan ini di awal - + mediaSosial.update.loading = true; try { - const response = await fetch(`/api/landingpage/mediasosial/${id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const res = await (ApiFetch.api.landingpage.mediasosial as any)[id].get(); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - - if (result?.success) { - const data = result.data; + if (res.data?.success) { + const data = res.data.data; this.id = data.id; this.form = { name: data.name || "", imageId: data.imageId || null, iconUrl: data.iconUrl || "", icon: data.icon || null, - }; return data; } else { - throw new Error( - result?.message || "Gagal mengambil data media sosial" - ); + toast.error(res.data?.message || "Gagal mengambil data media sosial"); + return null; } } catch (error) { - console.error((error as Error).message); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading media sosial:", error); + } toast.error("Terjadi kesalahan saat mengambil data media sosial"); + return null; } finally { - mediaSosial.update.loading = false; // ✅ Supaya berhenti loading walau error + mediaSosial.update.loading = false; } }, @@ -655,41 +637,25 @@ const mediaSosial = proxy({ try { mediaSosial.update.loading = true; + const res = await (ApiFetch.api.landingpage.mediasosial as any)[this.id].put({ + name: this.form.name, + imageId: this.form.imageId, + iconUrl: this.form.iconUrl, + icon: this.form.icon, + }); - const response = await fetch( - `/api/landingpage/mediasosial/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - imageId: this.form.imageId, - iconUrl: this.form.iconUrl, - icon: this.form.icon, - }), - } - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.message || `HTTP error! status: ${response.status}` - ); - } - - const result = await response.json(); - - if (result.success) { + if (res.data?.success) { toast.success("Berhasil update media sosial"); - await mediaSosial.findMany.load(); // refresh list + await mediaSosial.findMany.load(); return true; } else { - throw new Error(result.message || "Gagal update media sosial"); + toast.error(res.data?.message || "Gagal update media sosial"); + return false; } } catch (error) { - console.error("Error updating media sosial:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error updating media sosial:", error); + } toast.error( error instanceof Error ? error.message diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx index 6f89e888..dfb687c4 100644 --- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/[id]/page.tsx @@ -33,6 +33,13 @@ function EditKategoriBerita() { name: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' + ); + }; + useEffect(() => { const loadKategori = async () => { const id = params?.id as string; @@ -72,6 +79,11 @@ function EditKategoriBerita() { }; const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Nama kategori berita wajib diisi'); + return; + } + try { setIsSubmitting(true); // update global state hanya saat submit @@ -143,8 +155,11 @@ function EditKategoriBerita() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx index f6957fb0..c047ccfa 100644 --- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/create/page.tsx @@ -22,6 +22,13 @@ function CreateKategoriBerita() { const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); + // Check if form is valid + const isFormValid = () => { + return ( + createState.create.form.name?.trim() !== '' + ); + }; + const resetForm = () => { createState.create.form = { name: '', @@ -29,6 +36,11 @@ function CreateKategoriBerita() { }; const handleSubmit = async () => { + if (!createState.create.form.name?.trim()) { + toast.error('Nama kategori berita wajib diisi'); + return; + } + setIsSubmitting(true); try { await createState.create.create(); @@ -93,8 +105,11 @@ function CreateKategoriBerita() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx index 82be57ec..5f6d7a78 100644 --- a/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/kategori-berita/page.tsx @@ -160,7 +160,7 @@ function ListKategoriBerita({ search }: { search: string }) { )) ) : ( - + {/* ✅ Match column count (3 columns) */}
Tidak ada data kategori berita yang cocok diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx index 101c57bd..414e2749 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/[id]/edit/page.tsx @@ -48,6 +48,24 @@ function EditBerita() { 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.judul?.trim() !== '' && + formData.kategoriBeritaId !== '' && + !isHtmlEmpty(formData.deskripsi) && + (file !== null || originalData.imageId !== '') && // Either a new file is selected or an existing image exists + !isHtmlEmpty(formData.content) + ); + }; + const [originalData, setOriginalData] = useState({ judul: "", deskripsi: "", @@ -103,6 +121,31 @@ function EditBerita() { }; const handleSubmit = async () => { + if (!formData.judul?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (!formData.kategoriBeritaId) { + toast.error('Kategori wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi singkat wajib diisi'); + return; + } + + if (!file && !originalData.imageId) { + toast.error('Gambar wajib dipilih'); + return; + } + + if (isHtmlEmpty(formData.content)) { + toast.error('Konten wajib diisi'); + return; + } + try { setIsSubmitting(true); // Update global state hanya sekali di sini @@ -326,8 +369,11 @@ function EditBerita() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx index 99938ff1..0ae98722 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/create/page.tsx @@ -32,6 +32,24 @@ export default function CreateBerita() { 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 ( + beritaState.berita.create.form.judul?.trim() !== '' && + beritaState.berita.create.form.kategoriBeritaId !== '' && + !isHtmlEmpty(beritaState.berita.create.form.deskripsi) && + file !== null && + !isHtmlEmpty(beritaState.berita.create.form.content) + ); + }; + useShallowEffect(() => { beritaState.kategoriBerita.findMany.load(); }, []); @@ -49,6 +67,31 @@ export default function CreateBerita() { }; const handleSubmit = async () => { + if (!beritaState.berita.create.form.judul?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (!beritaState.berita.create.form.kategoriBeritaId) { + toast.error('Kategori wajib dipilih'); + return; + } + + if (isHtmlEmpty(beritaState.berita.create.form.deskripsi)) { + toast.error('Deskripsi singkat wajib diisi'); + return; + } + + if (!file) { + toast.error('Gambar wajib dipilih'); + return; + } + + if (isHtmlEmpty(beritaState.berita.create.form.content)) { + toast.error('Konten wajib diisi'); + return; + } + try { setIsSubmitting(true); if (!file) { @@ -250,8 +293,11 @@ export default function CreateBerita() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx index 97dc4d11..c36447f6 100644 --- a/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx +++ b/src/app/admin/(dashboard)/desa/berita/list-berita/page.tsx @@ -187,7 +187,7 @@ function ListBerita({ search }: { search: string }) { { - load(newPage, 10); + load(newPage, 10, debouncedSearch); // ✅ Include search parameter window.scrollTo({ top: 0, behavior: 'smooth' }); }} total={totalPages} diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx index 21dd4231..741e0b99 100644 --- a/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/foto/[id]/edit/page.tsx @@ -45,6 +45,22 @@ function EditFoto() { 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) && + (file !== null || originalData.imagesId !== '') // Either a new file is selected or an existing image exists + ); + }; + const [originalData, setOriginalData] = useState({ name: "", deskripsi: "", @@ -94,6 +110,21 @@ function EditFoto() { }; const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (!file && !originalData.imagesId) { + toast.error('Gambar wajib dipilih'); + return; + } + try { setIsSubmitting(true); // Update global state hanya sekali di sini @@ -285,8 +316,11 @@ function EditFoto() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx index 72193ec5..cf076af2 100644 --- a/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/foto/create/page.tsx @@ -30,6 +30,22 @@ function CreateFoto() { 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 ( + FotoState.create.form.name?.trim() !== '' && + !isHtmlEmpty(FotoState.create.form.deskripsi) && + file !== null + ); + }; + const resetForm = () => { FotoState.create.form = { name: '', @@ -41,6 +57,21 @@ function CreateFoto() { }; const handleSubmit = async () => { + if (!FotoState.create.form.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(FotoState.create.form.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (!file) { + toast.error('Gambar wajib dipilih'); + return; + } + try { setIsSubmitting(true); if (!file) { @@ -210,8 +241,11 @@ function CreateFoto() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx index a6647bd5..40dfb76f 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/[id]/edit/page.tsx @@ -28,6 +28,24 @@ function EditVideo() { 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 = () => { + const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); + return ( + formData.name?.trim() !== '' && + formData.linkVideo?.trim() !== '' && + !isHtmlEmpty(formData.deskripsi) && + embedLink !== null // Make sure the embed link is valid + ); + }; + const [originalData, setOriginalData] = useState({ name: "", deskripsi: "", @@ -86,6 +104,21 @@ function EditVideo() { }; const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (!formData.linkVideo?.trim()) { + toast.error('Link YouTube wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + try { setIsSubmitting(true); const converted = convertYoutubeUrlToEmbed(formData.linkVideo); @@ -218,8 +251,11 @@ function EditVideo() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx index 6896221f..ae0f3870 100644 --- a/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx +++ b/src/app/admin/(dashboard)/desa/gallery/video/create/page.tsx @@ -28,6 +28,23 @@ function CreateVideo() { 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 ( + videoState.create.form.name?.trim() !== '' && + link.trim() !== '' && + !isHtmlEmpty(videoState.create.form.deskripsi) && + embedLink !== null // Make sure the embed link is valid + ); + }; + const resetForm = () => { videoState.create.form = { name: '', @@ -38,6 +55,26 @@ function CreateVideo() { }; const handleSubmit = async () => { + if (!videoState.create.form.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (!link.trim()) { + toast.error('Link YouTube wajib diisi'); + return; + } + + if (isHtmlEmpty(videoState.create.form.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (!embedLink) { + toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); + return; + } + try { setIsSubmitting(true); if (!embedLink) { @@ -168,8 +205,11 @@ function CreateVideo() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx index 6ebb654a..3b3a6e81 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_penduduk_non_permanent/[id]/page.tsx @@ -35,6 +35,21 @@ function EditPelayananPendudukNonPermanent() { 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) + ); + }; + const [originalData, setOriginalData] = useState({ name: '', deskripsi: '', @@ -86,6 +101,16 @@ function EditPelayananPendudukNonPermanent() { }; const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + try { setIsSubmitting(true); if (!statePendudukNonPermanent.findById.data) return; @@ -173,8 +198,11 @@ function EditPelayananPendudukNonPermanent() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx index d354e203..6c10fc9f 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_perizinan_berusaha/[id]/page.tsx @@ -36,6 +36,22 @@ function EditPelayananPerizinanBerusaha() { }); 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) + ); + }; + const [originalData, setOriginalData] = useState({ id: '', name: '', @@ -102,6 +118,21 @@ function EditPelayananPerizinanBerusaha() { }; const handleSubmit = async () => { + if (!formData.name?.trim()) { + toast.error('Judul wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } + + if (!formData.link?.trim()) { + toast.error('Link wajib diisi'); + return; + } + try { setIsSubmitting(true); await state.update.update(formData); @@ -192,8 +223,11 @@ function EditPelayananPerizinanBerusaha() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx index 3e1adf6a..938488f2 100644 --- a/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/desa/layanan/pelayanan_surat_keterangan/[id]/edit/page.tsx @@ -149,6 +149,22 @@ function EditSuratKeterangan() { const [previewImage2, setPreviewImage2] = 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() !== '' && + !isHtmlEmpty(formData.deskripsi) && + (previewImage !== null || originalData.imageId !== '') // Either a new file is selected or an existing image exists + ); + }; + // 🧭 Load Initial Data useEffect(() => { const loadSurat = async () => { @@ -209,6 +225,21 @@ function EditSuratKeterangan() { // 💾 Submit Handler const handleSubmit = useCallback(async () => { + if (!formData.name?.trim()) { + toast.error('Nama surat keterangan wajib diisi'); + return; + } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Konten wajib diisi'); + return; + } + + if (!previewImage && !originalData.imageId) { + toast.error('Gambar konten pelayanan wajib dipilih'); + return; + } + try { setIsSubmitting(true); @@ -251,7 +282,7 @@ function EditSuratKeterangan() { } finally { setIsSubmitting(false); } - }, [formData, file, file2, router]); + }, [formData, file, file2, router, previewImage, originalData.imageId]); // 📝 Form Field Handlers const handleNameChange = (e: React.ChangeEvent) => { @@ -324,10 +355,10 @@ function EditSuratKeterangan() { {/* Action Buttons */} - diff --git a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx index 82d56817..f5dfff4d 100644 --- a/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-desa/[id]/maskot_desa/page.tsx @@ -34,6 +34,21 @@ function Page() { 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 () => { @@ -122,6 +137,12 @@ function Page() { toast.error("Judul wajib diisi"); return; } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error("Deskripsi wajib diisi"); + return; + } + try { setIsSubmitting(true); const uploadedImages = []; @@ -315,8 +336,11 @@ function Page() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 index f0b084eb..10f3105e 100644 --- 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 @@ -99,6 +99,21 @@ function Page() { 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 @@ -106,6 +121,11 @@ function Page() { toast.error('Judul wajib diisi'); return; } + + if (isHtmlEmpty(formData.deskripsi)) { + toast.error('Deskripsi wajib diisi'); + return; + } setIsSubmitting(true); @@ -260,8 +280,11 @@ function Page() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 index 62a45c04..7a743755 100644 --- 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 @@ -81,6 +81,21 @@ function Page() { }; }, [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); @@ -89,11 +104,16 @@ function Page() { // 💾 Submit const handleSubmit = async () => { - if (!formData.visi.trim()) { + 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; @@ -227,14 +247,16 @@ function Page() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} - loading={isSubmitting} > - Simpan + {isSubmitting ? : 'Simpan'} diff --git a/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx index d3b1cb21..e79103d6 100644 --- a/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx +++ b/src/app/admin/(dashboard)/desa/profil/profil-perbekel/page.tsx @@ -95,7 +95,7 @@ function Page() { fz={{ base: 'md', md: 'lg' }} lh={{ base: 1.4, md: 1.4 }} > - I.B. Surya Prabhawa Manuaba, S.H., M.H. + {perbekel.nama || "I.B. Surya Prabhawa Manuaba, S.H., M.H."} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx index 3cd8c1f6..b8d1e86e 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx @@ -51,6 +51,17 @@ function EditAPBDesa() { const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(true); + // 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 () => { @@ -213,8 +224,11 @@ function EditAPBDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 6ab98928..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 @@ -27,6 +27,17 @@ function CreateAPBDesa() { 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 = { tahun: 0, @@ -121,8 +132,11 @@ function CreateAPBDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx index 96bb9ebd..45e5937a 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx @@ -35,6 +35,15 @@ function EditBelanja() { value: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.value !== '' && + Number(formData.value) > 0 + ); + }; + // format angka ke rupiah const formatRupiah = (value: number | string) => { const number = @@ -172,8 +181,11 @@ function EditBelanja() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 013ffc24..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 @@ -24,6 +24,15 @@ function CreateBelanja() { 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 = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); @@ -126,8 +135,11 @@ function CreateBelanja() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 b0b0ed38..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 @@ -34,6 +34,15 @@ function EditPembiayaan() { 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' @@ -169,8 +178,11 @@ function EditPembiayaan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 56b71894..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 @@ -23,6 +23,15 @@ function CreatePembiayaan() { 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 = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); @@ -127,8 +136,11 @@ function CreatePembiayaan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 5b68ff7e..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 @@ -34,6 +34,15 @@ function EditPendapatan() { 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' @@ -176,8 +185,11 @@ function EditPendapatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 0c3ee514..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 @@ -22,6 +22,15 @@ function CreatePendapatan() { 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, '')); return new Intl.NumberFormat('id-ID', { @@ -122,8 +131,11 @@ function CreatePendapatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx index ef14160e..aebb8c18 100644 --- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/[id]/edit/page.tsx @@ -56,6 +56,14 @@ export default function EditPegawaiBumDes() { 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 ''; @@ -325,8 +333,11 @@ export default function EditPegawaiBumDes() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx index 5a8d1d1d..4999449a 100644 --- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/pegawai/create/page.tsx @@ -23,6 +23,15 @@ function CreatePegawaiBumDes() { 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 = { namaLengkap: "", @@ -284,8 +293,11 @@ function CreatePegawaiBumDes() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx index cb9f03ff..32496484 100644 --- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/[id]/page.tsx @@ -31,6 +31,23 @@ function EditPosisiOrganisasiBumDes() { 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 })); @@ -173,8 +190,11 @@ function EditPosisiOrganisasiBumDes() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx index 84aaabcf..32b88dd3 100644 --- a/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/Struktur-Organisasi-Dan-Sk-Pengurus-BumDes/posisi-organisasi/create/page.tsx @@ -15,6 +15,23 @@ function CreatePosisiOrganisasiBumDes() { 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(); }, []); @@ -115,8 +132,11 @@ function CreatePosisiOrganisasiBumDes() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 4ad28f5e..b5f5f37b 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/[id]/page.tsx @@ -41,6 +41,17 @@ export default function EditDemografiPekerjaan() { 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(() => { if (!id) return; @@ -186,8 +197,11 @@ export default function EditDemografiPekerjaan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 d8284367..ed5c84f1 100644 --- a/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/demografi-pekerjaan/create/page.tsx @@ -26,6 +26,17 @@ function CreateDemografiPekerjaan() { 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 = { pekerjaan: '', @@ -128,8 +139,11 @@ function CreateDemografiPekerjaan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 6dd1ff92..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 @@ -37,6 +37,16 @@ function EditJumlahPendudukMiskin() { 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; @@ -160,8 +170,11 @@ function EditJumlahPendudukMiskin() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 b26253a1..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 @@ -16,6 +16,16 @@ export default function CreateJumlahPendudukMiskin() { 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 = { year: new Date().getFullYear(), @@ -105,8 +115,11 @@ export default function CreateJumlahPendudukMiskin() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 bb0a39e6..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 @@ -33,6 +33,17 @@ function EditGrafikBerdasarkanPendidikan() { 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(() => { @@ -174,8 +185,11 @@ function EditGrafikBerdasarkanPendidikan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 73d89a1a..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 @@ -16,6 +16,17 @@ function CreateGrafikBerdasarkanPendidikan() { 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 = { ...stategrafik.create.form, @@ -127,8 +138,11 @@ function CreateGrafikBerdasarkanPendidikan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 51244c13..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 @@ -43,6 +43,16 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { 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) { @@ -179,8 +189,11 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 f31cf437..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 @@ -17,6 +17,16 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { 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 = { ...stategrafik.create.form, @@ -120,8 +130,11 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 4a3bf4f3..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 @@ -53,6 +53,19 @@ function EditDetailDataPengangguran() { 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) => { @@ -255,8 +268,11 @@ function EditDetailDataPengangguran() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 e7583359..3cd144f6 100644 --- a/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/jumlah-pengangguran/create/page.tsx @@ -27,6 +27,19 @@ function CreateJumlahPengangguran() { 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', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des' @@ -204,8 +217,11 @@ function CreateJumlahPengangguran() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 3fdad936..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 @@ -48,6 +48,27 @@ function EditLowonganKerja() { 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 () => { @@ -229,8 +250,11 @@ function EditLowonganKerja() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 dfbb955a..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 @@ -24,6 +24,27 @@ function CreateLowonganKerja() { 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 = { posisi: '', @@ -175,8 +196,11 @@ function CreateLowonganKerja() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx index 1b755f90..c180ad4a 100644 --- a/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/pasar-desa/kategori-produk/[id]/page.tsx @@ -28,6 +28,11 @@ function EditKategoriProduk() { const [formData, setFormData] = useState({ nama: '' }); const [originalData, setOriginalData] = useState({ nama: '' }); + // Check if form is valid + const isFormValid = () => { + return formData.nama?.trim() !== ''; + }; + useEffect(() => { const loadKategoriProduk = async () => { if (!id) return; @@ -146,8 +151,11 @@ function EditKategoriProduk() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 51ee8a3d..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 @@ -23,6 +23,11 @@ function CreateKategoriProduk() { 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(); }, []); @@ -101,8 +106,11 @@ function CreateKategoriProduk() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 f656a13f..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 @@ -68,6 +68,23 @@ function EditPasarDesa() { 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(); @@ -352,8 +369,11 @@ function EditPasarDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 0ece1759..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 @@ -32,6 +32,24 @@ export default function CreatePasarDesa() { 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(); }, []); @@ -265,8 +283,11 @@ export default function CreatePasarDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 f3cbf5bb..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 @@ -53,6 +53,24 @@ function EditProgramKemiskinan() { 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(() => { if (!id) return; @@ -235,8 +253,11 @@ function EditProgramKemiskinan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 674bb6a1..4d664ffb 100644 --- a/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/program-kemiskinan/create/page.tsx @@ -29,6 +29,25 @@ function CreateProgramKemiskinan() { 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: '', @@ -172,8 +191,11 @@ function CreateProgramKemiskinan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 592e7a35..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 @@ -41,6 +41,23 @@ function EditSektorUnggulanDesa() { 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) { @@ -168,8 +185,11 @@ function EditSektorUnggulanDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 f5beda9c..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 @@ -27,6 +27,23 @@ function CreateSektorUnggulanDesa() { 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 = { name: '', @@ -132,8 +149,11 @@ function CreateSektorUnggulanDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 350cbd38..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 @@ -44,6 +44,21 @@ function EditDigitalSmartVillage() { 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 () => { const id = params?.id as string; @@ -248,8 +263,11 @@ function EditDigitalSmartVillage() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 56426951..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 @@ -30,6 +30,22 @@ export default function CreateDesaDigital() { 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 = { name: '', @@ -227,8 +243,11 @@ export default function CreateDesaDigital() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx index e8f2d36c..241c96fa 100644 --- a/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/info-teknologi-tepat-guna/[id]/edit/page.tsx @@ -44,6 +44,21 @@ function EditInfoTeknologiTepatGuna() { 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(() => { const id = params?.id as string; @@ -260,8 +275,11 @@ function EditInfoTeknologiTepatGuna() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 035d6c0c..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 @@ -30,6 +30,22 @@ function CreateInfoTeknologiTepatGuna() { 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 = { name: '', @@ -202,8 +218,11 @@ function CreateInfoTeknologiTepatGuna() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 d07058ef..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 @@ -44,6 +44,23 @@ function EditKolaborasiInovasi() { 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 () => { @@ -199,8 +216,11 @@ function EditKolaborasiInovasi() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 c400f193..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 @@ -16,6 +16,22 @@ function CreateProgramKreatifDesa() { 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 = { name: "", @@ -135,8 +151,11 @@ function CreateProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 a14a850b..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 @@ -51,6 +51,11 @@ function EditMitraKolaborasi() { imageUrl: '', }); + // Check if form is valid + const isFormValid = () => { + return formData.name?.trim() !== ''; + }; + // Load data ke state lokal sekali saja useEffect(() => { const loadData = async () => { @@ -263,8 +268,11 @@ function EditMitraKolaborasi() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 c6c77ad2..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 @@ -29,6 +29,14 @@ function CreateMitraKolaborasi() { 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 = { name: '', @@ -181,8 +189,11 @@ function CreateMitraKolaborasi() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx index baaba7c0..ef76991f 100644 --- a/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/inovasi/program-kreatif-desa/[id]/edit/page.tsx @@ -51,6 +51,23 @@ function EditProgramKreatifDesa() { 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 () => { @@ -236,8 +253,11 @@ function EditProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 ee861f65..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 @@ -25,6 +25,23 @@ function CreateProgramKreatifDesa() { 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 = { name: "", @@ -127,8 +144,11 @@ function CreateProgramKreatifDesa() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 fdf71259..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 @@ -53,6 +53,21 @@ function EditKeamananLingkungan() { 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 () => { @@ -294,8 +309,11 @@ function EditKeamananLingkungan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 f1d148a6..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 @@ -35,6 +35,22 @@ function CreateKeamananLingkungan() { 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 = { name: '', @@ -237,8 +253,11 @@ function CreateKeamananLingkungan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx index 7bff7bad..3d4ff3b5 100644 --- a/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/kontak-darurat/kontak-darurat-item/[id]/edit/page.tsx @@ -39,6 +39,15 @@ function EditKontakItem() { icon: '', }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.nomorTelepon?.trim() !== '' && + formData.icon?.trim() !== '' + ); + }; + // Load data sekali dari global state useEffect(() => { const loadKontakDarurat = async () => { @@ -170,8 +179,11 @@ function EditKontakItem() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 c6789784..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 @@ -23,6 +23,16 @@ 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: '', @@ -115,8 +125,11 @@ function CreateKontakItem() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 80a8c6f1..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 @@ -46,6 +46,14 @@ export default function EditKontakDaruratKeamanan() { kategoriId: [], }); + // Check if form is valid + const isFormValid = () => { + return ( + formData.name?.trim() !== '' && + formData.kategoriId.length > 0 + ); + }; + // 🔁 Load data saat ID berubah useEffect(() => { const loadInitialData = async () => { @@ -213,8 +221,11 @@ export default function EditKontakDaruratKeamanan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 92c0a424..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 @@ -26,6 +26,14 @@ function CreateKontakDaruratKeamanan() { 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(); }, []); @@ -127,8 +135,11 @@ function CreateKontakDaruratKeamanan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 a079c8aa..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 @@ -55,6 +55,25 @@ function EditLaporanPublik() { 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; @@ -223,8 +242,11 @@ function EditLaporanPublik() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 4665545d..d086b10f 100644 --- a/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/laporan-publik/create/page.tsx @@ -26,6 +26,16 @@ function CreateLaporanPublik() { 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 = { judul: '', @@ -123,8 +133,11 @@ function CreateLaporanPublik() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} diff --git a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx index 9943477a..1a63549c 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/[id]/edit/page.tsx @@ -41,6 +41,23 @@ function EditPencegahanKriminalitas() { linkVideo: '', }); + // 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.deskripsiSingkat) && + !isHtmlEmpty(formData.deskripsi) && + convertYoutubeUrlToEmbed(formData.linkVideo) !== '' + ); + }; + // load data hanya sekali pas id berubah useEffect(() => { const loadKriminalitas = async () => { @@ -220,8 +237,11 @@ function EditPencegahanKriminalitas() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 ab5646c8..9e12f6d4 100644 --- a/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/pencegahan-kriminalitas/create/page.tsx @@ -28,6 +28,23 @@ function CreatePencegahanKriminalitas() { 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 = { judul: "", @@ -165,8 +182,11 @@ function CreatePencegahanKriminalitas() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 index 1c2b79d4..29f2a4fd 100644 --- 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 @@ -67,6 +67,17 @@ function EditPolsekTerdekat() { 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(); }, []); @@ -261,8 +272,11 @@ function EditPolsekTerdekat() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 index b3b63080..3f38a0d5 100644 --- 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 @@ -24,6 +24,17 @@ function CreatePolsekTerdekat() { 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(); }, []); @@ -219,8 +230,11 @@ function CreatePolsekTerdekat() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 index 04e64bd2..5eaa948d 100644 --- 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 @@ -33,6 +33,11 @@ function EditLayananPolsek() { nama: '', }); + // Check if form is valid + const isFormValid = () => { + return formData.nama?.trim() !== ''; + }; + useEffect(() => { const loadLayananPolsek = async () => { const id = params?.id as string; @@ -143,8 +148,11 @@ function EditLayananPolsek() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 index 378ba46e..67d551f4 100644 --- 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 @@ -22,6 +22,11 @@ function CreateLayananPolsek() { 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: '', @@ -93,8 +98,11 @@ function CreateLayananPolsek() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 48de4cf7..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 @@ -52,6 +52,21 @@ function EditTipsKeamanan() { 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 () => { @@ -295,8 +310,11 @@ function EditTipsKeamanan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 8ba626d6..675544b2 100644 --- a/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx +++ b/src/app/admin/(dashboard)/keamanan/tips-keamanan/create/page.tsx @@ -30,6 +30,22 @@ function CreateKeamananLingkungan() { 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 = { judul: '', @@ -199,8 +215,11 @@ function CreateKeamananLingkungan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 354169d8..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 @@ -30,6 +30,33 @@ function CreateArtikelKesehatan() { 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 = { title: '', @@ -65,10 +92,79 @@ 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) { + toast.error('Gambar wajib dipilih'); + return; + } + try { - if (!file) { - return toast.warn('Silakan pilih file gambar terlebih dahulu'); - } + setIsSubmitting(true); const res = await ApiFetch.api.fileStorage.create.post({ file, @@ -344,8 +440,11 @@ function CreateArtikelKesehatan() { onClick={handleSubmit} radius="md" size="md" + disabled={!isFormValid() || isSubmitting} style={{ - background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, + background: !isFormValid() || isSubmitting + ? `linear-gradient(135deg, #cccccc, #eeeeee)` + : `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, color: '#fff', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', }} 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 32db4e6b..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 @@ -45,6 +45,28 @@ function EditFasilitasKesehatan() { const params = useParams<{ id: string }>(); 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.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: '' }, @@ -111,6 +133,52 @@ function EditFasilitasKesehatan() { 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 { setIsSubmitting(true); @@ -264,8 +332,11 @@ function EditFasilitasKesehatan() { + Edit Daftar Informasi Publik @@ -128,10 +137,13 @@ function EditDaftarInformasiPublik() { diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx index 4f848982..fc26a831 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/administrasi-online/page.tsx @@ -24,6 +24,16 @@ function AdministrasiOnline() { const [opened, { open, close }] = useDisclosure(false); const state = useProxy(layananonlineDesa); + // Check if form is valid + const isFormValid = () => { + return ( + state.administrasiOnline.create.form.name?.trim() !== '' && + state.administrasiOnline.create.form.alamat?.trim() !== '' && + state.administrasiOnline.create.form.nomorTelepon?.trim() !== '' && + state.administrasiOnline.create.form.jenisLayananId?.trim() !== '' + ); + }; + useEffect(() => { // ✅ Panggil load data jenis layanan dari backend if (!state.jenisLayanan.findMany.data) { @@ -104,7 +114,11 @@ function AdministrasiOnline() { } /> - diff --git a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx index 6775bdb1..b45c6082 100644 --- a/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx +++ b/src/app/darmasaba/(pages)/inovasi/layanan-online-desa/pengaduan-masyarakat/page.tsx @@ -19,6 +19,28 @@ function PengaduanMasyarakat() { 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 ( + state.pengaduanMasyarakat.create.form.name?.trim() !== '' && + state.pengaduanMasyarakat.create.form.email?.trim() !== '' && + state.pengaduanMasyarakat.create.form.nomorTelepon?.trim() !== '' && + state.pengaduanMasyarakat.create.form.nik?.trim() !== '' && + state.pengaduanMasyarakat.create.form.judulPengaduan?.trim() !== '' && + state.pengaduanMasyarakat.create.form.lokasiKejadian?.trim() !== '' && + !isHtmlEmpty(state.pengaduanMasyarakat.create.form.deskripsiPengaduan) && + state.pengaduanMasyarakat.create.form.jenisPengaduanId?.trim() !== '' && + file !== null + ); + }; + useEffect(() => { // ✅ Panggil load data jenis layanan dari backend if (!state.jenisPengaduan.findMany.data) { @@ -207,7 +229,11 @@ function PengaduanMasyarakat() { - diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx index 42a22ec3..de174975 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/page.tsx @@ -37,6 +37,24 @@ function Page() { }; }; + // Check if form is valid + const isFormValid = () => { + return ( + beasiswaDesa.create.form.namaLengkap?.trim() !== '' && + beasiswaDesa.create.form.nis?.trim() !== '' && + beasiswaDesa.create.form.kelas?.trim() !== '' && + beasiswaDesa.create.form.jenisKelamin?.trim() !== '' && + beasiswaDesa.create.form.alamatDomisili?.trim() !== '' && + beasiswaDesa.create.form.tempatLahir?.trim() !== '' && + beasiswaDesa.create.form.tanggalLahir?.trim() !== '' && + beasiswaDesa.create.form.namaOrtu?.trim() !== '' && + beasiswaDesa.create.form.nik?.trim() !== '' && + beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' && + beasiswaDesa.create.form.penghasilan?.trim() !== '' && + beasiswaDesa.create.form.noHp?.trim() !== '' + ); + }; + const { data, page, totalPages, loading, load } = ungggulanDesa.findMany; useShallowEffect(() => { @@ -238,7 +256,7 @@ function Page() { onChange={(val) => { beasiswaDesa.create.form.noHp = val.target.value }} /> - + diff --git a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx index c408b5b1..6678c274 100644 --- a/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/beasiswa-desa/pelajari-lebih-lanjut/page.tsx @@ -46,6 +46,24 @@ export default function BeasiswaPage() { }; }; + // Check if form is valid + const isFormValid = () => { + return ( + beasiswaDesa.create.form.namaLengkap?.trim() !== '' && + beasiswaDesa.create.form.nis?.trim() !== '' && + beasiswaDesa.create.form.kelas?.trim() !== '' && + beasiswaDesa.create.form.jenisKelamin?.trim() !== '' && + beasiswaDesa.create.form.alamatDomisili?.trim() !== '' && + beasiswaDesa.create.form.tempatLahir?.trim() !== '' && + beasiswaDesa.create.form.tanggalLahir?.trim() !== '' && + beasiswaDesa.create.form.namaOrtu?.trim() !== '' && + beasiswaDesa.create.form.nik?.trim() !== '' && + beasiswaDesa.create.form.pekerjaanOrtu?.trim() !== '' && + beasiswaDesa.create.form.penghasilan?.trim() !== '' && + beasiswaDesa.create.form.noHp?.trim() !== '' + ); + }; + const handleSubmit = async () => { await beasiswaDesa.create.create(); resetForm(); @@ -391,6 +409,7 @@ export default function BeasiswaPage() { radius="xl" bg={colors['blue-button']} onClick={handleSubmit} + disabled={!isFormValid()} style={{ fontSize: '0.9375rem', fontWeight: 600, lineHeight: 1.4 }} > Kirim diff --git a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx index e137e07a..368d19ca 100644 --- a/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx +++ b/src/app/darmasaba/(pages)/pendidikan/perpustakaan-digital/_lib/modalPeminjaman.tsx @@ -42,6 +42,24 @@ export default function ModalPeminjaman({ const BATAS_HARI_PINJAM = 4; + // 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 ( + snap.create.form.nama?.trim() !== '' && + snap.create.form.noTelp?.trim() !== '' && + snap.create.form.alamat?.trim() !== '' && + snap.create.form.tanggalPinjam?.trim() !== '' && + !isHtmlEmpty(snap.create.form.catatan) + ); + }; + // Reset form setiap modal dibuka useEffect(() => { if (opened && buku) { @@ -222,13 +240,13 @@ export default function ModalPeminjaman({ diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx index f5d12f86..26aaad1b 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-informasi-publik/page.tsx @@ -54,6 +54,28 @@ function Page() { const permohonanInformasiPublikState = useProxy(statePermohonanInformasi); const router = useRouter(); + // Helper function to validate email format + const isValidEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Check if form is valid + const isFormValid = () => { + const form = permohonanInformasiPublikState.statepermohonanInformasiPublik.create.form; + return ( + form.name?.trim() !== '' && + form.nik?.trim() !== '' && + form.notelp?.trim() !== '' && + form.alamat?.trim() !== '' && + form.email?.trim() !== '' && + isValidEmail(form.email) && + form.jenisInformasiDimintaId && + form.caraMemperolehInformasiId && + form.caraMemperolehSalinanInformasiId + ); + }; + const submitForms = async () => { const { create } = permohonanInformasiPublikState.statepermohonanInformasiPublik; const hasil = await create.create(); @@ -266,6 +288,7 @@ function Page() { bg={colors['blue-button']} leftSection={} onClick={submitForms} + disabled={!isFormValid()} > Kirim Permohonan diff --git a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx index 80de7c2e..ad60d688 100644 --- a/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/permohonan-keberatan-informasi-publik/page.tsx @@ -56,6 +56,24 @@ function Page() { const stateKeberatan = useProxy(permohonanKeberatanInformasi); const router = useRouter(); + // Helper function to validate email format + const isValidEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Check if form is valid + const isFormValid = () => { + const form = stateKeberatan.create.form; + return ( + form.name?.trim() !== '' && + form.email?.trim() !== '' && + isValidEmail(form.email) && + form.notelp?.trim() !== '' && + form.alasan?.trim() !== '' + ); + }; + const submit = async () => { const hasil = await stateKeberatan.create.create(); if (hasil) router.push('/darmasaba/permohonan/berhasil'); @@ -232,6 +250,7 @@ function Page() { radius="md" fw={600} bg={colors['blue-button']} + disabled={!isFormValid()} > Kirim Permohonan diff --git a/src/app/darmasaba/_com/NavbarMainMenu.tsx b/src/app/darmasaba/_com/NavbarMainMenu.tsx index 79b200c1..11b88ffe 100644 --- a/src/app/darmasaba/_com/NavbarMainMenu.tsx +++ b/src/app/darmasaba/_com/NavbarMainMenu.tsx @@ -13,7 +13,7 @@ import { NavbarSubMenu } from "./NavbarSubMenu" import { authStore } from "@/store/authStore"; // contoh state auth (dummy aja dulu, bisa diganti sesuai sistem auth kamu) -const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4; +const isAdmin = authStore.user?.roleId === 0 || authStore.user?.roleId === 1 || authStore.user?.roleId === 2 || authStore.user?.roleId === 3 || authStore.user?.roleId === 4; export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { const { item, isSearch } = useSnapshot(stateNav) @@ -46,11 +46,11 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { {listNavbar.map((item, k) => ( - child.href && pathname.startsWith(child.href)))} + child.href && pathname.startsWith(child.href)))} /> ))} @@ -73,7 +73,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) { { - next.push("/admin/landing-page/profil/program-inovasi") + next.push("/admin/landing-page/profile/program-inovasi") }} color={colors["blue-button"]} radius="xl" diff --git a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx index 695f0572..f13710e5 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/ModuleView.tsx @@ -10,8 +10,7 @@ import { SimpleGrid, Skeleton, Stack, - Text, - useMantineColorScheme + Text } from "@mantine/core"; import { useShallowEffect } from "@mantine/hooks"; import { Prisma } from "@prisma/client"; @@ -24,8 +23,6 @@ type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: tr function ModuleItem({ data }: { data: ProgramInovasiItem }) { const router = useTransitionRouter(); - const { colorScheme } = useMantineColorScheme(); - const isDark = colorScheme === "dark"; return ( @@ -37,7 +34,7 @@ function ModuleItem({ data }: { data: ProgramInovasiItem }) { role="button" tabIndex={0} className="cursor-pointer transition-all" - bg={isDark ? "dark.6" : "white"} + bg="white" >
{data.image?.link ? ( diff --git a/src/app/darmasaba/layout.tsx b/src/app/darmasaba/layout.tsx index afe980de..7be59fe4 100644 --- a/src/app/darmasaba/layout.tsx +++ b/src/app/darmasaba/layout.tsx @@ -1,3 +1,5 @@ +"use client"; + import colors from "@/con/colors"; import { Box, Space, Stack } from "@mantine/core"; @@ -5,21 +7,20 @@ import { Navbar } from "@/app/darmasaba/_com/Navbar"; import Footer from "./_com/Footer"; - export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - - - {children} - -