docs: add AI collaboration contract and fix KegiatanCard image handling - bump to 0.1.48
This commit is contained in:
321
AI-CONTRACT.md
Normal file
321
AI-CONTRACT.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# AI-CONTRACT.md
|
||||||
|
|
||||||
|
Kontrak kerja antara **manusia (developer)** dan **AI assistant** (Claude Code,
|
||||||
|
Cursor, Copilot, atau agent coding lainnya) di repo ini. Tujuannya satu:
|
||||||
|
mencegah perbaikan 1 bug berubah jadi 3 bug baru (bug eksponensial). AI
|
||||||
|
**wajib** baca file ini sebelum menulis/menghapus kode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Prinsip Dasar
|
||||||
|
|
||||||
|
1. **Minimal diff, maximal pemahaman.** Baca kode sebelum ubah. Jangan
|
||||||
|
refactor yang tidak diminta. Jangan "rapikan" kode di sekitar bug.
|
||||||
|
2. **Fix akar, bukan gejala.** Kalau error muncul di layer A tapi penyebab
|
||||||
|
di layer B, perbaiki B. Jangan tambal di A.
|
||||||
|
3. **Satu masalah = satu perubahan logis.** Jangan campur fix bug dengan
|
||||||
|
refactor, rename, atau fitur baru dalam satu sesi tanpa izin.
|
||||||
|
4. **Tidak ada asumsi diam-diam.** Kalau butuh info (nama field, endpoint,
|
||||||
|
flow, schema), tanya atau baca kode — jangan tebak.
|
||||||
|
5. **Setiap perubahan harus reversible.** Diff kecil, commit jelas, bisa
|
||||||
|
di-revert tanpa efek samping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Sebelum Menulis Kode
|
||||||
|
|
||||||
|
Checklist wajib sebelum edit file:
|
||||||
|
|
||||||
|
- [ ] Sudah baca file target (bukan cuma potongan)
|
||||||
|
- [ ] Tahu siapa yang memanggil fungsi/komponen yang akan diubah
|
||||||
|
- [ ] Tahu apakah ada test/konsumer lain yang bergantung padanya
|
||||||
|
- [ ] Tahu layer yang benar (route / controller / component / hook /
|
||||||
|
service / repository / lib / util — sesuai arsitektur project)
|
||||||
|
- [ ] Cek dokumen panduan project (mis. `CLAUDE.md`, `CONTRIBUTING.md`,
|
||||||
|
`ARCHITECTURE.md`, ADR) untuk aturan spesifik
|
||||||
|
- [ ] Kalau ubah tipe/kontrak (API, function signature, schema), cek
|
||||||
|
semua pemakai
|
||||||
|
|
||||||
|
Jika salah satu tidak jelas: **berhenti, baca lagi, atau tanya user.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Saat Fix Bug
|
||||||
|
|
||||||
|
1. **Reproduksi dulu di kepala.** Jelaskan (minimal ke diri sendiri)
|
||||||
|
kenapa bug terjadi sebelum menyentuh kode.
|
||||||
|
2. **Temukan akar sebenarnya.** "Karena field X `undefined`/`null`/empty"
|
||||||
|
bukan akar — akarnya kenapa X bisa kosong.
|
||||||
|
3. **Perbaiki sekecil mungkin.** Kalau cukup 3 baris, jangan ubah 30.
|
||||||
|
4. **Jangan tambah try/catch hanya untuk menyembunyikan error** — itu
|
||||||
|
melahirkan bug baru yang lebih sulit dilacak.
|
||||||
|
5. **Jangan tambah fallback/default value spekulatif.** Kalau field
|
||||||
|
seharusnya selalu ada, perbaiki kenapa bisa kosong.
|
||||||
|
6. **Jangan rename, reorder, atau reformat** di file yang sama kecuali
|
||||||
|
langsung terkait fix.
|
||||||
|
7. **Setelah fix, verifikasi**: minimal jalankan typecheck/lint sesuai
|
||||||
|
tooling project (mis. `tsc`, `eslint`, `ruff`, `mypy`, `cargo check`,
|
||||||
|
`go vet`, `rspec`, dll). Idealnya jalankan test suite yang relevan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Yang Dilarang (Akar Bug Eksponensial)
|
||||||
|
|
||||||
|
- ❌ **Silent catch**: `catch (e) {}`, `except: pass`, `_ = err`, atau
|
||||||
|
pola serupa — tanpa alasan yang didokumentasi di komentar.
|
||||||
|
- ❌ **Comment-out kode** sebagai "backup". Hapus atau kembalikan, jangan
|
||||||
|
biarkan mayat — git sudah jadi backup.
|
||||||
|
- ❌ **Copy-paste antar file**. Extract ke shared module/util/helper.
|
||||||
|
- ❌ **Duplikasi util/helper/hook/service** yang sudah ada — cek dulu
|
||||||
|
sebelum bikin baru.
|
||||||
|
- ❌ **Tambah flag/opsi/parameter baru** hanya untuk menghindari break
|
||||||
|
konsumer lama — fix konsumernya sekalian.
|
||||||
|
- ❌ **Destructive git command** (`reset --hard`, `push --force`,
|
||||||
|
`branch -D`, `clean -fdx`) tanpa instruksi eksplisit.
|
||||||
|
- ❌ **Skip hook** (`--no-verify`, `--no-gpg-sign`) tanpa izin.
|
||||||
|
- ❌ **Ubah schema/migrasi database** tanpa migration file yang sesuai.
|
||||||
|
- ❌ **Tambah dependency baru** tanpa izin user.
|
||||||
|
- ❌ **Hardcode credential, secret, URL produksi, atau data user**.
|
||||||
|
- ❌ **Ubah konfigurasi CI/CD, environment, atau infra** tanpa diskusi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Saat Menambah Fitur
|
||||||
|
|
||||||
|
- Baca panduan arsitektur project sebelum mulai.
|
||||||
|
- Tentukan layer sebelum menulis. Jangan taruh bisnis logika di route,
|
||||||
|
controller, atau komponen presentasi.
|
||||||
|
- Jangan buat abstraksi untuk kebutuhan hipotetis. Tulis kode yang
|
||||||
|
diminta sekarang (YAGNI — *You Aren't Gonna Need It*).
|
||||||
|
- Hormati batas ukuran file yang sudah disepakati di project. Kalau
|
||||||
|
belum ada, gunakan rule of thumb: file >500 baris = sinyal untuk
|
||||||
|
pisah; fungsi >50 baris = sinyal untuk extract.
|
||||||
|
- Ikuti konvensi naming, struktur folder, dan pattern yang sudah ada —
|
||||||
|
konsistensi lebih penting dari preferensi pribadi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Saat Ragu
|
||||||
|
|
||||||
|
Urutan tindakan:
|
||||||
|
|
||||||
|
1. Baca kode terkait lebih dalam.
|
||||||
|
2. Cek dokumen panduan project (`CLAUDE.md`, `README.md`, ADR, dll).
|
||||||
|
3. Cek git history (`git log -p`, `git blame`) kalau pertanyaannya soal
|
||||||
|
"kenapa ini begini".
|
||||||
|
4. **Tanya user** — lebih baik tanya 1 pertanyaan daripada menulis 100
|
||||||
|
baris yang harus dibuang.
|
||||||
|
|
||||||
|
Jangan pernah "pokoknya coba dulu, kalau salah revert". Revert itu murah
|
||||||
|
di local, tapi mahal kalau sudah merusak state (DB, session, file
|
||||||
|
sistem, deployment, dll).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Saat Selesai
|
||||||
|
|
||||||
|
- Jelaskan perubahan **secara singkat**: apa, di mana (file:line), kenapa.
|
||||||
|
- Sebutkan efek samping kalau ada (perubahan kontrak, breaking change,
|
||||||
|
perlu migrasi, perlu restart service, dll).
|
||||||
|
- Jangan ringkas diff yang user sudah lihat — user baca kode langsung.
|
||||||
|
- Kalau project punya channel notifikasi atau workflow report
|
||||||
|
(Slack/Discord/Telegram/email), kirim sesuai konvensi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Eskalasi
|
||||||
|
|
||||||
|
Hentikan pekerjaan dan tanya user kalau:
|
||||||
|
|
||||||
|
- Fix butuh ubah >5 file untuk bug yang kelihatannya kecil.
|
||||||
|
- Ketemu bug lain di tengah jalan yang tidak diminta.
|
||||||
|
- Perubahan berpotensi mengenai data produksi, session aktif, atau
|
||||||
|
user nyata.
|
||||||
|
- User memberi instruksi yang bertentangan dengan dokumen panduan
|
||||||
|
project — konfirmasi dulu sebelum melanggar aturan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Tools sebagai Mata dan Tangan AI
|
||||||
|
|
||||||
|
AI **wajib** memakai tools yang tersedia (MCP server, CLI commands,
|
||||||
|
debugger, browser automation, log inspector, DB query tool, dll) sebagai
|
||||||
|
**mata dan tangan**-nya.
|
||||||
|
|
||||||
|
- **Mata**: sebelum menebak state sistem, AI harus lihat langsung. Cek
|
||||||
|
log, query DB read-only, baca file config, jalankan health check, atau
|
||||||
|
pakai tool inspeksi yang relevan. Jangan berasumsi tentang data,
|
||||||
|
konfigurasi, atau tampilan — **cek dulu**.
|
||||||
|
- **Tangan**: gunakan tools untuk verifikasi end-to-end setelah
|
||||||
|
perubahan. Contoh: setelah fix UI, jalankan/preview halamannya dan
|
||||||
|
pastikan render + console bersih. Setelah fix logic, jalankan test
|
||||||
|
atau panggil endpoint yang relevan.
|
||||||
|
- **Maksimalkan pemakaian.** Kalau ada tool yang relevan, pakai — jangan
|
||||||
|
memilih jalan manual yang lebih rapuh. Semakin sering tools dipakai
|
||||||
|
untuk verifikasi, semakin solid project ini.
|
||||||
|
- **Ajukan tool baru kalau perlu.** Kalau AI merasa butuh tool yang
|
||||||
|
belum ada, AI **boleh dan didorong** untuk mengajukan pembuatannya
|
||||||
|
ke user. Format pengajuan:
|
||||||
|
1. Nama tool + signature (input/output)
|
||||||
|
2. Kenapa dibutuhkan (masalah konkret yang sedang dihadapi)
|
||||||
|
3. Sumber data (tabel DB / cache key / endpoint / file system)
|
||||||
|
4. Estimasi dampak ke kualitas investigasi/perbaikan
|
||||||
|
- **Jangan buat tool baru tanpa izin.** Ajukan dulu, tunggu persetujuan
|
||||||
|
user, baru implementasi (+ update dokumentasi).
|
||||||
|
- **Tools adalah sumber kebenaran runtime.** Kalau memory/log mengatakan
|
||||||
|
X tapi tool inspeksi langsung mengatakan Y, percayai tool.
|
||||||
|
|
||||||
|
Tujuan: AI tidak buta terhadap state sistem nyata, dan setiap perbaikan
|
||||||
|
diverifikasi secara nyata — bukan "harusnya sudah jalan".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Kontrak Public API / Interface (Wajib Dijaga)
|
||||||
|
|
||||||
|
Setiap interface yang dipakai oleh konsumen eksternal — REST/GraphQL
|
||||||
|
endpoint, MCP tool, library export, CLI command, webhook payload, event
|
||||||
|
schema, dll — adalah **kontrak publik**. Begitu konsumen (termasuk AI
|
||||||
|
agent dengan memory) tahu bentuk kontraknya, perubahan diam-diam bisa
|
||||||
|
bikin mereka bertindak berdasarkan asumsi yang sudah tidak valid — dan
|
||||||
|
kamu **tidak akan tahu** sampai terjadi kejadian aneh di prod.
|
||||||
|
|
||||||
|
### Apa yang dianggap kontrak (freeze)
|
||||||
|
|
||||||
|
| Kategori | Contoh | Aturan |
|
||||||
|
| ----------------- | --------------------------------------- | -------------------------------------------------- |
|
||||||
|
| Nama interface | endpoint path, tool name, function name | Tidak boleh rename tanpa bump versi |
|
||||||
|
| Parameter input | nama field, tipe, posisi | Nama & tipe tidak boleh berubah |
|
||||||
|
| Required flag | field wajib | Tidak boleh naik (optional → required) tanpa versi |
|
||||||
|
| Enum values | nilai yang valid | Tidak boleh dihapus/diganti |
|
||||||
|
| Error mode | format error response, exception type | Pola error harus konsisten |
|
||||||
|
| Field output | bentuk response | Tidak boleh dihapus/diganti tipenya |
|
||||||
|
|
||||||
|
### Apa yang boleh berubah (additive)
|
||||||
|
|
||||||
|
- Tambah interface baru
|
||||||
|
- Tambah parameter **optional** baru
|
||||||
|
- Tambah field output baru (konsumen lama akan mengabaikan yang tidak
|
||||||
|
mereka tahu, asal parsing-nya tolerant)
|
||||||
|
- Perbaiki pesan error (tanpa ubah polanya)
|
||||||
|
- Refactor implementasi internal (query, helper, dll)
|
||||||
|
|
||||||
|
### Cara kerja penjaga kontrak
|
||||||
|
|
||||||
|
1. **Contract test**: snapshot bentuk kontrak (nama, required,
|
||||||
|
properties, enum) untuk setiap interface publik. Letakkan di folder
|
||||||
|
khusus mis. `tests/contract/`.
|
||||||
|
2. **Kalau contract test merah karena perubahan yang disengaja**:
|
||||||
|
1. Update dokumentasi kontrak
|
||||||
|
2. Bump versi (semver, tag, atau version field)
|
||||||
|
3. Update snapshot di contract test
|
||||||
|
4. Jelaskan migrasinya di commit message + changelog
|
||||||
|
3. **Kalau contract test merah karena refactor yang tidak disengaja**:
|
||||||
|
**Jangan update snapshot untuk menghijaukan test.** Balikkan refactor
|
||||||
|
atau perbaiki supaya kontrak tetap sama. Snapshot bukan sampah yang
|
||||||
|
bisa di-regenerate seenaknya — dia alarm kebakaran.
|
||||||
|
|
||||||
|
### Larangan spesifik
|
||||||
|
|
||||||
|
- ❌ **Jangan rename** interface publik tanpa migration plan + bump versi
|
||||||
|
- ❌ **Jangan hapus enum value** — konsumen bisa punya kode/memory yang
|
||||||
|
memanggil nilai itu
|
||||||
|
- ❌ **Jangan naikkan param dari optional → required** tanpa bump versi
|
||||||
|
- ❌ **Jangan ubah bentuk error** (format response ↔ throw exception) —
|
||||||
|
ini mengubah handler logic di sisi konsumen
|
||||||
|
- ❌ **Jangan update snapshot contract test** tanpa update dokumentasi
|
||||||
|
|
||||||
|
### Apa yang BUKAN tugas contract test
|
||||||
|
|
||||||
|
- Memverifikasi logika bisnis (itu unit test biasa)
|
||||||
|
- Memverifikasi integrasi DB/external service (itu integration test)
|
||||||
|
- Memastikan data yang di-return benar (itu QA / staging)
|
||||||
|
|
||||||
|
Contract test **hanya** menjaga bentuk kontrak — cepat, deterministic,
|
||||||
|
tanpa dependency eksternal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Hygiene Dokumen Panduan AI
|
||||||
|
|
||||||
|
Dokumen panduan AI (`CLAUDE.md`, `AI-CONTRACT.md`, `.cursorrules`,
|
||||||
|
`.github/copilot-instructions.md`, dll) di-load **setiap turn** percakapan.
|
||||||
|
Semakin gemuk file utamanya, semakin banyak token terbuang setiap turn —
|
||||||
|
dan ironisnya, AI jadi lebih sulit menemukan info penting karena tertimbun
|
||||||
|
detail. File panduan yang gemuk **bukan** tanda dokumentasi yang baik;
|
||||||
|
sering justru sebaliknya.
|
||||||
|
|
||||||
|
### Pecah, jangan tumpuk
|
||||||
|
|
||||||
|
Gunakan **referensi file** alih-alih menumpuk semua di satu file. Banyak
|
||||||
|
AI agent (termasuk Claude Code) auto-load file yang di-reference dengan
|
||||||
|
sintaks `@path/to/file.md`. Contoh struktur `CLAUDE.md` yang sehat:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
## Architecture
|
||||||
|
See @docs/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## Agent Specs
|
||||||
|
See @docs/AGENTIC_OVERVIEW.md
|
||||||
|
|
||||||
|
## ADR History
|
||||||
|
See @docs/adr/README.md
|
||||||
|
````
|
||||||
|
|
||||||
|
`CLAUDE.md` utama tetap ramping, tapi info detail tetap accessible saat
|
||||||
|
dibutuhkan.
|
||||||
|
|
||||||
|
### Apa yang WAJIB tetap di CLAUDE.md (load setiap turn)
|
||||||
|
|
||||||
|
- Konvensi coding inti (naming, formatting, import order)
|
||||||
|
- Perintah build/test/lint yang sering dipakai
|
||||||
|
- Aturan komunikasi (bahasa, gaya, format response)
|
||||||
|
- Struktur folder high-level (1-2 level)
|
||||||
|
- Larangan absolut (jangan commit ke main, jangan touch folder X, dll)
|
||||||
|
- **Pointer** (`@path/...`) ke file detail lainnya
|
||||||
|
|
||||||
|
### Apa yang DIPINDAH ke file terpisah
|
||||||
|
|
||||||
|
- Spec arsitektur lengkap → `docs/ARCHITECTURE.md`
|
||||||
|
- Detail flow / sequence diagram → `docs/flows/*.md`
|
||||||
|
- ADR history (Architecture Decision Records) → `docs/adr/`
|
||||||
|
- Contoh kode panjang → `docs/examples/`
|
||||||
|
- API/interface reference lengkap → `docs/api/`
|
||||||
|
- Onboarding & setup detail → `docs/SETUP.md`
|
||||||
|
- Glossary domain terminology → `docs/GLOSSARY.md`
|
||||||
|
- Catatan investigasi/post-mortem → `docs/incidents/`
|
||||||
|
|
||||||
|
Lokasi alternatif: `.claude/` atau `.ai/` kalau tim ingin memisahkan
|
||||||
|
khusus untuk AI tooling, di luar dokumentasi developer biasa.
|
||||||
|
|
||||||
|
### Cek duplikasi secara rutin
|
||||||
|
|
||||||
|
Info yang sama sering muncul di beberapa section seiring waktu — biasanya
|
||||||
|
karena ditambahkan saat debugging tanpa cek file dulu. Audit berkala:
|
||||||
|
|
||||||
|
- [ ] Sama-sama dijelaskan di `README.md` dan `CLAUDE.md`? Pilih satu,
|
||||||
|
yang lain referensikan.
|
||||||
|
- [ ] Aturan yang sama disebut di 2-3 section? Konsolidasi ke satu section
|
||||||
|
kanonik, section lain tinggal pointer.
|
||||||
|
- [ ] Contoh kode panjang muncul inline? Pindah ke `docs/examples/`.
|
||||||
|
- [ ] Konvensi yang sudah jadi default di linter/formatter masih ditulis
|
||||||
|
manual? Hapus — biarkan tooling yang jaga, dokumen tidak perlu
|
||||||
|
mengulang.
|
||||||
|
- [ ] Info yang sudah usang (refer ke file/fitur yang dihapus)? Bersihkan
|
||||||
|
— dokumen yang setengah benar lebih merusak daripada tidak ada.
|
||||||
|
|
||||||
|
### Rule of thumb ukuran
|
||||||
|
|
||||||
|
Kalau `CLAUDE.md` (atau equivalent) sudah > 300 baris, itu **sinyal kuat**
|
||||||
|
untuk pecah file. Dokumen panduan AI yang ideal: cukup pendek untuk dibaca
|
||||||
|
ulang dalam 1 menit oleh manusia, dengan pointer ke detail untuk AI yang
|
||||||
|
butuh konteks lebih dalam.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Aturan Emas
|
||||||
|
|
||||||
|
> **Lebih baik tidak melakukan apa-apa daripada memperburuk kode.**
|
||||||
|
>
|
||||||
|
> Kalau setelah 2 kali percobaan fix masih memunculkan bug baru, **stop**.
|
||||||
|
> Laporkan ke user, jelaskan apa yang sudah dicoba dan kenapa gagal.
|
||||||
|
> Jangan tambal terus — itu cara bug beranak eksponensial.
|
||||||
@@ -30,6 +30,7 @@ bun eslint . --fix
|
|||||||
- Architecture, request flow, domain modules, key files: @.claude/ARCHITECTURE.md
|
- Architecture, request flow, domain modules, key files: @.claude/ARCHITECTURE.md
|
||||||
- Database conventions, auth flow, file handling: @.claude/DATABASE.md
|
- Database conventions, auth flow, file handling: @.claude/DATABASE.md
|
||||||
- Env vars, Docker, CI/CD, releasing: @.claude/DEPLOYMENT.md
|
- Env vars, Docker, CI/CD, releasing: @.claude/DEPLOYMENT.md
|
||||||
|
- AI collaboration contract, rules, and guidelines: @AI-CONTRACT.md
|
||||||
|
|
||||||
### Workflow for Code Changes
|
### Workflow for Code Changes
|
||||||
1. **Commit** existing changes before starting new work
|
1. **Commit** existing changes before starting new work
|
||||||
|
|||||||
19
MIND/PLAN/ai-collaboration-contract.md
Normal file
19
MIND/PLAN/ai-collaboration-contract.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Plan: Add AI Collaboration Contract and Fix UI Issues
|
||||||
|
|
||||||
|
## Background
|
||||||
|
- Need a clear contract for AI collaboration to prevent "exponential bugs".
|
||||||
|
- UI fix for `KegiatanCard` to handle missing images gracefully.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
- Add `AI-CONTRACT.md` with guidelines.
|
||||||
|
- Link `AI-CONTRACT.md` in `CLAUDE.md`.
|
||||||
|
- Fix `KegiatanCard.tsx` image rendering.
|
||||||
|
- Bump version to 0.1.48.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
1. Create `AI-CONTRACT.md`.
|
||||||
|
2. Update `CLAUDE.md`.
|
||||||
|
3. Update `KegiatanCard.tsx`.
|
||||||
|
4. Bump version in `package.json`.
|
||||||
|
5. Verify build.
|
||||||
|
6. Commit and push.
|
||||||
11
MIND/PLAN/task-ai-collaboration-contract.md
Normal file
11
MIND/PLAN/task-ai-collaboration-contract.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Task: Add AI Collaboration Contract and Fix UI Issues
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [x] Create `AI-CONTRACT.md`
|
||||||
|
- [x] Update `CLAUDE.md` to reference the contract
|
||||||
|
- [x] Fix `KegiatanCard.tsx` image rendering logic
|
||||||
|
- [x] Bump version in `package.json` to 0.1.48
|
||||||
|
- [x] Verify build successful
|
||||||
|
- [ ] Commit changes
|
||||||
|
- [ ] Create branch and push
|
||||||
|
- [ ] Merge to `stg`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "desa-darmasaba",
|
"name": "desa-darmasaba",
|
||||||
"version": "0.1.47",
|
"version": "0.1.48",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
153
src/app/admin/(dashboard)/_state/desa/profile/lambangDesa.ts
Normal file
153
src/app/admin/(dashboard)/_state/desa/profile/lambangDesa.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
const lambangDesaForm = z.object({
|
||||||
|
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||||
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const lambangDesaDefaultForm = {
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type LambangDesaForm = Prisma.LambangDesaGetPayload<{
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
judul: true;
|
||||||
|
deskripsi: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const lambangDesa = proxy({
|
||||||
|
findUnique: {
|
||||||
|
data: null as LambangDesaForm | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/lambang/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengambil data lambang desa"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
console.error("Load lambang desa error:", msg);
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data lambang desa");
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...lambangDesaDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(lambangDesaData: LambangDesaForm) {
|
||||||
|
this.id = lambangDesaData.id;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.form = {
|
||||||
|
judul: lambangDesaData.judul || "",
|
||||||
|
deskripsi: lambangDesaData.deskripsi || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateField(field: keyof typeof lambangDesaDefaultForm, value: string) {
|
||||||
|
this.form[field] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const validation = lambangDesaForm.safeParse(this.form);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
const errors = validation.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
toast.error(`Form tidak valid: ${errors}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/lambang/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
toast.success("Berhasil update lambang desa");
|
||||||
|
await lambangDesa.findUnique.load(this.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update lambang desa");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
this.error = errorMessage;
|
||||||
|
console.error("Update lambang desa error:", errorMessage);
|
||||||
|
toast.error("Terjadi kesalahan saat update lambang desa");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...lambangDesaDefaultForm };
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default lambangDesa;
|
||||||
241
src/app/admin/(dashboard)/_state/desa/profile/mantanPerbekel.ts
Normal file
241
src/app/admin/(dashboard)/_state/desa/profile/mantanPerbekel.ts
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
|
|
||||||
|
const mantanPerbekelForm = z.object({
|
||||||
|
nama: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
|
daerah: z.string().min(3, "Daerah minimal 3 karakter"),
|
||||||
|
periode: z.string().min(3, "Periode minimal 3 karakter"),
|
||||||
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mantanPerbekelDefaultForm = {
|
||||||
|
nama: "",
|
||||||
|
daerah: "",
|
||||||
|
periode: "",
|
||||||
|
imageId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mantanPerbekel = proxy({
|
||||||
|
create: {
|
||||||
|
form: { ...mantanPerbekelDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
async create() {
|
||||||
|
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.create.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
return toast.error(err);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mantanPerbekel.create.loading = true;
|
||||||
|
const res = await ApiFetch.api.desa.mantanperbekel["create"].post(
|
||||||
|
mantanPerbekel.create.form
|
||||||
|
);
|
||||||
|
if (res.status === 200) {
|
||||||
|
mantanPerbekel.findMany.load();
|
||||||
|
return toast.success("Foto berhasil disimpan!");
|
||||||
|
}
|
||||||
|
return toast.error("Gagal menyimpan foto");
|
||||||
|
} catch (error) {
|
||||||
|
console.log((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
mantanPerbekel.create.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
mantanPerbekel.create.form = { ...mantanPerbekelDefaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findMany: {
|
||||||
|
data: null as
|
||||||
|
| Prisma.PerbekelDariMasaKeMasaGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}>[]
|
||||||
|
| null,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
loading: false,
|
||||||
|
search: "",
|
||||||
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
mantanPerbekel.findMany.loading = true;
|
||||||
|
mantanPerbekel.findMany.page = page;
|
||||||
|
mantanPerbekel.findMany.search = search;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const query: any = { page, limit };
|
||||||
|
if (search) query.search = search;
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.desa.mantanperbekel["findMany"].get({
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
mantanPerbekel.findMany.data = res.data.data ?? [];
|
||||||
|
mantanPerbekel.findMany.totalPages = res.data.totalPages ?? 1;
|
||||||
|
} else {
|
||||||
|
mantanPerbekel.findMany.data = [];
|
||||||
|
mantanPerbekel.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal fetch mantan perbekel paginated:", err);
|
||||||
|
mantanPerbekel.findMany.data = [];
|
||||||
|
mantanPerbekel.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
mantanPerbekel.findMany.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findUnique: {
|
||||||
|
data: null as Prisma.PerbekelDariMasaKeMasaGetPayload<{
|
||||||
|
include: {
|
||||||
|
image: true;
|
||||||
|
};
|
||||||
|
}> | null,
|
||||||
|
async load(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/desa/mantanperbekel/${id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
mantanPerbekel.findUnique.data = data.data ?? null;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to fetch mantan perbekel:", res.statusText);
|
||||||
|
mantanPerbekel.findUnique.data = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching mantan perbekel:", error);
|
||||||
|
mantanPerbekel.findUnique.data = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
loading: false,
|
||||||
|
async byId(id: string) {
|
||||||
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
try {
|
||||||
|
mantanPerbekel.delete.loading = true;
|
||||||
|
const response = await fetch(`/api/desa/mantanperbekel/del/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(result.message || "Mantan perbekel berhasil dihapus");
|
||||||
|
await mantanPerbekel.findMany.load();
|
||||||
|
} else {
|
||||||
|
toast.error(result.message || "Gagal menghapus mantan perbekel");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Gagal delete:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menghapus mantan perbekel");
|
||||||
|
} finally {
|
||||||
|
mantanPerbekel.delete.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...mantanPerbekelDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/mantanperbekel/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
if (result?.success) {
|
||||||
|
const data = result.data;
|
||||||
|
this.id = data.id;
|
||||||
|
this.form = {
|
||||||
|
nama: data.nama,
|
||||||
|
daerah: data.daerah,
|
||||||
|
periode: data.periode,
|
||||||
|
imageId: data.imageId || "",
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal memuat data");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading foto:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Gagal memuat data"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async update() {
|
||||||
|
const cek = mantanPerbekelForm.safeParse(mantanPerbekel.update.form);
|
||||||
|
if (!cek.success) {
|
||||||
|
const err = `[${cek.error.issues
|
||||||
|
.map((v) => `${v.path.join(".")}`)
|
||||||
|
.join("\n")}] required`;
|
||||||
|
toast.error(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mantanPerbekel.update.loading = true;
|
||||||
|
const response = await fetch(`/api/desa/mantanperbekel/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
nama: this.form.nama,
|
||||||
|
daerah: this.form.daerah,
|
||||||
|
periode: this.form.periode,
|
||||||
|
imageId: this.form.imageId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
toast.success(result.message || "Mantan perbekel berhasil diupdate");
|
||||||
|
await mantanPerbekel.findMany.load();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal mengupdate mantan perbekel");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating mantan perbekel:", error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Gagal mengupdate mantan perbekel"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
mantanPerbekel.update.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
mantanPerbekel.update.id = "";
|
||||||
|
mantanPerbekel.update.form = { ...mantanPerbekelDefaultForm };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default mantanPerbekel;
|
||||||
183
src/app/admin/(dashboard)/_state/desa/profile/maskotDesa.ts
Normal file
183
src/app/admin/(dashboard)/_state/desa/profile/maskotDesa.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
const maskotForm = z.object({
|
||||||
|
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||||
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
|
images: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string().min(1, "Label wajib"),
|
||||||
|
imageId: z.string().min(1, "Image ID wajib"),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1, "Minimal 1 gambar harus diisi"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const maskotDefaultForm = {
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
images: [] as { label: string; imageId: string }[],
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormData = typeof maskotDefaultForm;
|
||||||
|
|
||||||
|
type MaskotDesaForm = Prisma.MaskotDesaGetPayload<{
|
||||||
|
include: {
|
||||||
|
images: {
|
||||||
|
include: {
|
||||||
|
image: {
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
name: true;
|
||||||
|
path: true;
|
||||||
|
link: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const maskotDesa = proxy({
|
||||||
|
findUnique: {
|
||||||
|
data: null as MaskotDesaForm | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/maskot/${id}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal mengambil data profile");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
console.error("Load profile error:", msg);
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data profile");
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...maskotDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(profileData: MaskotDesaForm) {
|
||||||
|
this.id = profileData.id;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.form = {
|
||||||
|
judul: profileData.judul || "",
|
||||||
|
deskripsi: profileData.deskripsi || "",
|
||||||
|
images: (profileData.images || []).map((img) => ({
|
||||||
|
label: img.label,
|
||||||
|
imageId: img?.image?.id || "",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateField<K extends keyof FormData>(field: K, value: FormData[K]) {
|
||||||
|
this.form[field] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
addImage() {
|
||||||
|
this.form.images.push({ label: "", imageId: "" });
|
||||||
|
},
|
||||||
|
|
||||||
|
removeImage(index: number) {
|
||||||
|
this.form.images.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const validation = maskotForm.safeParse(this.form);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
const errors = validation.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
toast.error(`Form tidak valid: ${errors}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/maskot/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
toast.success("Berhasil update profile");
|
||||||
|
await maskotDesa.findUnique.load(this.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update profile");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
toast.error("Terjadi kesalahan saat update profile");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...maskotDefaultForm };
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadForEdit(id: string) {
|
||||||
|
const data = await this.findUnique.load(id);
|
||||||
|
if (data) {
|
||||||
|
this.update.initialize(data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.findUnique.reset();
|
||||||
|
this.update.reset();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default maskotDesa;
|
||||||
185
src/app/admin/(dashboard)/_state/desa/profile/profilPerbekel.ts
Normal file
185
src/app/admin/(dashboard)/_state/desa/profile/profilPerbekel.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
const profilPerbekelForm = z.object({
|
||||||
|
biodata: z.string().min(3, "Biodata minimal 3 karakter"),
|
||||||
|
pengalaman: z.string().min(3, "Pengalaman minimal 3 karakter"),
|
||||||
|
pengalamanOrganisasi: z
|
||||||
|
.string()
|
||||||
|
.min(3, "Pengalaman Organisasi minimal 3 karakter"),
|
||||||
|
programUnggulan: z.string().min(3, "Program Unggulan minimal 3 karakter"),
|
||||||
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const profilPerbekelDefaultForm = {
|
||||||
|
biodata: "",
|
||||||
|
pengalaman: "",
|
||||||
|
pengalamanOrganisasi: "",
|
||||||
|
programUnggulan: "",
|
||||||
|
imageId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProfilPerbekelForm = Prisma.ProfilPerbekelGetPayload<{
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
biodata: true;
|
||||||
|
pengalaman: true;
|
||||||
|
pengalamanOrganisasi: true;
|
||||||
|
programUnggulan: true;
|
||||||
|
imageId: true;
|
||||||
|
image?: {
|
||||||
|
select: {
|
||||||
|
link: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const profilPerbekel = proxy({
|
||||||
|
findUnique: {
|
||||||
|
data: null as ProfilPerbekelForm | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/profileperbekel/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengambil data profil perbekel"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data profil perbekel");
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: {
|
||||||
|
id: "",
|
||||||
|
form: { ...profilPerbekelDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(profilData: ProfilPerbekelForm) {
|
||||||
|
this.id = profilData.id;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.form = {
|
||||||
|
biodata: profilData.biodata || "",
|
||||||
|
pengalaman: profilData.pengalaman || "",
|
||||||
|
pengalamanOrganisasi: profilData.pengalamanOrganisasi || "",
|
||||||
|
programUnggulan: profilData.programUnggulan || "",
|
||||||
|
imageId: profilData.imageId || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateField(field: keyof typeof profilPerbekelDefaultForm, value: string) {
|
||||||
|
this.form[field] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const validation = profilPerbekelForm.safeParse(this.form);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
const errors = validation.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
toast.error(`Form tidak valid: ${errors}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/desa/profile/profileperbekel/${this.id}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
toast.success("Berhasil update profil perbekel");
|
||||||
|
await profilPerbekel.findUnique.load(this.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update profil perbekel");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
toast.error("Terjadi kesalahan saat update profil perbekel");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...profilPerbekelDefaultForm };
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadForEdit(id: string) {
|
||||||
|
const profileData = await this.findUnique.load(id);
|
||||||
|
if (profileData) {
|
||||||
|
this.edit.initialize(profileData);
|
||||||
|
}
|
||||||
|
return profileData;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.findUnique.reset();
|
||||||
|
this.edit.reset();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default profilPerbekel;
|
||||||
153
src/app/admin/(dashboard)/_state/desa/profile/sejarahDesa.ts
Normal file
153
src/app/admin/(dashboard)/_state/desa/profile/sejarahDesa.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
const sejarahDesaForm = z.object({
|
||||||
|
judul: z.string().min(3, "Judul minimal 3 karakter"),
|
||||||
|
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sejarahDesaDefaultForm = {
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type SejarahDesaForm = Prisma.SejarahDesaGetPayload<{
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
judul: true;
|
||||||
|
deskripsi: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const sejarahDesa = proxy({
|
||||||
|
findUnique: {
|
||||||
|
data: null as SejarahDesaForm | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/sejarah/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengambil data sejarah desa"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
console.error("Load sejarah desa error:", msg);
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data sejarah desa");
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...sejarahDesaDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(sejarahData: SejarahDesaForm) {
|
||||||
|
this.id = sejarahData.id;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.form = {
|
||||||
|
judul: sejarahData.judul || "",
|
||||||
|
deskripsi: sejarahData.deskripsi || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateField(field: keyof typeof sejarahDesaDefaultForm, value: string) {
|
||||||
|
this.form[field] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const validation = sejarahDesaForm.safeParse(this.form);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
const errors = validation.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
toast.error(`Form tidak valid: ${errors}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/sejarah/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
toast.success("Berhasil update profile");
|
||||||
|
await sejarahDesa.findUnique.load(this.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update profile");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
this.error = errorMessage;
|
||||||
|
console.error("Update profile error:", errorMessage);
|
||||||
|
toast.error("Terjadi kesalahan saat update profile");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...sejarahDesaDefaultForm };
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default sejarahDesa;
|
||||||
153
src/app/admin/(dashboard)/_state/desa/profile/visiMisiDesa.ts
Normal file
153
src/app/admin/(dashboard)/_state/desa/profile/visiMisiDesa.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
const visiMisiDesaForm = z.object({
|
||||||
|
visi: z.string().min(3, "Visi minimal 3 karakter"),
|
||||||
|
misi: z.string().min(3, "Misi minimal 3 karakter"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const visiMisiDesaDefaultForm = {
|
||||||
|
visi: "",
|
||||||
|
misi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type VisiMisiDesaForm = Prisma.VisiMisiDesaGetPayload<{
|
||||||
|
select: {
|
||||||
|
id: true;
|
||||||
|
visi: true;
|
||||||
|
misi: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const visiMisiDesa = proxy({
|
||||||
|
findUnique: {
|
||||||
|
data: null as VisiMisiDesaForm | null,
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
|
async load(id: string) {
|
||||||
|
if (!id) {
|
||||||
|
toast.warn("ID tidak valid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/visi-misi/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.data = result.data;
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
result.message || "Gagal mengambil data visi misi desa"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = (error as Error).message;
|
||||||
|
this.error = msg;
|
||||||
|
console.error("Load visi misi desa error:", msg);
|
||||||
|
toast.error("Terjadi kesalahan saat mengambil data visi misi desa");
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = null;
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
id: "",
|
||||||
|
form: { ...visiMisiDesaDefaultForm },
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
isReadOnly: false,
|
||||||
|
|
||||||
|
initialize(visiMisiData: VisiMisiDesaForm) {
|
||||||
|
this.id = visiMisiData.id;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.form = {
|
||||||
|
visi: visiMisiData.visi || "",
|
||||||
|
misi: visiMisiData.misi || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
updateField(field: keyof typeof visiMisiDesaDefaultForm, value: string) {
|
||||||
|
this.form[field] = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
const validation = visiMisiDesaForm.safeParse(this.form);
|
||||||
|
|
||||||
|
if (!validation.success) {
|
||||||
|
const errors = validation.error.issues
|
||||||
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
||||||
|
.join(", ");
|
||||||
|
toast.error(`Form tidak valid: ${errors}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/desa/profile/visi-misi/${this.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
toast.success("Berhasil update visi misi desa");
|
||||||
|
await visiMisiDesa.findUnique.load(this.id);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Gagal update visi misi desa");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = (error as Error).message;
|
||||||
|
this.error = errorMessage;
|
||||||
|
console.error("Update visi misi desa error:", errorMessage);
|
||||||
|
toast.error("Terjadi kesalahan saat update visi misi desa");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.id = "";
|
||||||
|
this.form = { ...visiMisiDesaDefaultForm };
|
||||||
|
this.error = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.isReadOnly = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default visiMisiDesa;
|
||||||
@@ -43,15 +43,17 @@ export function KegiatanCard({
|
|||||||
style={{ cursor: 'pointer', transition: 'box-shadow 0.2s' }}
|
style={{ cursor: 'pointer', transition: 'box-shadow 0.2s' }}
|
||||||
onClick={onNavigate}
|
onClick={onNavigate}
|
||||||
>
|
>
|
||||||
<Card.Section>
|
{item.image?.link && (
|
||||||
<Image
|
<Card.Section>
|
||||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
<Image
|
||||||
height={200}
|
src={item.image.link}
|
||||||
alt={item.judul}
|
height={200}
|
||||||
fit="cover"
|
alt={item.judul}
|
||||||
loading="lazy"
|
fit="cover"
|
||||||
/>
|
loading="lazy"
|
||||||
</Card.Section>
|
/>
|
||||||
|
</Card.Section>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack mt="md" gap="xs">
|
<Stack mt="md" gap="xs">
|
||||||
<Badge color="blue" variant="light" size="sm" radius="md" w="fit-content">
|
<Badge color="blue" variant="light" size="sm" radius="md" w="fit-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user