nico/27-okt-25 #1

Merged
nicoarya20 merged 277 commits from nico/27-okt-25 into main 2025-10-27 22:18:01 +08:00
1655 changed files with 160877 additions and 933 deletions
Showing only changes of commit 63054cedf0 - Show all commits

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import {
@@ -10,7 +11,7 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,10 +24,9 @@ function EditKategoriPengumuman() {
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: editState.update.form.name || '',
});
const [formData, setFormData] = useState({ name: '' });
// Load data awal sekali aja
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -35,9 +35,7 @@ function EditKategoriPengumuman() {
try {
const data = await editState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
});
setFormData({ name: data.name || '' });
}
} catch (error) {
console.error('Error loading kategori Pengumuman:', error);
@@ -48,8 +46,16 @@ function EditKategoriPengumuman() {
loadKategori();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// Update global state hanya di sini
editState.update.form = {
...editState.update.form,
name: formData.name,
@@ -97,9 +103,7 @@ function EditKategoriPengumuman() {
label="Nama Kategori Pengumuman"
placeholder="Masukkan nama kategori Pengumuman"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
onChange={(e) => handleChange('name', e.target.value)}
required
/>

View File

@@ -28,16 +28,16 @@ function EditPengumuman() {
const params = useParams();
const [formData, setFormData] = useState({
judul: editState.pengumuman.edit.form.judul || "",
deskripsi: editState.pengumuman.edit.form.deskripsi || "",
categoryPengumumanId:
editState.pengumuman.edit.form.categoryPengumumanId || "",
content: editState.pengumuman.edit.form.content || "",
judul: "",
deskripsi: "",
categoryPengumumanId: "",
content: "",
});
// Load pengumuman by id saat pertama kali
// Load kategori & pengumuman by id saat pertama kali
useEffect(() => {
editState.category.findMany.load();
const loadpengumuman = async () => {
const id = params?.id as string;
if (!id) return;
@@ -61,9 +61,13 @@ function EditPengumuman() {
loadpengumuman();
}, [params?.id]);
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
// update global state
// update global state hanya sekali pas submit
editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form,
...formData,
@@ -108,26 +112,22 @@ function EditPengumuman() {
<TextInput
label="Judul Pengumuman"
placeholder="Masukkan judul"
defaultValue={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
value={formData.judul}
onChange={(e) => handleChange("judul", e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
defaultValue={formData.deskripsi}
onChange={(e) =>
setFormData({ ...formData, deskripsi: e.target.value })
}
value={formData.deskripsi}
onChange={(e) => handleChange("deskripsi", e.target.value)}
required
/>
<Select
value={formData.categoryPengumumanId}
onChange={(val) =>
setFormData({ ...formData, categoryPengumumanId: val || "" })
}
onChange={(val) => handleChange("categoryPengumumanId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -150,9 +150,7 @@ function EditPengumuman() {
</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) =>
setFormData({ ...formData, content: htmlContent })
}
onChange={(htmlContent) => handleChange("content", htmlContent)}
/>
</Box>

View File

@@ -24,9 +24,10 @@ function EditKategoriPotensi() {
const params = useParams();
const [formData, setFormData] = useState({
nama: editState.update.form.nama || '',
nama: '',
});
// Load data dari backend -> isi ke formData lokal
useEffect(() => {
const loadKategori = async () => {
const id = params?.id as string;
@@ -48,8 +49,16 @@ function EditKategoriPotensi() {
loadKategori();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// Update global state hanya pas submit
editState.update.form = {
...editState.update.form,
nama: formData.nama,
@@ -68,7 +77,12 @@ function EditKategoriPotensi() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -89,12 +103,12 @@ function EditKategoriPotensi() {
<TextInput
label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.currentTarget.value)}
required
/>
<Group justify="right">
<Group justify="flex-end">
<Button
onClick={handleSubmit}
radius="md"

View File

@@ -40,8 +40,14 @@ function EditPotensi() {
imageId: "",
});
// handle input changes
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load();
const loadPotensi = async () => {
const id = params?.id as string;
if (!id) return;
@@ -72,22 +78,27 @@ function EditPotensi() {
const handleSubmit = async () => {
try {
potensiState.edit.form = {
...potensiState.edit.form,
...formData,
};
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
potensiState.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
potensiState.edit.form = {
...formData,
imageId,
};
await potensiState.edit.update();
toast.success("Potensi berhasil diperbarui!");
router.push("/admin/desa/potensi/list-potensi");
@@ -101,7 +112,12 @@ function EditPotensi() {
<Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
@@ -122,22 +138,22 @@ function EditPotensi() {
<TextInput
label="Judul Potensi"
placeholder="Masukkan judul"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi"
defaultValue={formData.deskripsi}
onChange={(e) => setFormData({ ...formData, deskripsi: e.target.value })}
value={formData.deskripsi}
onChange={(e) => handleChange("deskripsi", e.target.value)}
required
/>
<Select
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || "" })}
onChange={(val) => handleChange("kategoriId", val || "")}
label="Kategori"
placeholder="Pilih kategori"
data={
@@ -164,7 +180,9 @@ function EditPotensi() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
@@ -172,7 +190,11 @@ function EditPotensi() {
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
<IconUpload
size={48}
color={colors["blue-button"]}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
@@ -214,7 +236,9 @@ function EditPotensi() {
</Text>
<EditEditor
value={formData.content}
onChange={(htmlContent) => setFormData({ ...formData, content: htmlContent })}
onChange={(htmlContent) =>
handleChange("content", htmlContent)
}
/>
</Box>

View File

@@ -26,15 +26,17 @@ function EditPerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: state.update.form.nama || '',
daerah: state.update.form.daerah || '',
periode: state.update.form.periode || '',
imageId: state.update.form.imageId || ''
nama: '',
daerah: '',
periode: '',
imageId: ''
});
// load data pertama kali
useEffect(() => {
const loadFoto = async () => {
const id = params?.id as string;
@@ -48,7 +50,9 @@ function EditPerbekelDariMasaKeMasa() {
periode: data.periode || '',
imageId: data.imageId || ''
});
if (data?.imageGalleryFoto?.link) setPreviewImage(data.imageGalleryFoto.link);
if (data?.imageGalleryFoto?.link) {
setPreviewImage(data.imageGalleryFoto.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
@@ -58,8 +62,17 @@ function EditPerbekelDariMasaKeMasa() {
loadFoto();
}, [params?.id]);
// helper ubah state formData
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// update global state hanya sekali pas submit
state.update.form = { ...state.update.form, ...formData };
if (file) {
@@ -106,8 +119,8 @@ function EditPerbekelDariMasaKeMasa() {
<TextInput
label="Nama"
placeholder="Masukkan nama"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -161,7 +174,7 @@ function EditPerbekelDariMasaKeMasa() {
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading='lazy'
loading="lazy"
/>
</Box>
)}
@@ -170,16 +183,16 @@ function EditPerbekelDariMasaKeMasa() {
<TextInput
label="Daerah"
placeholder="Masukkan daerah"
defaultValue={formData.daerah}
onChange={(e) => setFormData({ ...formData, daerah: e.target.value })}
value={formData.daerah}
onChange={(e) => handleChange('daerah', e.target.value)}
required
/>
<TextInput
label="Periode"
placeholder="Masukkan periode"
defaultValue={formData.periode}
onChange={(e) => setFormData({ ...formData, periode: e.target.value })}
value={formData.periode}
onChange={(e) => handleChange('periode', e.target.value)}
required
/>

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import {
@@ -29,13 +29,13 @@ function EditAPBDesa() {
const params = useParams();
const [formData, setFormData] = useState({
tahun: apbState.update.form.tahun || '',
pendapatanIds: apbState.update.form.pendapatanIds || [],
belanjaIds: apbState.update.form.belanjaIds || [],
pembiayaanIds: apbState.update.form.pembiayaanIds || [],
tahun: '',
pendapatanIds: [] as string[],
belanjaIds: [] as string[],
pembiayaanIds: [] as string[],
});
// Load APB desa by id
// Load APB desa by id → hanya update formData, bukan global state
useEffect(() => {
const loadAPBdesa = async () => {
const id = params?.id as string;
@@ -45,7 +45,7 @@ function EditAPBDesa() {
const data = await apbState.update.load(id);
if (data) {
setFormData({
tahun: data.tahun || 0,
tahun: String(data.tahun || ''),
pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
@@ -60,8 +60,13 @@ function EditAPBDesa() {
loadAPBdesa();
}, [params?.id]);
const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
// update global state cuma pas submit
apbState.update.form = {
...apbState.update.form,
tahun: Number(formData.tahun),
@@ -111,10 +116,8 @@ function EditAPBDesa() {
{/* Tahun */}
<TextInput
type="number"
defaultValue={formData.tahun}
onChange={(e) =>
setFormData({ ...formData, tahun: e.target.value })
}
value={formData.tahun}
onChange={(e) => handleChange("tahun", e.target.value)}
label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Masukkan tahun anggaran"
required
@@ -123,23 +126,17 @@ function EditAPBDesa() {
{/* Selects */}
<SelectPendapatan
selectedIds={formData.pendapatanIds}
onSelectionChange={(ids) =>
setFormData({ ...formData, pendapatanIds: ids })
}
onSelectionChange={(ids) => handleChange("pendapatanIds", ids)}
/>
<SelectBelanja
selectedIds={formData.belanjaIds}
onSelectionChange={(ids) =>
setFormData({ ...formData, belanjaIds: ids })
}
onSelectionChange={(ids) => handleChange("belanjaIds", ids)}
/>
<SelectPembiayaan
selectedIds={formData.pembiayaanIds}
onSelectionChange={(ids) =>
setFormData({ ...formData, pembiayaanIds: ids })
}
onSelectionChange={(ids) => handleChange("pembiayaanIds", ids)}
/>
{/* Save Button */}
@@ -164,7 +161,13 @@ function EditAPBDesa() {
/* --- Sub Components --- */
function SelectPendapatan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
function SelectPendapatan({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => {
@@ -192,7 +195,13 @@ function EditAPBDesa() {
);
}
function SelectBelanja({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
function SelectBelanja({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
@@ -220,7 +229,13 @@ function EditAPBDesa() {
);
}
function SelectPembiayaan({ selectedIds, onSelectionChange }: { selectedIds: string[]; onSelectionChange: (ids: string[]) => void }) {
function SelectPembiayaan({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import {
@@ -24,12 +25,16 @@ function EditBelanja() {
const params = useParams();
const [formData, setFormData] = useState({
name: belanjaState.update.form.name || '',
value: belanjaState.update.form.value || '',
name: '',
value: '',
});
// format angka ke rupiah
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number =
typeof value === 'number'
? value
: Number(value.replace(/\D/g, '')) || 0;
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -37,8 +42,9 @@ function EditBelanja() {
}).format(number);
};
// buang semua simbol jadi angka murni
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
return Number(value.replace(/\D/g, '')) || 0;
};
useEffect(() => {
@@ -51,7 +57,7 @@ function EditBelanja() {
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
value: String(data.value || ''),
});
}
} catch (error) {
@@ -69,7 +75,7 @@ function EditBelanja() {
...belanjaState.update.form,
name: formData.name,
value: Number(formData.value),
}
};
await belanjaState.update.update();
toast.success("Jenis Belanja berhasil diperbarui!");
@@ -78,7 +84,7 @@ function EditBelanja() {
console.error("Error updating jenis belanja:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis belanja");
}
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -112,19 +118,21 @@ function EditBelanja() {
<TextInput
label="Nama Jenis Belanja"
placeholder="Masukkan nama jenis belanja"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
defaultValue={formatRupiah(formData.value)}
value={formData.value ? formatRupiah(formData.value) : ''}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
setFormData({ ...formData, value: String(cleanValue) });
}}
required
/>

View File

@@ -24,12 +24,15 @@ function EditPembiayaan() {
const params = useParams();
const [formData, setFormData] = useState({
name: pembiayaanState.update.form.name || '',
value: pembiayaanState.update.form.value || '',
name: '',
value: '',
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
const number =
typeof value === 'number'
? value
: Number(value.toString().replace(/\D/g, '')) || 0;
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -38,7 +41,7 @@ function EditPembiayaan() {
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
return Number(value.replace(/\D/g, '')) || 0;
};
useEffect(() => {
@@ -51,7 +54,7 @@ function EditPembiayaan() {
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
value: String(data.value || ''),
});
}
} catch (error) {
@@ -68,7 +71,7 @@ function EditPembiayaan() {
pembiayaanState.update.form = {
...pembiayaanState.update.form,
name: formData.name,
value: Number(formData.value),
value: unformatRupiah(formData.value),
};
await pembiayaanState.update.update();
@@ -82,7 +85,7 @@ function EditPembiayaan() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan Back Button */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -112,19 +115,21 @@ function EditPembiayaan() {
<TextInput
label="Nama Jenis Pembiayaan"
placeholder="Masukkan nama jenis pembiayaan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
defaultValue={formatRupiah(formData.value)}
value={formData.value ? formatRupiah(formData.value) : ''}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
setFormData((prev) => ({ ...prev, value: String(cleanValue) }));
}}
required
/>

View File

@@ -24,12 +24,16 @@ function EditPendapatan() {
const params = useParams();
const [formData, setFormData] = useState({
name: pendapatanState.update.form.name || '',
value: pendapatanState.update.form.value || '',
name: '',
value: '',
});
// helper format
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.toString().replace(/\D/g, ''));
const number = typeof value === 'number'
? value
: Number(value.toString().replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -39,6 +43,7 @@ function EditPendapatan() {
const unformatRupiah = (value: string) => Number(value.replace(/\D/g, ''));
// load data once
useEffect(() => {
const id = params?.id as string;
if (!id) return;
@@ -48,8 +53,8 @@ function EditPendapatan() {
const data = await pendapatanState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
name: data.name ?? '',
value: data.value?.toString() ?? '',
});
}
} catch (error) {
@@ -61,6 +66,13 @@ function EditPendapatan() {
loadPendapatan();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
pendapatanState.update.form = {
@@ -110,19 +122,19 @@ function EditPendapatan() {
<TextInput
label="Nama Jenis Pendapatan"
placeholder="Masukkan nama jenis pendapatan"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
defaultValue={formatRupiah(formData.value)}
value={formData.value ? formatRupiah(formData.value) : ''}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
const cleanValue = unformatRupiah(raw).toString();
handleChange('value', cleanValue);
}}
required
/>

View File

@@ -10,15 +10,21 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
interface FormData {
pekerjaan: string;
lakiLaki: number;
perempuan: number;
}
function EditDemografiPekerjaan() {
const router = useRouter();
const params = useParams() as { id: string };
@@ -26,19 +32,27 @@ function EditDemografiPekerjaan() {
const id = params.id;
const [formData, setFormData] = useState<FormData>({
pekerjaan: '',
lakiLaki: 0,
perempuan: 0,
});
// Load data sekali waktu
useEffect(() => {
if (!id) return;
stateDemografi.update.id = id;
stateDemografi.findUnique
.load(id)
.then(() => {
const data = stateDemografi.findUnique.data;
if (data) {
stateDemografi.update.form = {
setFormData({
pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0),
};
});
}
})
.catch((error) => {
@@ -47,9 +61,22 @@ function EditDemografiPekerjaan() {
});
}, [id]);
const handleChange =
(field: keyof FormData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[field]:
field === 'lakiLaki' || field === 'perempuan'
? Number(e.currentTarget.value)
: e.currentTarget.value,
}));
};
const handleSubmit = async () => {
try {
stateDemografi.update.id = id;
stateDemografi.update.form = { ...formData };
await stateDemografi.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/ekonomi/demografi-pekerjaan');
@@ -91,10 +118,8 @@ function EditDemografiPekerjaan() {
<TextInput
label="Pekerjaan"
placeholder="Masukkan jenis pekerjaan"
defaultValue={stateDemografi.update.form.pekerjaan}
onChange={(e) =>
(stateDemografi.update.form.pekerjaan = e.currentTarget.value)
}
value={formData.pekerjaan}
onChange={handleChange('pekerjaan')}
required
/>
@@ -102,12 +127,8 @@ function EditDemografiPekerjaan() {
label="Jumlah Pekerja Laki-laki"
type="number"
placeholder="Masukkan jumlah pekerja laki-laki"
defaultValue={stateDemografi.update.form.lakiLaki}
onChange={(e) =>
(stateDemografi.update.form.lakiLaki = Number(
e.currentTarget.value
))
}
value={formData.lakiLaki}
onChange={handleChange('lakiLaki')}
required
/>
@@ -115,12 +136,8 @@ function EditDemografiPekerjaan() {
label="Jumlah Pekerja Perempuan"
type="number"
placeholder="Masukkan jumlah pekerja perempuan"
defaultValue={stateDemografi.update.form.perempuan}
onChange={(e) =>
(stateDemografi.update.form.perempuan = Number(
e.currentTarget.value
))
}
value={formData.perempuan}
onChange={handleChange('perempuan')}
required
/>

View File

@@ -3,10 +3,19 @@
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import {
Box,
Button,
Paper,
Stack,
TextInput,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
@@ -17,6 +26,13 @@ function EditJumlahPendudukMiskin() {
const id = params.id;
// 🔹 State lokal untuk form
const [formData, setFormData] = useState({
year: 0,
totalPoorPopulation: 0,
});
// 🔹 Load data awal dari backend
useEffect(() => {
if (!id) return;
@@ -25,10 +41,10 @@ function EditJumlahPendudukMiskin() {
await stateJPM.findUnique.load(id);
const data = stateJPM.findUnique.data;
if (data) {
stateJPM.update.form = {
setFormData({
year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0,
};
});
}
} catch (error) {
console.error('Gagal memuat data:', error);
@@ -39,9 +55,21 @@ function EditJumlahPendudukMiskin() {
loadData();
}, [id]);
// 🔹 Handler input controlled
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: Number(value),
}));
};
// 🔹 Submit form
const handleSubmit = async () => {
try {
stateJPM.update.id = id;
// update global state cuma saat submit
stateJPM.update.form = { ...formData };
await stateJPM.update.submit();
toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
@@ -55,7 +83,12 @@ function EditJumlahPendudukMiskin() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -78,10 +111,8 @@ function EditJumlahPendudukMiskin() {
placeholder="Masukkan tahun"
type="number"
required
defaultValue={stateJPM.update.form.year}
onChange={(val) => {
stateJPM.update.form.year = Number(val.currentTarget.value);
}}
value={formData.year}
onChange={(e) => handleChange('year', e.currentTarget.value)}
/>
<TextInput
@@ -89,10 +120,10 @@ function EditJumlahPendudukMiskin() {
placeholder="Masukkan jumlah penduduk miskin"
type="number"
required
defaultValue={stateJPM.update.form.totalPoorPopulation}
onChange={(val) => {
stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
value={formData.totalPoorPopulation}
onChange={(e) =>
handleChange('totalPoorPopulation', e.currentTarget.value)
}
/>
<Group justify="right">

View File

@@ -1,11 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() {
@@ -14,34 +14,59 @@ function EditGrafikBerdasarkanPendidikan() {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id;
// state lokal untuk form
const [formData, setFormData] = useState({
SD: '',
SMP: '',
SMA: '',
D3: '',
S1: '',
});
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data;
if (data) {
stategrafik.update.form = {
setFormData({
SD: data.SD || '',
SMP: data.SMP || '',
SMA: data.SMA || '',
D3: data.D3 || '',
S1: data.S1 || '',
};
});
}
});
}
}, [id]);
const handleChange = (field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[field]: e.currentTarget.value,
}));
};
const handleSubmit = async () => {
stategrafik.update.id = id;
stategrafik.update.form = { ...formData }; // update global state pas submit aja
await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -63,36 +88,36 @@ function EditGrafikBerdasarkanPendidikan() {
label="SD"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.SD}
onChange={(val) => (stategrafik.update.form.SD = val.currentTarget.value)}
value={formData.SD}
onChange={handleChange('SD')}
/>
<TextInput
label="SMP"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.SMP}
onChange={(val) => (stategrafik.update.form.SMP = val.currentTarget.value)}
value={formData.SMP}
onChange={handleChange('SMP')}
/>
<TextInput
label="SMA"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.SMA}
onChange={(val) => (stategrafik.update.form.SMA = val.currentTarget.value)}
value={formData.SMA}
onChange={handleChange('SMA')}
/>
<TextInput
label="D3"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.D3}
onChange={(val) => (stategrafik.update.form.D3 = val.currentTarget.value)}
value={formData.D3}
onChange={handleChange('D3')}
/>
<TextInput
label="S1"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.S1}
onChange={(val) => (stategrafik.update.form.S1 = val.currentTarget.value)}
value={formData.S1}
onChange={handleChange('S1')}
/>
<Group justify="right">

View File

@@ -2,39 +2,70 @@
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import {
Box,
Button,
Paper,
Stack,
TextInput,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter();
const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const stategrafik = useProxy(
grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur
);
const id = params.id;
// ✅ state lokal, controlled
const [formData, setFormData] = useState({
usia18_25: '',
usia26_35: '',
usia36_45: '',
usia46_keatas: '',
});
// load data dari global state -> masukin ke local state
useEffect(() => {
if (id) {
stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data;
if (data) {
stategrafik.update.form = {
setFormData({
usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '',
};
});
}
});
}
}, [id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// ✅ baru update global state pas submit
stategrafik.update.id = id;
stategrafik.update.form = { ...formData };
await stategrafik.update.submit();
toast.success('Data grafik berhasil diperbarui!');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
@@ -49,7 +80,12 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -71,40 +107,32 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
value={formData.usia18_25}
onChange={(e) => handleChange('usia18_25', e.currentTarget.value)}
required
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
value={formData.usia26_35}
onChange={(e) => handleChange('usia26_35', e.currentTarget.value)}
required
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
value={formData.usia36_45}
onChange={(e) => handleChange('usia36_45', e.currentTarget.value)}
required
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
value={formData.usia46_keatas}
onChange={(e) => handleChange('usia46_keatas', e.currentTarget.value)}
required
/>

View File

@@ -2,10 +2,21 @@
'use client';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Select, NumberInput } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Select,
NumberInput,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
@@ -24,40 +35,57 @@ function EditDetailDataPengangguran() {
});
// Hitung total & perubahan otomatis
const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
const calculateTotalAndChange = useCallback(
async (data: typeof formData) => {
const total = data.educatedUnemployment + data.uneducatedUnemployment;
let percentageChange = 0;
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentMonthIndex = monthOrder.indexOf(formData.month);
let percentageChange = 0;
const monthOrder = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des',
];
const currentMonthIndex = monthOrder.indexOf(data.month);
if (currentMonthIndex !== -1) {
let prevMonthIndex = currentMonthIndex - 1;
let prevYear = formData.year;
if (currentMonthIndex !== -1) {
let prevMonthIndex = currentMonthIndex - 1;
let prevYear = data.year;
if (prevMonthIndex < 0) {
prevMonthIndex = 11;
prevYear--;
if (prevMonthIndex < 0) {
prevMonthIndex = 11;
prevYear--;
}
const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: prevYear,
});
if (prevData && prevData.totalUnemployment > 0) {
const change =
((total - prevData.totalUnemployment) /
prevData.totalUnemployment) *
100;
percentageChange = parseFloat(change.toFixed(1));
}
}
const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear });
if (prevData && prevData.totalUnemployment > 0) {
const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
}
return { total, percentageChange };
};
return { total, percentageChange };
},
[stateDetail.findByMonthYear]
);
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange();
setFormData({ ...newData, totalUnemployment: total, percentageChange });
const { total, percentageChange } = await calculateTotalAndChange(newData);
setFormData({
...newData,
totalUnemployment: total,
percentageChange,
});
};
// Load detail hanya sekali
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
@@ -68,45 +96,39 @@ function EditDetailDataPengangguran() {
const data = stateDetail.findUnique.data;
if (data) {
const yearValue =
data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
// Convert year from Date to number if needed
const yearValue = data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
stateDetail.update.id = id; // set ID untuk update
// Set the ID for update
stateDetail.update.id = id;
// Update Valtio state with converted year
stateDetail.update.form = {
...data,
year: yearValue,
percentageChange: data.percentageChange || 0 // Ensure it's always a number
};
// Update local formData with converted year
setFormData({
month: data.month,
year: yearValue,
totalUnemployment: data.totalUnemployment,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
percentageChange: data.percentageChange || 0, // Ensure it's always a number
percentageChange: data.percentageChange || 0,
});
}
} catch (error) {
console.error("Error loading detail:", error);
toast.error("Gagal memuat data detail");
console.error('Error loading detail:', error);
toast.error('Gagal memuat data detail');
}
};
loadDetail();
}, [params?.id]);
}, [params?.id, stateDetail.findUnique]);
const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange();
const { total, percentageChange } = await calculateTotalAndChange(formData);
try {
stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange };
stateDetail.update.form = {
...formData,
totalUnemployment: total,
percentageChange,
};
const success = await stateDetail.update.submit();
if (success) {
toast.success('Detail data pengangguran berhasil diperbarui!');
@@ -121,7 +143,12 @@ function EditDetailDataPengangguran() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm">
@@ -129,36 +156,57 @@ function EditDetailDataPengangguran() {
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Bulan"
data={['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']}
data={[
'Jan','Feb','Mar','Apr','Mei','Jun',
'Jul','Agu','Sep','Okt','Nov','Des',
]}
value={formData.month}
onChange={(val) => updateFormData({ month: val || '' })}
/>
<NumberInput
label="Tahun"
defaultValue={formData.year}
value={formData.year}
onChange={(val) => updateFormData({ year: Number(val) })}
required
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
defaultValue={formData.educatedUnemployment}
onChange={(val) => updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })}
value={formData.educatedUnemployment}
onChange={(val) =>
updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })
}
required
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
defaultValue={formData.uneducatedUnemployment}
onChange={(val) => updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })}
value={formData.uneducatedUnemployment}
onChange={(val) =>
updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })
}
required
/>
<Text fz="sm" fw={500}>Total Otomatis: {formData.totalUnemployment}</Text>
<Text fz="sm" fw={500}>Perubahan Otomatis: {formData.percentageChange !== null ? `${formData.percentageChange}%` : '-'}</Text>
<Text fz="sm" fw={500}>
Total Otomatis: {formData.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{' '}
{formData.percentageChange !== null
? `${formData.percentageChange}%`
: '-'}
</Text>
<Group justify="right">
<Button
@@ -181,4 +229,3 @@ function EditDetailDataPengangguran() {
}
export default EditDetailDataPengangguran;

View File

@@ -26,15 +26,16 @@ function EditLowonganKerja() {
const params = useParams();
const [formData, setFormData] = useState({
posisi: lowonganKerjaState.update.form.posisi,
namaPerusahaan: lowonganKerjaState.update.form.namaPerusahaan,
lokasi: lowonganKerjaState.update.form.lokasi,
tipePekerjaan: lowonganKerjaState.update.form.tipePekerjaan,
gaji: lowonganKerjaState.update.form.gaji,
deskripsi: lowonganKerjaState.update.form.deskripsi,
kualifikasi: lowonganKerjaState.update.form.kualifikasi,
posisi: '',
namaPerusahaan: '',
lokasi: '',
tipePekerjaan: '',
gaji: '',
deskripsi: '',
kualifikasi: '',
});
// load data sekali aja ketika mount / id berubah
useEffect(() => {
const loadLowongan = async () => {
const id = params?.id as string;
@@ -62,14 +63,17 @@ function EditLowonganKerja() {
loadLowongan();
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
lowonganState.update.id = params?.id as string;
lowonganState.update.form = {
...lowonganState.update.form,
...formData,
};
lowonganState.update.form = { ...formData };
await lowonganState.update.update();
toast.success("Lowongan kerja berhasil diperbarui!");
@@ -107,40 +111,40 @@ function EditLowonganKerja() {
<TextInput
label="Posisi"
placeholder="Masukkan posisi"
defaultValue={formData.posisi}
onChange={(e) => setFormData({ ...formData, posisi: e.target.value })}
value={formData.posisi}
onChange={(e) => handleChange("posisi", e.target.value)}
required
/>
<TextInput
label="Nama Perusahaan"
placeholder="Masukkan nama perusahaan"
defaultValue={formData.namaPerusahaan}
onChange={(e) => setFormData({ ...formData, namaPerusahaan: e.target.value })}
value={formData.namaPerusahaan}
onChange={(e) => handleChange("namaPerusahaan", e.target.value)}
required
/>
<TextInput
label="Lokasi"
placeholder="Masukkan lokasi"
defaultValue={formData.lokasi}
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
value={formData.lokasi}
onChange={(e) => handleChange("lokasi", e.target.value)}
required
/>
<TextInput
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"
defaultValue={formData.tipePekerjaan}
onChange={(e) => setFormData({ ...formData, tipePekerjaan: e.target.value })}
value={formData.tipePekerjaan}
onChange={(e) => handleChange("tipePekerjaan", e.target.value)}
required
/>
<TextInput
label="Gaji (per bulan)"
placeholder="Masukkan gaji"
defaultValue={formData.gaji}
onChange={(e) => setFormData({ ...formData, gaji: e.target.value })}
value={formData.gaji}
onChange={(e) => handleChange("gaji", e.target.value)}
required
/>
@@ -150,7 +154,7 @@ function EditLowonganKerja() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => handleChange("deskripsi", val)}
/>
</Box>
@@ -160,7 +164,7 @@ function EditLowonganKerja() {
</Text>
<EditEditor
value={formData.kualifikasi}
onChange={(val) => setFormData({ ...formData, kualifikasi: val })}
onChange={(val) => handleChange("kualifikasi", val)}
/>
</Box>

View File

@@ -25,9 +25,8 @@ function EditKategoriProduk() {
const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [formData, setFormData] = useState({
nama: '',
});
const [formData, setFormData] = useState({ nama: '' });
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadKategoriProduk = async () => {
@@ -37,21 +36,30 @@ function EditKategoriProduk() {
const data = await statePasar.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
// simpan id ke state global hanya untuk referensi
statePasar.edit.id = id;
setFormData({
nama: data.nama || '',
});
// simpan data ke state lokal
setFormData({ nama: data.nama || '' });
}
} catch (error) {
console.error('Error loading kategori produk:', error);
toast.error('Gagal memuat data kategori produk');
} finally {
setLoading(false);
}
};
loadKategoriProduk();
}, [id]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
@@ -59,10 +67,8 @@ function EditKategoriProduk() {
return;
}
statePasar.edit.form = {
nama: formData.nama.trim(),
};
// update global state hanya saat submit
statePasar.edit.form = { nama: formData.nama.trim() };
if (!statePasar.edit.id) {
statePasar.edit.id = id; // fallback
}
@@ -79,12 +85,21 @@ function EditKategoriProduk() {
}
};
if (loading) {
return <Text>Loading...</Text>;
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -104,10 +119,11 @@ function EditKategoriProduk() {
>
<Stack gap="md">
<TextInput
name="nama"
label={<Text fw="bold" fz="sm">Nama Kategori Produk</Text>}
placeholder="Masukkan nama kategori produk"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={handleChange}
required
/>

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
@@ -24,6 +24,15 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
type FormData = {
nama: string;
harga: number;
alamatUsaha: string;
imageId: string;
rating: number;
kategoriId: string[];
};
function EditPasarDesa() {
const pasarState = useProxy(pasarDesaState);
const router = useRouter();
@@ -31,17 +40,19 @@ function EditPasarDesa() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
nama: pasarState.pasarDesa.edit.form.nama || '',
harga: pasarState.pasarDesa.edit.form.harga || 0,
alamatUsaha: pasarState.pasarDesa.edit.form.alamatUsaha || '',
imageId: pasarState.pasarDesa.edit.form.imageId || '',
rating: pasarState.pasarDesa.edit.form.rating || 0,
kategoriId: pasarState.pasarDesa.edit.form.kategoriId || [],
const [formData, setFormData] = useState<FormData>({
nama: '',
harga: 0,
alamatUsaha: '',
imageId: '',
rating: 0,
kategoriId: [],
});
// load data awal
useEffect(() => {
pasarState.kategoriProduk.findMany.load();
const loadPasarDesa = async () => {
const id = params?.id as string;
if (!id) return;
@@ -70,19 +81,27 @@ function EditPasarDesa() {
loadPasarDesa();
}, [params?.id]);
const handleChange = (key: keyof FormData, value: any) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
const handleSubmit = async () => {
try {
pasarState.pasarDesa.edit.form = { ...pasarState.pasarDesa.edit.form, ...formData };
// upload image kalau ada file baru
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
pasarState.pasarDesa.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// update global state hanya saat submit
pasarState.pasarDesa.edit.form = {
...formData,
imageId,
};
await pasarState.pasarDesa.edit.update();
toast.success('Pasar desa berhasil diperbarui!');
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
@@ -114,6 +133,7 @@ function EditPasarDesa() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Dropzone upload */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Produk
@@ -170,11 +190,12 @@ function EditPasarDesa() {
)}
</Box>
{/* Controlled Inputs */}
<TextInput
label="Nama Produk"
placeholder="Masukkan nama produk"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
@@ -182,8 +203,8 @@ function EditPasarDesa() {
type="number"
label="Harga Produk"
placeholder="Masukkan harga produk"
defaultValue={formData.harga}
onChange={(e) => setFormData({ ...formData, harga: Number(e.target.value) })}
value={formData.harga}
onChange={(e) => handleChange('harga', Number(e.target.value))}
required
/>
@@ -194,16 +215,16 @@ function EditPasarDesa() {
step={0.1}
label="Rating Produk"
placeholder="Masukkan rating produk (0-5)"
defaultValue={formData.rating}
onChange={(e) => setFormData({ ...formData, rating: Number(e.target.value) })}
value={formData.rating}
onChange={(e) => handleChange('rating', Number(e.target.value))}
required
/>
<TextInput
label="Alamat Usaha"
placeholder="Masukkan alamat usaha"
defaultValue={formData.alamatUsaha}
onChange={(e) => setFormData({ ...formData, alamatUsaha: e.target.value })}
value={formData.alamatUsaha}
onChange={(e) => handleChange('alamatUsaha', e.target.value)}
required
/>
@@ -211,7 +232,7 @@ function EditPasarDesa() {
label="Kategori Produk"
placeholder="Pilih kategori produk"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
onChange={(val) => handleChange('kategoriId', val)}
data={
pasarState.kategoriProduk.findMany.data?.map((v) => ({
value: v.id,

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -19,53 +18,95 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
type FormData = {
nama: string;
deskripsi: string;
icon: string;
statistik: {
tahun: string;
jumlah: string;
};
};
function EditProgramKemiskinan() {
const router = useRouter();
const params = useParams() as { id: string };
const stateProgram = useProxy(programKemiskinanState);
const id = params.id;
const [formData, setFormData] = useState<FormData>({
nama: '',
deskripsi: '',
icon: '',
statistik: {
tahun: '',
jumlah: '',
},
});
// load data ke local state sekali aja
useEffect(() => {
if (id) {
stateProgram.findUnique.load(id).then(() => {
const data = stateProgram.findUnique.data;
if (data) {
stateProgram.update.form = {
nama: data.nama || '',
deskripsi: data.deskripsi || '',
icon: data.icon || '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
},
};
}
}).catch((err) => {
console.error("Error load data:", err);
toast.error("Gagal mengambil data program");
});
stateProgram.findUnique
.load(id)
.then(() => {
const data = stateProgram.findUnique.data;
if (data) {
setFormData({
nama: data.nama || '',
deskripsi: data.deskripsi || '',
icon: data.icon || '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
},
});
}
})
.catch((err) => {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
});
}
}, [id]);
}, [id, stateProgram.findUnique]);
const handleChange = (field: keyof FormData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleStatistikChange = (field: keyof FormData['statistik'], value: string) => {
setFormData((prev) => ({
...prev,
statistik: {
...prev.statistik,
[field]: value,
},
}));
};
const handleSubmit = async () => {
try {
stateProgram.update.id = id;
stateProgram.update.form = formData;
await stateProgram.update.update();
toast.success("Program berhasil diperbarui!");
toast.success('Program berhasil diperbarui!');
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {
console.error("Error update program:", error);
toast.error("Terjadi kesalahan saat memperbarui program");
console.error('Error update program:', error);
toast.error('Terjadi kesalahan saat memperbarui program');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -92,10 +133,8 @@ function EditProgramKemiskinan() {
>
<Stack gap="md">
<TextInput
defaultValue={stateProgram.update.form.nama}
onChange={(e) => {
stateProgram.update.form.nama = e.target.value;
}}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label={<Text fw="bold" fz="sm">Judul Program</Text>}
placeholder="Masukkan judul program"
required
@@ -106,10 +145,8 @@ function EditProgramKemiskinan() {
Deskripsi
</Text>
<EditEditor
value={stateProgram.update.form.deskripsi}
onChange={(val) => {
stateProgram.update.form.deskripsi = val;
}}
value={formData.deskripsi}
onChange={(val) => handleChange('deskripsi', val)}
/>
</Box>
@@ -118,10 +155,8 @@ function EditProgramKemiskinan() {
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={stateProgram.update.form.icon as IconKey}
onChange={(value) => {
stateProgram.update.form.icon = value;
}}
value={formData.icon as IconKey}
onChange={(val) => handleChange('icon', val)}
/>
</Box>
@@ -131,10 +166,8 @@ function EditProgramKemiskinan() {
</Text>
<TextInput
type="number"
defaultValue={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
value={formData.statistik.jumlah}
onChange={(e) => handleStatistikChange('jumlah', e.target.value)}
label="Jumlah Masyarakat Miskin"
placeholder="Masukkan jumlah masyarakat miskin"
required
@@ -142,10 +175,8 @@ function EditProgramKemiskinan() {
<TextInput
type="number"
defaultValue={stateProgram.update.form.statistik.tahun}
onChange={(e) => {
stateProgram.update.form.statistik.tahun = e.target.value;
}}
value={formData.statistik.tahun}
onChange={(e) => handleStatistikChange('tahun', e.target.value)}
label="Tahun"
placeholder="Masukkan tahun"
required

View File

@@ -16,7 +16,7 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -28,28 +28,46 @@ function EditSektorUnggulanDesa() {
const id = params.id;
// state lokal buat form
const [formData, setFormData] = useState({
name: '',
description: '',
value: 0,
});
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateGrafik.findUnique.load(id).then(() => {
const data = stateGrafik.findUnique.data;
if (data) {
stateGrafik.update.form = {
name: data.name || '',
description: data.description || '',
value: data.value || 0,
};
}
}).catch((err) => {
console.error('Error load sektor unggulan:', err);
toast.error('Gagal mengambil data sektor unggulan');
});
stateGrafik.findUnique
.load(id)
.then(() => {
const data = stateGrafik.findUnique.data;
if (data) {
setFormData({
name: data.name || '',
description: data.description || '',
value: data.value || 0,
});
}
})
.catch((err) => {
console.error('Error load sektor unggulan:', err);
toast.error('Gagal mengambil data sektor unggulan');
});
}
}, [id]);
const handleChange =
(field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = field === 'value' ? Number(e.currentTarget.value) : e.currentTarget.value;
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
stateGrafik.update.id = id;
stateGrafik.update.form = { ...formData }; // update global pas submit
await stateGrafik.update.submit();
toast.success('Sektor unggulan berhasil diperbarui!');
router.push('/admin/ekonomi/sektor-unggulan-desa');
@@ -89,10 +107,8 @@ function EditSektorUnggulanDesa() {
<TextInput
label="Nama Sektor Unggulan"
placeholder="Masukkan nama sektor unggulan"
defaultValue={stateGrafik.update.form.name}
onChange={(val) => {
stateGrafik.update.form.name = val.currentTarget.value;
}}
value={formData.name}
onChange={handleChange('name')}
required
/>
<Box>
@@ -100,20 +116,18 @@ function EditSektorUnggulanDesa() {
Konten
</Text>
<EditEditor
value={stateGrafik.update.form.description}
onChange={(htmlContent) => {
stateGrafik.update.form.description = htmlContent;
}}
value={formData.description}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, description: htmlContent }))
}
/>
</Box>
<TextInput
label="Jumlah"
type="number"
placeholder="Masukkan jumlah"
defaultValue={stateGrafik.update.form.value}
onChange={(val) => {
stateGrafik.update.form.value = Number(val.currentTarget.value);
}}
value={formData.value}
onChange={handleChange('value')}
required
/>

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import { useEffect, useState } from 'react';
@@ -20,36 +19,49 @@ export default function EditHubunganOrganisasi() {
tipe: '',
});
// load data awal sekali aja
useEffect(() => {
strukturorganisasiState.pegawai.findMany.load();
if (id) {
state.edit.load(id).then(data => {
(async () => {
const data = await state.edit.load(id);
if (data) {
setForm({
atasanId: data.atasanId,
bawahanId: data.bawahanId,
tipe: data.tipe || '',
atasanId: data.atasanId ?? '',
bawahanId: data.bawahanId ?? '',
tipe: data.tipe ?? '',
});
}
});
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleChange = (field: keyof typeof form, value: string) => {
setForm(prev => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
if (!form.atasanId || !form.bawahanId) {
toast.warn("Atasan dan bawahan harus diisi");
toast.warn('Atasan dan bawahan harus diisi');
return;
}
// update global state cuma pas submit
state.edit.id = id;
state.edit.form = form;
const result = await state.edit.update();
if (result) {
toast.success("Data berhasil diperbarui");
router.push('/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi');
toast.success('Data berhasil diperbarui');
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/hubungan-organisasi'
);
}
};
@@ -58,35 +70,45 @@ export default function EditHubunganOrganisasi() {
<Paper p="md" w={{ base: '100%', md: '50%' }}>
<Stack>
<Title order={3}>Edit Hubungan Organisasi</Title>
<Select
label="Atasan"
placeholder="Pilih atasan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
data={
pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []
}
value={form.atasanId}
onChange={(val) => setForm({ ...form, atasanId: val || '' })}
onChange={val => handleChange('atasanId', val || '')}
/>
<Select
label="Bawahan"
placeholder="Pilih bawahan"
searchable
data={pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []}
data={
pegawaiList?.map(p => ({
value: p.id,
label: p.namaLengkap,
})) || []
}
value={form.bawahanId}
onChange={(val) => setForm({ ...form, bawahanId: val || '' })}
onChange={val => handleChange('bawahanId', val || '')}
/>
<TextInput
label="Tipe"
placeholder="Contoh: langsung_melapor"
defaultValue={form.tipe}
onChange={(e) => setForm({ ...form, tipe: e.currentTarget.value })}
value={form.tipe}
onChange={e => handleChange('tipe', e.currentTarget.value)}
/>
<Button onClick={handleSubmit} color="blue">Simpan</Button>
<Button onClick={handleSubmit} color="blue">
Simpan
</Button>
</Stack>
</Paper>
</Box>

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
@@ -41,20 +42,28 @@ interface PegawaiFormData {
export default function EditPegawai() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const [previewImage, setPreviewImage] = useState<PreviewImage | string | null>(null);
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const [previewImage, setPreviewImage] = useState<PreviewImage | string | null>(null);
const [formData, setFormData] = useState<PegawaiFormData>({
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
namaLengkap: '',
gelarAkademik: '',
imageId: '',
tanggalMasuk: '',
email: '',
telepon: '',
alamat: '',
posisiId: '',
isActive: true,
});
// helper buat update state formData
const updateForm = (field: keyof PegawaiFormData, value: any) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
// Format date to YYYY-MM-DD for date input
const formatDateForInput = (dateString: string) => {
@@ -65,32 +74,29 @@ export default function EditPegawai() {
useEffect(() => {
strukturorganisasiState.posisiOrganisasi.findMany.load();
const loadPegawai = async () => {
try {
const data = await stateOrganisasi.edit.load(id);
if (data) {
setFormData({
namaLengkap: data.namaLengkap || "",
gelarAkademik: data.gelarAkademik || "",
imageId: data.imageId || "",
tanggalMasuk: data.tanggalMasuk || "",
email: data.email || "",
telepon: data.telepon || "",
alamat: data.alamat || "",
posisiId: data.posisiId || "",
isActive: data.isActive ?? true, // pakai nullish coalescing
namaLengkap: data.namaLengkap || '',
gelarAkademik: data.gelarAkademik || '',
imageId: data.imageId || '',
tanggalMasuk: data.tanggalMasuk || '',
email: data.email || '',
telepon: data.telepon || '',
alamat: data.alamat || '',
posisiId: data.posisiId || '',
isActive: data.isActive ?? true,
});
if (data.image?.link) {
setPreviewImage(data.image.link);
} else {
setPreviewImage(null);
}
setPreviewImage(data.image?.link || null);
}
} catch (error) {
console.error("Error loading pegawai:", error);
console.error('Error loading pegawai:', error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pegawai"
error instanceof Error ? error.message : 'Gagal mengambil data pegawai'
);
}
};
@@ -106,18 +112,16 @@ export default function EditPegawai() {
}
stateOrganisasi.edit.form = {
...formData,
namaLengkap: formData.namaLengkap.trim(),
gelarAkademik: formData.gelarAkademik.trim(),
imageId: formData.imageId ? formData.imageId.trim() : "",
imageId: formData.imageId ? formData.imageId.trim() : '',
tanggalMasuk: formData.tanggalMasuk.trim(),
email: formData.email.trim(),
telepon: formData.telepon.trim(),
alamat: formData.alamat.trim(),
posisiId: formData.posisiId.trim(),
isActive: formData.isActive,
};
if (id && !stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id;
@@ -126,67 +130,90 @@ export default function EditPegawai() {
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
router.push(
'/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai'
);
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pegawai");
console.error('Error updating pegawai:', error);
toast.error(
error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'
);
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<Button onClick={() => router.back()} variant="subtle" color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Stack gap={'xs'}>
<Title order={4}>Edit Data Pegawai</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
defaultValue={formData.namaLengkap}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
value={formData.namaLengkap}
onChange={(e) => updateForm('namaLengkap', e.target.value)}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
defaultValue={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
value={formData.gelarAkademik}
onChange={(e) => updateForm('gelarAkademik', e.target.value)}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Text fz={'md'} fw={'bold'}>
Gambar
</Text>
<Box>
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
const file = files[0];
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
preview: URL.createObjectURL(file),
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
maxSize={5 * 1024 ** 2}
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
'image/*': ['.jpeg', '.jpg', '.png', '.webp'],
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: 'none' }}
>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload
size={52}
color="var(--mantine-color-blue-6)"
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX
size={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle>
<div>
@@ -194,14 +221,20 @@ export default function EditPegawai() {
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
Attach as many files as you like, each file should not
exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Image
src={typeof previewImage === 'string' ? previewImage : previewImage?.preview}
src={
typeof previewImage === 'string'
? previewImage
: previewImage?.preview
}
alt="Preview"
width={280}
height={180}
@@ -213,70 +246,70 @@ export default function EditPegawai() {
)}
</Box>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
defaultValue={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => updateForm('tanggalMasuk', e.target.value)}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
defaultValue={formData.email}
onChange={(e) => (formData.email = e.currentTarget.value)}
value={formData.email}
onChange={(e) => updateForm('email', e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
defaultValue={formData.telepon}
onChange={(e) => (formData.telepon = e.currentTarget.value)}
value={formData.telepon}
onChange={(e) => updateForm('telepon', e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={formData.alamat}
onChange={(e) => (formData.alamat = e.currentTarget.value)}
value={formData.alamat}
onChange={(e) => updateForm('alamat', e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={
strukturorganisasiState.posisiOrganisasi.findMany.data?.map((p) => ({
value: p.id, // harus string
label: p.nama,
})) || []
strukturorganisasiState.posisiOrganisasi.findMany.data?.map(
(p) => ({
value: p.id,
label: p.nama,
})
) || []
}
value={formData.posisiId}
onChange={(value) => {
if (value !== null) {
setFormData({ ...formData, posisiId: value }); // value harus string
}
if (value !== null) updateForm('posisiId', value);
}}
/>
<Select
label="Status Pegawai"
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)} // 'true' atau 'false'
onChange={(val) => {
setFormData({ ...formData, isActive: val === 'true' });
}}
value={String(formData.isActive)}
onChange={(val) => updateForm('isActive', val === 'true')}
/>
<Group>
<Button
onClick={handleSubmit}
color="blue"
>
<Button onClick={handleSubmit} color="blue">
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box >
</Box>
);
}

View File

@@ -32,6 +32,7 @@ function EditPosisiOrganisasi() {
hierarki: 0,
});
// Load data awal sekali saja
useEffect(() => {
const loadPosisiOrganisasi = async () => {
if (!id) return;
@@ -42,9 +43,9 @@ function EditPosisiOrganisasi() {
if (data) {
stateOrganisasi.edit.id = id;
setFormData({
nama: data.nama || '',
deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0,
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
hierarki: data.hierarki ?? 0,
});
}
} catch (error) {
@@ -56,6 +57,10 @@ function EditPosisiOrganisasi() {
loadPosisiOrganisasi();
}, [id]);
const handleChange = (field: string, value: string | number) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
@@ -63,6 +68,7 @@ function EditPosisiOrganisasi() {
return;
}
// update global state HANYA saat submit
stateOrganisasi.edit.form = {
nama: formData.nama.trim(),
deskripsi: formData.deskripsi.trim(),
@@ -114,10 +120,8 @@ function EditPosisiOrganisasi() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.nama}
onChange={(e) =>
setFormData({ ...formData, nama: e.target.value })
}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label="Nama Posisi Organisasi"
placeholder="Masukkan nama posisi organisasi"
required
@@ -130,19 +134,16 @@ function EditPosisiOrganisasi() {
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData({ ...formData, deskripsi: htmlContent })
handleChange('deskripsi', htmlContent)
}
/>
</Box>
<TextInput
type="number"
defaultValue={formData.hierarki}
value={formData.hierarki}
onChange={(e) =>
setFormData({
...formData,
hierarki: parseInt(e.target.value) || 0,
})
handleChange('hierarki', parseInt(e.target.value) || 0)
}
label="Hierarki"
placeholder="Masukkan hierarki"

View File

@@ -12,18 +12,22 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPenghargaan() {
const stateDesaDigital = useProxy(desaDigitalState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const [formData, setFormData] = useState({
name: stateDesaDigital.findUnique.data?.name || '',
deskripsi: stateDesaDigital.findUnique.data?.deskripsi || '',
imageId: stateDesaDigital.findUnique.data?.imageId || '',
})
function EditDigitalSmartVillage() {
const stateDesaDigital = useProxy(desaDigitalState);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// ✅ hanya lokal state untuk form
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
imageId: '',
});
// load data sekali saat mount
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
@@ -53,12 +57,11 @@ function EditPenghargaan() {
const handleSubmit = async () => {
try {
// ✅ update global state hanya saat submit
stateDesaDigital.edit.form = {
...stateDesaDigital.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
...formData,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
@@ -78,7 +81,7 @@ function EditPenghargaan() {
console.error("Error updating desa digital smart village:", error);
toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
}
}
};
return (
<Box>
@@ -87,78 +90,80 @@ function EditPenghargaan() {
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Desa Digital Smart Village</Title>
{/* ✅ controlled input */}
<TextInput
defaultValue={formData.name}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
{/* ✅ controlled editor */}
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateDesaDigital.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
@@ -170,4 +175,4 @@ function EditPenghargaan() {
);
}
export default EditPenghargaan;
export default EditDigitalSmartVillage;

View File

@@ -30,17 +30,19 @@ function EditInfoTeknologiTepatGuna() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: stateInfoTekno.findUnique.data?.name || '',
deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '',
imageId: stateInfoTekno.findUnique.data?.imageId || '',
name: '',
deskripsi: '',
imageId: '',
});
// Load data pertama kali
useEffect(() => {
const id = params?.id as string;
if (!id) return;
const loadPenghargaan = async () => {
const loadData = async () => {
try {
const data = await stateInfoTekno.edit.load(id);
if (data) {
@@ -58,16 +60,19 @@ function EditInfoTeknologiTepatGuna() {
}
};
loadPenghargaan();
loadData();
}, [params?.id]);
// Submit form
const handleSubmit = async () => {
try {
// sync local → global pas submit
stateInfoTekno.edit.form = {
...stateInfoTekno.edit.form,
...formData,
};
// upload file kalau ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -119,8 +124,8 @@ function EditInfoTeknologiTepatGuna() {
<TextInput
label="Judul"
placeholder="Masukkan judul info teknologi tepat guna"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required
/>
@@ -188,10 +193,9 @@ function EditInfoTeknologiTepatGuna() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateInfoTekno.edit.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>

View File

@@ -27,14 +27,14 @@ function EditKolaborasiInovasi() {
const params = useParams();
const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || "",
deskripsi: kolaborasiState.update.form.deskripsi || "",
tahun: kolaborasiState.update.form.tahun || "",
slug: kolaborasiState.update.form.slug || "",
kolaborator: kolaborasiState.update.form.kolaborator || "",
name: "",
deskripsi: "",
tahun: "",
slug: "",
kolaborator: "",
});
// Load data
// Load data awal dari server
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
@@ -44,11 +44,11 @@ function EditKolaborasiInovasi() {
const data = await kolaborasiState.update.load(id);
if (data) {
setFormData({
name: data.name || "",
deskripsi: data.deskripsi || "",
tahun: data.tahun || "",
slug: data.slug || "",
kolaborator: data.kolaborator || "",
name: data.name ?? "",
deskripsi: data.deskripsi ?? "",
tahun: data.tahun?.toString() ?? "",
slug: data.slug ?? "",
kolaborator: data.kolaborator ?? "",
});
}
} catch (error) {
@@ -60,6 +60,7 @@ function EditKolaborasiInovasi() {
loadKolaborasi();
}, [params?.id]);
// Handler submit → baru update global state
const handleSubmit = async () => {
try {
kolaborasiState.update.form = {
@@ -70,6 +71,7 @@ function EditKolaborasiInovasi() {
slug: formData.slug,
kolaborator: formData.kolaborator,
};
await kolaborasiState.update.submit();
toast.success("Kolaborasi inovasi berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi");
@@ -79,6 +81,11 @@ function EditKolaborasiInovasi() {
}
};
// Handler input (biar lebih DRY)
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md">
@@ -104,32 +111,32 @@ function EditKolaborasiInovasi() {
<TextInput
label="Nama Kolaborasi"
placeholder="Masukkan nama kolaborasi"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
defaultValue={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
value={formData.slug}
onChange={(e) => handleChange("slug", e.target.value)}
required
/>
<TextInput
label="Tahun"
placeholder="Masukkan tahun"
defaultValue={formData.tahun}
onChange={(e) => setFormData({ ...formData, tahun: e.target.value })}
value={formData.tahun}
onChange={(e) => handleChange("tahun", e.target.value)}
required
/>
<TextInput
label="Kolaborator"
placeholder="Masukkan nama kolaborator"
defaultValue={formData.kolaborator}
onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })}
value={formData.kolaborator}
onChange={(e) => handleChange("kolaborator", e.target.value)}
required
/>
@@ -139,10 +146,7 @@ function EditKolaborasiInovasi() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
kolaborasiState.update.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) => handleChange("deskripsi", htmlContent)}
/>
</Box>

View File

@@ -33,17 +33,22 @@ function EditMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// Local form state (controlled)
const [formData, setFormData] = useState({
name: state.update.form.name || '',
imageId: state.update.form.imageId || '',
name: '',
imageId: '',
});
// Load data ke state lokal sekali saja
useEffect(() => {
const loadFoto = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
@@ -51,25 +56,31 @@ function EditMitraKolaborasi() {
name: data.name || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading foto:', error);
toast.error('Gagal memuat data foto');
console.error('Error loading data:', error);
toast.error('Gagal memuat data mitra');
}
};
loadFoto();
loadData();
}, [params?.id]);
const handleChange = (key: string, value: string) => {
setFormData((prev) => ({
...prev,
[key]: value,
}));
};
const handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
name: formData.name,
imageId: formData.imageId,
};
// upload file jika ada
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
@@ -79,8 +90,16 @@ function EditMitraKolaborasi() {
if (!uploaded?.id) {
return toast.error('Gagal upload gambar');
}
state.update.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// update global state hanya saat submit
state.update.form = {
...state.update.form,
name: formData.name,
imageId,
};
await state.update.update();
toast.success('Mitra berhasil diperbarui!');
router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
@@ -118,8 +137,8 @@ function EditMitraKolaborasi() {
<TextInput
label="Nama Mitra"
placeholder="Masukkan nama mitra"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>

View File

@@ -11,24 +11,28 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditJenisLayanan() {
const state = useProxy(layananonlineDesa.jenisLayanan)
const router = useRouter()
const params = useParams()
const [formData, setFormData] = useState({
nama: state.edit.form.nama,
deskripsi: state.edit.form.deskripsi,
})
const state = useProxy(layananonlineDesa.jenisLayanan);
const router = useRouter();
const params = useParams();
// state lokal untuk form
const [formData, setFormData] = useState({
nama: "",
deskripsi: "",
});
// load data dari backend ke local state
useEffect(() => {
const loadJenisLayanan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.edit.load(id);
if (data) {
setFormData({
nama: data.nama,
deskripsi: data.deskripsi,
nama: data.nama ?? "",
deskripsi: data.deskripsi ?? "",
});
}
} catch (error) {
@@ -36,24 +40,26 @@ function EditJenisLayanan() {
toast.error("Gagal memuat data jenis layanan");
}
};
loadJenisLayanan();
}, [params?.id]);
// submit update → baru sync ke global state
const handleSubmit = async () => {
try {
state.edit.form = {
...state.edit.form,
nama: formData.nama,
deskripsi: formData.deskripsi,
}
await state.edit.update()
toast.success("Jenis layanan berhasil diperbarui!")
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan")
...formData,
};
await state.edit.update();
toast.success("Jenis layanan berhasil diperbarui!");
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan");
} catch (error) {
console.error("Error updating jenis layanan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis layanan");
}
}
};
return (
<Box>
@@ -62,27 +68,33 @@ function EditJenisLayanan() {
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Paper bg={colors["white-1"]} p="md" w={{ base: "100%", md: "50%" }}>
<Stack gap="xs">
<Title order={3}>Edit Jenis Layanan</Title>
<TextInput
defaultValue={formData.nama}
onChange={(val) => {
setFormData({ ...formData, nama: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Jenis Layanan</Text>}
value={formData.nama}
onChange={(e) =>
setFormData((prev) => ({ ...prev, nama: e.target.value }))
}
label={<Text fz="sm" fw="bold">Nama Jenis Layanan</Text>}
placeholder="masukkan nama jenis layanan"
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>
</Stack>
</Paper>
</Box>

View File

@@ -10,7 +10,7 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -25,9 +25,10 @@ function EditJenisPengaduan() {
const state = useProxy(layananonlineDesa.jenisPengaduan);
const [formData, setFormData] = useState({
nama: "",
nama: '',
});
// Load data sekali aja
useEffect(() => {
const loadJenisPengaduan = async () => {
if (!id) return;
@@ -36,51 +37,58 @@ function EditJenisPengaduan() {
const data = await state.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
state.edit.id = id;
state.edit.id = id; // inject id ke state global (hanya sekali)
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading jenis pengaduan:", error);
toast.error("Gagal memuat data jenis pengaduan");
console.error('Error loading jenis pengaduan:', error);
toast.error('Gagal memuat data jenis pengaduan');
}
};
loadJenisPengaduan();
}, [id]);
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
if (!formData.nama.trim()) {
toast.error('Nama jenis pengaduan tidak boleh kosong');
return;
}
// Update ke global state HANYA pas submit
state.edit.form = {
nama: formData.nama.trim(),
};
// Safety fallback kalau ID belum ada
if (!state.edit.id) {
state.edit.id = id;
}
try {
if (!formData.nama.trim()) {
toast.error('Nama jenis pengaduan tidak boleh kosong');
return;
}
state.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!state.edit.id) {
state.edit.id = id; // fallback
}
const success = await state.edit.update();
if (success) {
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan");
router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
}
} catch (error) {
console.error("Error updating jenis pengaduan:", error);
// toast akan ditampilkan dari fungsi update
console.error('Error updating jenis pengaduan:', error);
// toast ditangani di dalam state.update
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header + tombol back */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -97,7 +105,7 @@ function EditJenisPengaduan() {
</Title>
</Group>
{/* Card Form */}
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
@@ -108,8 +116,8 @@ function EditJenisPengaduan() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
required

View File

@@ -41,6 +41,7 @@ function EditProgramKreatifDesa() {
icon: '',
});
// Load data hanya sekali berdasarkan params.id
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
@@ -50,17 +51,11 @@ function EditProgramKreatifDesa() {
const data = await stateProgramKreatif.update.load(id);
if (data) {
stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = {
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
name: data.name || '',
slug: data.slug || '',
deskripsi: data.deskripsi || '',
icon: data.icon || '',
});
}
} catch (error) {
@@ -72,10 +67,15 @@ function EditProgramKreatifDesa() {
loadProgramKreatif();
}, [params?.id]);
const handleChange =
(field: keyof FormProgramKreatif) =>
(value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
stateProgramKreatif.update.form = {
...stateProgramKreatif.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
@@ -85,7 +85,7 @@ function EditProgramKreatifDesa() {
router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) {
console.error('Error updating program kreatif:', error);
toast.error('Gagal memuat data program kreatif');
toast.error('Gagal menyimpan program kreatif');
}
};
@@ -93,7 +93,12 @@ function EditProgramKreatifDesa() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -114,16 +119,16 @@ function EditProgramKreatifDesa() {
<TextInput
label="Nama Program Kreatif Desa"
placeholder="Masukkan nama program kreatif desa"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name')(e.target.value)}
required
/>
<TextInput
label="Deskripsi Singkat Program Kreatif Desa"
placeholder="Masukkan deskripsi singkat program kreatif desa"
defaultValue={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
value={formData.slug}
onChange={(e) => handleChange('slug')(e.target.value)}
required
/>
@@ -133,10 +138,7 @@ function EditProgramKreatifDesa() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateProgramKreatif.update.form.deskripsi = htmlContent;
}}
onChange={handleChange('deskripsi')}
/>
</Box>
@@ -146,10 +148,7 @@ function EditProgramKreatifDesa() {
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramKreatif.update.form.icon = value;
}}
onChange={handleChange('icon')}
/>
</Box>

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import {
@@ -40,12 +39,12 @@ function EditKeamananLingkungan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: keamananState.edit.form.name || "",
deskripsi: keamananState.edit.form.deskripsi || "",
imageId: keamananState.edit.form.imageId || "",
name: "",
deskripsi: "",
imageId: "",
});
// Load data by id
// Load data sekali pas mount
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
@@ -71,16 +70,16 @@ function EditKeamananLingkungan() {
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
keamananState.edit.form = {
...keamananState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
};
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
@@ -93,9 +92,17 @@ function EditKeamananLingkungan() {
return toast.error("Gagal upload gambar");
}
keamananState.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// update global state hanya sekali pas submit
keamananState.edit.form = {
...keamananState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId,
};
await keamananState.edit.update();
toast.success("Keamanan Lingkungan berhasil diperbarui!");
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal");
@@ -186,7 +193,7 @@ function EditKeamananLingkungan() {
</Dropzone>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} loading="lazy"/>
<Image alt="" src={previewImage} w={200} h={200} loading="lazy" />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
@@ -194,10 +201,8 @@ function EditKeamananLingkungan() {
)}
<TextInput
defaultValue={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
label="Judul Keamanan Lingkungan"
placeholder="Masukkan judul"
required
@@ -209,10 +214,7 @@ function EditKeamananLingkungan() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
keamananState.edit.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) => handleChange("deskripsi", htmlContent)}
/>
</Box>

View File

@@ -27,11 +27,12 @@ function EditKontakItem() {
const params = useParams();
const [formData, setFormData] = useState({
name: kontakState.update.form.nama || '',
nomorTelepon: kontakState.update.form.nomorTelepon || '',
icon: kontakState.update.form.icon || '',
name: '',
nomorTelepon: '',
icon: '',
});
// Load data sekali dari global state
useEffect(() => {
const loadKontakDarurat = async () => {
const id = params?.id as string;
@@ -55,14 +56,23 @@ function EditKontakItem() {
loadKontakDarurat();
}, [params?.id]);
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => {
try {
// Update global state sekali pas submit
kontakState.update.form = {
...kontakState.update.form,
nama: formData.name,
nomorTelepon: formData.nomorTelepon,
icon: formData.icon,
};
await kontakState.update.update();
toast.success('Kontak Darurat berhasil diperbarui!');
router.push('/admin/keamanan/kontak-darurat/kontak-darurat-item');
@@ -99,16 +109,16 @@ function EditKontakItem() {
<TextInput
label="Nama Kontak"
placeholder="Masukkan nama kontak"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
required
/>
<TextInput
label="Nomor Telepon"
placeholder="Masukkan nomor telepon"
defaultValue={formData.nomorTelepon}
onChange={(e) => setFormData({ ...formData, nomorTelepon: e.target.value })}
value={formData.nomorTelepon}
onChange={(e) => handleChange('nomorTelepon', e.target.value)}
required
/>
@@ -118,10 +128,7 @@ function EditKontakItem() {
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
kontakState.update.form.icon = value;
}}
onChange={(value) => handleChange('icon', value)}
/>
</Box>

View File

@@ -15,40 +15,40 @@ import {
Text,
TextInput,
Title,
Tooltip
Tooltip,
} from "@mantine/core";
import {
IconArrowBack
} from "@tabler/icons-react";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditKontakDaruratKeamanan() {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const params = useParams();
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const [isLoading, setIsLoading] = useState(true);
const [formData, setFormData] = useState({
name: kontakState.update.form.nama || "",
icon: kontakState.update.form.icon || "",
kategoriId: kontakState.update.form.kategoriId || [],
name: "",
icon: "" as IconKey | "",
kategoriId: [] as string[],
});
// Load data
// Load data dari backend
useEffect(() => {
const loadData = async () => {
try {
setIsLoading(true);
await kontakDarurat.kontakDaruratItem.findMany.load();
const id = params?.id as string;
if (id) {
const data = await kontakState.update.load(id);
if (data) {
setFormData({
name: data.nama || "",
icon: data.icon || "",
icon: (data.icon as IconKey) || "",
kategoriId: data.kategoriId || [],
});
}
@@ -63,6 +63,7 @@ function EditKontakDaruratKeamanan() {
loadData();
}, [params?.id]);
// Handle submit
const handleSubmit = async () => {
try {
@@ -72,6 +73,7 @@ function EditKontakDaruratKeamanan() {
icon: formData.icon,
kategoriId: formData.kategoriId,
};
await kontakState.update.update();
toast.success("Kontak Darurat berhasil diperbarui!");
router.push("/admin/keamanan/kontak-darurat");
@@ -110,43 +112,54 @@ function EditKontakDaruratKeamanan() {
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Nama kategori */}
{/* Nama Kontak */}
<TextInput
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
label="Nama Kontak Darurat"
placeholder="Masukkan nama kontak darurat"
required
/>
{/* MultiSelect */}
<MultiSelect
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })}
onChange={(val) =>
setFormData((prev) => ({ ...prev, kategoriId: val }))
}
label={<Text fw={"bold"} fz={"sm"}>Kontak Item</Text>}
placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"}
data={
Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data)
? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({
value: v.id,
label: v.nama
}))
value: v.id,
label: v.nama,
}))
: []
}
clearable
searchable
required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
error={
!formData.kategoriId.length
? "Pilih minimal satu kategori"
: undefined
}
disabled={isLoading}
/>
{/* Icon Select */}
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<Text fz={"sm"} fw={"bold"}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
kontakState.update.form.icon = value;
}}
onChange={(value) =>
setFormData((prev) => ({ ...prev, icon: value }))
}
/>
</Box>

View File

@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
@@ -30,13 +28,20 @@ function EditLaporanPublik() {
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
judul: stateLaporan.edit.form.judul || '',
lokasi: stateLaporan.edit.form.lokasi || '',
tanggalWaktu: stateLaporan.edit.form.tanggalWaktu || '',
status: stateLaporan.edit.form.status || '',
penanganan: stateLaporan.edit.form.penanganan || '',
kronologi: stateLaporan.edit.form.kronologi || '',
const [formData, setFormData] = useState<{
judul: string;
lokasi: string;
tanggalWaktu: string;
status: Status;
penanganan: string;
kronologi: string;
}>({
judul: '',
lokasi: '',
tanggalWaktu: '',
status: 'Proses', // Default status
penanganan: '',
kronologi: '',
});
useEffect(() => {
@@ -52,7 +57,7 @@ function EditLaporanPublik() {
lokasi: data.lokasi || '',
tanggalWaktu: data.tanggalWaktu || '',
status: data.status || '',
penanganan: data.penanganan?.map((p: any) => p.deskripsi)[0] || '',
penanganan: data.penanganan?.[0]?.deskripsi || '',
kronologi: data.kronologi || '',
});
}
@@ -63,18 +68,17 @@ function EditLaporanPublik() {
};
loadLaporanPublik();
}, [params?.id]);
}, [params?.id, stateLaporan.edit]);
const handleChange = (field: string, value: string | Status) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
stateLaporan.edit.form = {
...stateLaporan.edit.form,
judul: formData.judul,
lokasi: formData.lokasi,
tanggalWaktu: formData.tanggalWaktu,
status: formData.status,
penanganan: formData.penanganan,
kronologi: formData.kronologi,
...formData,
};
await stateLaporan.edit.update();
@@ -111,16 +115,16 @@ function EditLaporanPublik() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
value={formData.judul}
onChange={(e) => handleChange('judul', e.target.value)}
label={<Text fw="bold" fz="sm">Judul Laporan Publik</Text>}
placeholder="Masukkan judul laporan publik"
required
/>
<TextInput
defaultValue={formData.lokasi}
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
value={formData.lokasi}
onChange={(e) => handleChange('lokasi', e.target.value)}
label={<Text fw="bold" fz="sm">Lokasi Laporan Publik</Text>}
placeholder="Masukkan lokasi laporan publik"
required
@@ -129,15 +133,20 @@ function EditLaporanPublik() {
<DateTimePicker
label="Tanggal & Waktu Laporan Publik"
value={formData.tanggalWaktu ? new Date(formData.tanggalWaktu) : null}
onChange={(val) =>
setFormData({ ...formData, tanggalWaktu: val ? val.toString() : '' })
}
onChange={(value: string | null) => {
if (value) {
const date = new Date(value);
handleChange('tanggalWaktu', date.toISOString());
} else {
handleChange('tanggalWaktu', '');
}
}}
required
/>
<Select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e as Status })}
onChange={(val) => handleChange('status', val as Status)}
label={<Text fw="bold" fz="sm">Status Laporan Publik</Text>}
placeholder="Pilih status laporan publik"
data={[
@@ -152,10 +161,7 @@ function EditLaporanPublik() {
<Text fw="bold" fz="sm" mb={6}>Kronologi Laporan Publik</Text>
<EditEditor
value={formData.kronologi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, kronologi: htmlContent }));
stateLaporan.edit.form.kronologi = htmlContent;
}}
onChange={(htmlContent) => handleChange('kronologi', htmlContent)}
/>
</Box>
@@ -163,10 +169,7 @@ function EditLaporanPublik() {
<Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text>
<EditEditor
value={formData.penanganan}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, penanganan: htmlContent }));
stateLaporan.edit.form.penanganan = htmlContent;
}}
onChange={(htmlContent) => handleChange('penanganan', htmlContent)}
/>
</Box>

View File

@@ -12,7 +12,7 @@ import {
Stack,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -32,6 +32,7 @@ function EditPencegahanKriminalitas() {
linkVideo: '',
});
// load data hanya sekali pas id berubah
useEffect(() => {
const loadKriminalitas = async () => {
const id = params?.id as string;
@@ -41,10 +42,10 @@ function EditPencegahanKriminalitas() {
const data = await kriminalitasState.update.load(id);
if (data) {
setFormData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
deskripsiSingkat: data.deskripsiSingkat || '',
linkVideo: data.linkVideo || '',
judul: data.judul ?? '',
deskripsi: data.deskripsi ?? '',
deskripsiSingkat: data.deskripsiSingkat ?? '',
linkVideo: data.linkVideo ?? '',
});
}
} catch (error) {
@@ -58,6 +59,12 @@ function EditPencegahanKriminalitas() {
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
const handleChange =
(field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({ ...prev, [field]: e.target.value }));
};
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
@@ -66,16 +73,13 @@ function EditPencegahanKriminalitas() {
}
try {
// Update the form data first
// update global state saat submit
kriminalitasState.update.form = {
...kriminalitasState.update.form,
judul: formData.judul,
deskripsi: formData.deskripsi,
deskripsiSingkat: formData.deskripsiSingkat,
linkVideo: formData.linkVideo,
};
// Set the ID and then call update
kriminalitasState.update.id = params?.id as string;
await kriminalitasState.update.update();
@@ -119,18 +123,16 @@ function EditPencegahanKriminalitas() {
<TextInput
label="Judul"
placeholder="Masukkan judul"
defaultValue={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
value={formData.judul}
onChange={handleChange('judul')}
required
/>
<TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
defaultValue={formData.deskripsiSingkat}
onChange={(e) =>
setFormData({ ...formData, deskripsiSingkat: e.target.value })
}
value={formData.deskripsiSingkat}
onChange={handleChange('deskripsiSingkat')}
required
/>
@@ -141,7 +143,7 @@ function EditPencegahanKriminalitas() {
<EditEditor
value={formData.deskripsi}
onChange={(val) =>
setFormData({ ...formData, deskripsi: val })
setFormData((prev) => ({ ...prev, deskripsi: val }))
}
/>
</Box>
@@ -150,10 +152,8 @@ function EditPencegahanKriminalitas() {
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
defaultValue={formData.linkVideo}
onChange={(e) =>
setFormData({ ...formData, linkVideo: e.currentTarget.value })
}
value={formData.linkVideo}
onChange={handleChange('linkVideo')}
required
/>
{embedLink && (

View File

@@ -52,50 +52,36 @@ function EditPolsekTerdekat() {
layananPolsekId: "",
});
useEffect(() => {
const loadPolsekTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await polsekState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.layananPolsekId || "",
});
// load data untuk form edit
useEffect(() => {
const loadPolsekTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await polsekState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.layananPolsekId || "",
});
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
};
loadPolsekTerdekat();
}, [params?.id]);
const handleSubmit = async () => {
try {
polsekState.edit.form = {
...polsekState.edit.form,
...formData,
};
await polsekState.edit.update();
toast.success("Polsek terdekat berhasil diperbarui!");
router.push("/admin/keamanan/polsek-terdekat");
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error("Gagal memperbarui data polsek terdekat");
}
};
loadPolsekTerdekat();
}, [params?.id]);
const fetchLayanan = async () => {
try {
@@ -198,6 +184,22 @@ function EditPolsekTerdekat() {
fetchLayanan();
}, []);
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
polsekState.edit.form = { ...formData }; // update global state hanya di sini
await polsekState.edit.update();
toast.success("Polsek terdekat berhasil diperbarui!");
router.push("/admin/keamanan/polsek-terdekat");
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error("Gagal memperbarui data polsek terdekat");
}
};
return (
<Box px={{ base: "sm", md: "lg" }} py="md">
{/* Modal Tambah */}
@@ -273,88 +275,59 @@ function EditPolsekTerdekat() {
<Stack gap="md">
{/* Input fields */}
<TextInput
defaultValue={formData.nama}
onChange={(val) =>
setFormData({ ...formData, nama: val.target.value })
}
value={formData.nama}
onChange={(e) => handleChange("nama", e.currentTarget.value)}
label="Nama Polsek Terdekat"
placeholder="Masukkan nama Polsek Terdekat"
required
/>
<TextInput
defaultValue={formData.jarakKeDesa}
onChange={(val) =>
setFormData({ ...formData, jarakKeDesa: val.target.value })
}
value={formData.jarakKeDesa}
onChange={(e) => handleChange("jarakKeDesa", e.currentTarget.value)}
label="Jarak Polsek Terdekat"
placeholder="Masukkan jarak Polsek Terdekat"
/>
<TextInput
defaultValue={formData.alamat}
onChange={(val) =>
setFormData({ ...formData, alamat: val.target.value })
}
value={formData.alamat}
onChange={(e) => handleChange("alamat", e.currentTarget.value)}
label="Alamat Polsek Terdekat"
placeholder="Masukkan alamat Polsek Terdekat"
/>
<TextInput
defaultValue={formData.nomorTelepon}
onChange={(val) =>
setFormData({ ...formData, nomorTelepon: val.target.value })
}
value={formData.nomorTelepon}
onChange={(e) => handleChange("nomorTelepon", e.currentTarget.value)}
label="Nomor Telepon"
placeholder="Masukkan nomor telepon Polsek Terdekat"
/>
<TextInput
defaultValue={formData.jamOperasional}
onChange={(val) =>
setFormData({ ...formData, jamOperasional: val.target.value })
}
value={formData.jamOperasional}
onChange={(e) => handleChange("jamOperasional", e.currentTarget.value)}
label="Jam Operasional"
placeholder="Masukkan jam operasional Polsek Terdekat"
/>
<TextInput
defaultValue={formData.embedMapUrl}
onChange={(val) =>
setFormData({ ...formData, embedMapUrl: val.target.value })
}
value={formData.embedMapUrl}
onChange={(e) => handleChange("embedMapUrl", e.currentTarget.value)}
label="Embed Map URL"
placeholder="Masukkan embed map url"
/>
<TextInput
defaultValue={formData.namaTempatMaps}
onChange={(val) =>
setFormData({ ...formData, namaTempatMaps: val.target.value })
}
value={formData.namaTempatMaps}
onChange={(e) => handleChange("namaTempatMaps", e.currentTarget.value)}
label="Nama Tempat Maps"
placeholder="Masukkan nama tempat di maps"
/>
<TextInput
defaultValue={formData.alamatMaps}
onChange={(val) =>
setFormData({ ...formData, alamatMaps: val.target.value })
}
value={formData.alamatMaps}
onChange={(e) => handleChange("alamatMaps", e.currentTarget.value)}
label="Alamat Maps"
placeholder="Masukkan alamat di maps"
/>
<TextInput
defaultValue={formData.linkPetunjukArah}
onChange={(val) =>
setFormData({ ...formData, linkPetunjukArah: val.target.value })
}
value={formData.linkPetunjukArah}
onChange={(e) => handleChange("linkPetunjukArah", e.currentTarget.value)}
label="Link Petunjuk Arah"
placeholder="Masukkan link petunjuk arah"
/>
{/* Dropdown Layanan */}
<Select
label="Layanan Polsek"
placeholder="Pilih layanan polsek"
data={layananOptions}
value={polsekState.create.form.layananPolsekId}
onChange={(val) => {
polsekState.create.form.layananPolsekId = val || "";
}}
value={formData.layananPolsekId}
onChange={(val) => handleChange("layananPolsekId", val || "")}
/>
<Button
variant="light"

View File

@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import {
@@ -39,9 +38,9 @@ function EditTipsKeamanan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
judul: keamananState.update.form.judul || "",
deskripsi: keamananState.update.form.deskripsi || "",
imageId: keamananState.update.form.imageId || "",
judul: "",
deskripsi: "",
imageId: "",
});
// Load data saat pertama kali
@@ -70,14 +69,12 @@ function EditTipsKeamanan() {
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]);
const handleSubmit = async () => {
try {
keamananState.update.form = {
...keamananState.update.form,
...formData,
};
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
@@ -87,10 +84,14 @@ function EditTipsKeamanan() {
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error("Gagal upload gambar");
keamananState.update.form.imageId = uploaded.id;
imageId = uploaded.id;
}
keamananState.update.form = {
...formData,
imageId,
};
await keamananState.update.update();
toast.success("Tips Keamanan berhasil diperbarui!");
router.push("/admin/keamanan/tips-keamanan");
@@ -105,7 +106,12 @@ function EditTipsKeamanan() {
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
@@ -137,7 +143,9 @@ function EditTipsKeamanan() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
radius="md"
@@ -145,7 +153,11 @@ function EditTipsKeamanan() {
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
<IconUpload
size={48}
color={colors["blue-button"]}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
@@ -199,8 +211,10 @@ function EditTipsKeamanan() {
<TextInput
label="Nama Tips Keamanan"
placeholder="Masukkan nama tips keamanan"
defaultValue={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
value={formData.judul}
onChange={(e) =>
setFormData((prev) => ({ ...prev, judul: e.target.value }))
}
required
/>
@@ -211,10 +225,9 @@ function EditTipsKeamanan() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
keamananState.update.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
@@ -15,7 +15,7 @@ import {
Text,
TextInput,
Title,
Tooltip
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -44,32 +44,18 @@ function EditArtikelKesehatan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({
title: stateArtikelKesehatan.edit.form.title,
content: stateArtikelKesehatan.edit.form.content,
imageId: stateArtikelKesehatan.edit.form.imageId,
introduction: { content: stateArtikelKesehatan.edit.form.introduction?.content },
symptom: {
title: stateArtikelKesehatan.edit.form.symptom?.title,
content: stateArtikelKesehatan.edit.form.symptom?.content
},
prevention: {
title: stateArtikelKesehatan.edit.form.prevention?.title,
content: stateArtikelKesehatan.edit.form.prevention?.content
},
firstAid: {
title: stateArtikelKesehatan.edit.form.firstAid?.title,
content: stateArtikelKesehatan.edit.form.firstAid?.content
},
mythVsFact: {
title: stateArtikelKesehatan.edit.form.mythVsFact?.title,
mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos,
fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta
},
doctorSign: {
content: stateArtikelKesehatan.edit.form.doctorSign?.content
}
title: '',
content: '',
imageId: '',
introduction: { content: '' },
symptom: { title: '', content: '' },
prevention: { title: '', content: '' },
firstAid: { title: '', content: '' },
mythVsFact: { title: '', mitos: '', fakta: '' },
doctorSign: { content: '' },
});
// Load data artikel
useEffect(() => {
const loadArtikelKesehatan = async () => {
const id = params?.id as string;
@@ -78,74 +64,89 @@ function EditArtikelKesehatan() {
try {
await stateArtikelKesehatan.edit.load(id);
const { form } = stateArtikelKesehatan.edit;
if (form) {
setFormData({
title: form.title,
content: form.content,
introduction: { content: form.introduction?.content || '' },
imageId: form.imageId,
symptom: {
title: form.symptom?.title || '',
content: form.symptom?.content || ''
},
prevention: {
title: form.prevention?.title || '',
content: form.prevention?.content || ''
},
firstAid: {
title: form.firstAid?.title || '',
content: form.firstAid?.content || ''
},
mythVsFact: {
title: form.mythVsFact?.title || '',
mitos: form.mythVsFact?.mitos || '',
fakta: form.mythVsFact?.fakta || ''
},
doctorSign: {
content: form.doctorSign?.content || ''
}
});
if (!form) return;
if (form?.imageId) {
setPreviewImage(`${process.env.NEXT_PUBLIC_API_URL}/file/${form.imageId}`);
}
setFormData({
title: form.title || '',
content: form.content || '',
imageId: form.imageId || '',
introduction: { content: form.introduction?.content || '' },
symptom: { title: form.symptom?.title || '', content: form.symptom?.content || '' },
prevention: { title: form.prevention?.title || '', content: form.prevention?.content || '' },
firstAid: { title: form.firstAid?.title || '', content: form.firstAid?.content || '' },
mythVsFact: {
title: form.mythVsFact?.title || '',
mitos: form.mythVsFact?.mitos || '',
fakta: form.mythVsFact?.fakta || '',
},
doctorSign: { content: form.doctorSign?.content || '' },
});
if (form.imageId) {
setPreviewImage(`${process.env.NEXT_PUBLIC_API_URL}/file/${form.imageId}`);
}
} catch (error) {
console.error("Error loading artikel kesehatan:", error);
toast.error("Gagal memuat data artikel kesehatan");
console.error('Error loading artikel kesehatan:', error);
toast.error('Gagal memuat data artikel kesehatan');
}
};
loadArtikelKesehatan();
}, [params?.id]);
const handleFileChange = (files: File[]) => {
const selectedFile = files[0];
if (!selectedFile) return;
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
};
const handleSubmit = async () => {
try {
// Copy formData ke global state
stateArtikelKesehatan.edit.form = { ...formData };
// Upload gambar kalau ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
if (!uploaded?.id) return toast.error('Gagal upload gambar');
stateArtikelKesehatan.edit.form.imageId = uploaded.id;
}
const success = await stateArtikelKesehatan.edit.submit();
if (success) {
toast.success("Artikel kesehatan berhasil diperbarui!");
router.push("/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan");
toast.success('Artikel kesehatan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
}
} catch (error) {
console.error("Error updating artikel kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data artikel kesehatan");
console.error('Error updating artikel kesehatan:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data artikel kesehatan');
}
};
const InputText = ({
label,
value,
onChange,
placeholder,
required,
}: {
label: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
required?: boolean;
}) => (
<TextInput
label={label}
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
required={required}
/>
);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
@@ -170,34 +171,31 @@ function EditArtikelKesehatan() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
{/* Judul */}
<InputText
label="Judul"
value={formData.title}
onChange={(value) => setFormData((prev) => ({ ...prev, title: value }))}
placeholder="Masukkan judul artikel"
defaultValue={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
required
/>
{/* Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Berita
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
onDrop={handleFileChange}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
@@ -215,64 +213,57 @@ function EditArtikelKesehatan() {
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: "contain",
border: `1px solid ${colors["blue-button"]}`,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
<TextInput
{/* Konten */}
<InputText
label="Deskripsi"
value={formData.content}
onChange={(value) => setFormData((prev) => ({ ...prev, content: value }))}
placeholder="Masukkan deskripsi artikel"
defaultValue={formData.content}
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
required
/>
<TextInput
{/* Pendahuluan */}
<InputText
label="Pendahuluan"
placeholder="Masukkan pendahuluan"
defaultValue={formData.introduction.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
introduction: { ...prev.introduction, content: e.target.value }
}))
value={formData.introduction.content}
onChange={(value) =>
setFormData((prev) => ({ ...prev, introduction: { content: value } }))
}
placeholder="Masukkan pendahuluan"
/>
{/* Gejala */}
<Box>
<Text fw="bold">Gejala</Text>
<Stack gap="xs">
<TextInput
<InputText
label="Judul Gejala"
placeholder="Masukkan judul gejala"
defaultValue={formData.symptom.title}
onChange={(e) =>
setFormData(prev => ({
...prev,
symptom: { ...prev.symptom, title: e.target.value }
}))
value={formData.symptom.title}
onChange={(value) =>
setFormData((prev) => ({ ...prev, symptom: { ...prev.symptom, title: value } }))
}
/>
<EditEditor
value={formData.symptom.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
symptom: { ...prev.symptom, content: e }
}))
onChange={(value) =>
setFormData((prev) => ({ ...prev, symptom: { ...prev.symptom, content: value } }))
}
/>
</Stack>
@@ -281,23 +272,17 @@ function EditArtikelKesehatan() {
{/* Pencegahan */}
<Box>
<Text fw="bold">Pencegahan</Text>
<TextInput
<InputText
label="Judul"
defaultValue={formData.prevention.title}
onChange={(e) =>
setFormData(prev => ({
...prev,
prevention: { ...prev.prevention, title: e.target.value }
}))
value={formData.prevention.title}
onChange={(value) =>
setFormData((prev) => ({ ...prev, prevention: { ...prev.prevention, title: value } }))
}
/>
<EditEditor
value={formData.prevention.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
prevention: { ...prev.prevention, content: e }
}))
onChange={(value) =>
setFormData((prev) => ({ ...prev, prevention: { ...prev.prevention, content: value } }))
}
/>
</Box>
@@ -305,23 +290,17 @@ function EditArtikelKesehatan() {
{/* Pertolongan Pertama */}
<Box>
<Text fw="bold">Pertolongan Pertama</Text>
<TextInput
<InputText
label="Judul"
defaultValue={formData.firstAid.title}
onChange={(e) =>
setFormData(prev => ({
...prev,
firstAid: { ...prev.firstAid, title: e.target.value }
}))
value={formData.firstAid.title}
onChange={(value) =>
setFormData((prev) => ({ ...prev, firstAid: { ...prev.firstAid, title: value } }))
}
/>
<EditEditor
value={formData.firstAid.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
firstAid: { ...prev.firstAid, content: e }
}))
onChange={(value) =>
setFormData((prev) => ({ ...prev, firstAid: { ...prev.firstAid, content: value } }))
}
/>
</Box>
@@ -329,48 +308,36 @@ function EditArtikelKesehatan() {
{/* Mitos vs Fakta */}
<Box>
<Text fw="bold">Mitos vs Fakta</Text>
<TextInput
<InputText
label="Judul"
defaultValue={formData.mythVsFact.title}
onChange={(e) =>
setFormData(prev => ({
...prev,
mythVsFact: { ...prev.mythVsFact, title: e.target.value }
}))
value={formData.mythVsFact.title}
onChange={(value) =>
setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, title: value } }))
}
/>
<Text fw="500">Mitos</Text>
<EditEditor
value={formData.mythVsFact.mitos}
onChange={(e) =>
setFormData(prev => ({
...prev,
mythVsFact: { ...prev.mythVsFact, mitos: e }
}))
onChange={(value) =>
setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, mitos: value } }))
}
/>
<Text fw="500">Fakta</Text>
<EditEditor
value={formData.mythVsFact.fakta}
onChange={(e) =>
setFormData(prev => ({
...prev,
mythVsFact: { ...prev.mythVsFact, fakta: e }
}))
onChange={(value) =>
setFormData((prev) => ({ ...prev, mythVsFact: { ...prev.mythVsFact, fakta: value } }))
}
/>
</Box>
{/* Kapan harus ke dokter */}
{/* Dokter */}
<Box>
<Text fw="bold">Kapan Harus Ke Dokter</Text>
<EditEditor
value={formData.doctorSign.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
doctorSign: { ...prev.doctorSign, content: e }
}))
onChange={(value) =>
setFormData((prev) => ({ ...prev, doctorSign: { content: value } }))
}
/>
</Box>
@@ -384,7 +351,7 @@ function EditArtikelKesehatan() {
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)'
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
@@ -43,89 +43,64 @@ interface FasilitasKesehatanFormBase {
}
function EditFasilitasKesehatan() {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState<FasilitasKesehatanFormBase>({
name: stateFasilitasKesehatan.edit.form.name || '',
informasiUmum: {
fasilitas: stateFasilitasKesehatan.edit.form.informasiUmum?.fasilitas || '',
alamat: stateFasilitasKesehatan.edit.form.informasiUmum?.alamat || '',
jamOperasional: stateFasilitasKesehatan.edit.form.informasiUmum?.jamOperasional || '',
},
layananUnggulan: {
content: stateFasilitasKesehatan.edit.form.layananUnggulan?.content || '',
},
dokterdanTenagaMedis: {
name: stateFasilitasKesehatan.edit.form.dokterdanTenagaMedis?.name || '',
specialist: stateFasilitasKesehatan.edit.form.dokterdanTenagaMedis?.specialist || '',
jadwal: stateFasilitasKesehatan.edit.form.dokterdanTenagaMedis?.jadwal || '',
},
fasilitasPendukung: {
content: stateFasilitasKesehatan.edit.form.fasilitasPendukung?.content || '',
},
prosedurPendaftaran: {
content: stateFasilitasKesehatan.edit.form.prosedurPendaftaran?.content || '',
},
tarifDanLayanan: {
layanan: stateFasilitasKesehatan.edit.form.tarifDanLayanan?.layanan || '',
tarif: stateFasilitasKesehatan.edit.form.tarifDanLayanan?.tarif || '',
},
name: '',
informasiUmum: { fasilitas: '', alamat: '', jamOperasional: '' },
layananUnggulan: { content: '' },
dokterdanTenagaMedis: { name: '', specialist: '', jadwal: '' },
fasilitasPendukung: { content: '' },
prosedurPendaftaran: { content: '' },
tarifDanLayanan: { layanan: '', tarif: '' },
});
// Helper untuk update nested state
const updateForm = <K extends keyof FasilitasKesehatanFormBase>(
key: K,
value: FasilitasKesehatanFormBase[K]
) => setFormData(prev => ({ ...prev, [key]: value }));
const updateNested = <
K extends keyof FasilitasKesehatanFormBase,
N extends keyof FasilitasKesehatanFormBase[K]
>(key: K, nestedKey: N, value: FasilitasKesehatanFormBase[K][N]) =>
setFormData(prev => ({
...prev,
[key]: { ...prev[key] as object, [nestedKey]: value },
}));
// Load data
useEffect(() => {
const loadFasilitasKesehatan = async () => {
const load = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateFasilitasKesehatan.edit.load(id);
const { form } = stateFasilitasKesehatan.edit;
if (form) {
setFormData({
name: form.name,
informasiUmum: {
fasilitas: form.informasiUmum?.fasilitas || '',
alamat: form.informasiUmum?.alamat || '',
jamOperasional: form.informasiUmum?.jamOperasional || '',
},
layananUnggulan: { content: form.layananUnggulan?.content || '' },
dokterdanTenagaMedis: {
name: form.dokterdanTenagaMedis?.name || '',
specialist: form.dokterdanTenagaMedis?.specialist || '',
jadwal: form.dokterdanTenagaMedis?.jadwal || '',
},
fasilitasPendukung: { content: form.fasilitasPendukung?.content || '' },
prosedurPendaftaran: { content: form.prosedurPendaftaran?.content || '' },
tarifDanLayanan: {
layanan: form.tarifDanLayanan?.layanan || '',
tarif: form.tarifDanLayanan?.tarif || '',
},
});
}
} catch (error) {
console.error("Error loading fasilitas kesehatan:", error);
toast.error("Gagal memuat data fasilitas kesehatan");
await state.edit.load(id);
const form = state.edit.form;
if (form) setFormData(form as FasilitasKesehatanFormBase);
} catch (err) {
console.error(err);
toast.error('Gagal memuat data fasilitas kesehatan');
}
};
loadFasilitasKesehatan();
load();
}, [params?.id]);
// Submit
const handleSubmit = async () => {
try {
stateFasilitasKesehatan.edit.form = {
...stateFasilitasKesehatan.edit.form,
...formData,
};
const success = await stateFasilitasKesehatan.edit.submit();
state.edit.form = { ...state.edit.form, ...formData };
const success = await state.edit.submit();
if (success) {
toast.success("Fasilitas kesehatan berhasil diperbarui!");
router.push("/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan");
toast.success('Fasilitas kesehatan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan');
}
} catch (error) {
console.error("Error updating fasilitas kesehatan:", error);
toast.error("Terjadi kesalahan saat memperbarui data fasilitas kesehatan");
} catch (err) {
console.error(err);
toast.error('Terjadi kesalahan saat memperbarui data fasilitas kesehatan');
}
};
@@ -156,145 +131,104 @@ function EditFasilitasKesehatan() {
<TextInput
label="Nama Fasilitas Kesehatan"
placeholder="Masukkan nama fasilitas kesehatan"
defaultValue={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
value={formData.name}
onChange={(e) => updateForm('name', e.target.value)}
required
/>
{/* Informasi Umum */}
<Box>
<Text fw="bold" mb={5}>Informasi Umum</Text>
<Text fw="bold" mb={5}>
Informasi Umum
</Text>
<TextInput
label="Fasilitas"
defaultValue={formData.informasiUmum.fasilitas}
onChange={(e) =>
setFormData(prev => ({
...prev,
informasiUmum: { ...prev.informasiUmum, fasilitas: e.target.value },
}))
}
value={formData.informasiUmum.fasilitas}
onChange={(e) => updateNested('informasiUmum', 'fasilitas', e.target.value)}
/>
<TextInput
label="Alamat"
defaultValue={formData.informasiUmum.alamat}
onChange={(e) =>
setFormData(prev => ({
...prev,
informasiUmum: { ...prev.informasiUmum, alamat: e.target.value },
}))
}
value={formData.informasiUmum.alamat}
onChange={(e) => updateNested('informasiUmum', 'alamat', e.target.value)}
/>
<TextInput
label="Jam Operasional"
defaultValue={formData.informasiUmum.jamOperasional}
onChange={(e) =>
setFormData(prev => ({
...prev,
informasiUmum: { ...prev.informasiUmum, jamOperasional: e.target.value },
}))
}
value={formData.informasiUmum.jamOperasional}
onChange={(e) => updateNested('informasiUmum', 'jamOperasional', e.target.value)}
/>
</Box>
{/* Layanan Unggulan */}
<Box>
<Text fw="bold" mb={5}>Layanan Unggulan</Text>
<Text fw="bold" mb={5}>
Layanan Unggulan
</Text>
<EditEditor
value={formData.layananUnggulan.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
layananUnggulan: { content: e },
}))
}
onChange={(v) => updateNested('layananUnggulan', 'content', v)}
/>
</Box>
{/* Dokter dan Tenaga Medis */}
<Box>
<Text fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
<Text fw="bold" mb={5}>
Dokter dan Tenaga Medis
</Text>
<TextInput
label="Nama Dokter"
defaultValue={formData.dokterdanTenagaMedis.name}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, name: e.target.value },
}))
}
value={formData.dokterdanTenagaMedis.name}
onChange={(e) => updateNested('dokterdanTenagaMedis', 'name', e.target.value)}
/>
<TextInput
label="Specialist"
defaultValue={formData.dokterdanTenagaMedis.specialist}
value={formData.dokterdanTenagaMedis.specialist}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, specialist: e.target.value },
}))
updateNested('dokterdanTenagaMedis', 'specialist', e.target.value)
}
/>
<TextInput
label="Jadwal"
defaultValue={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, jadwal: e.target.value },
}))
}
value={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) => updateNested('dokterdanTenagaMedis', 'jadwal', e.target.value)}
/>
</Box>
{/* Fasilitas Pendukung */}
<Box>
<Text fw="bold" mb={5}>Fasilitas Pendukung</Text>
<Text fw="bold" mb={5}>
Fasilitas Pendukung
</Text>
<EditEditor
value={formData.fasilitasPendukung.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
fasilitasPendukung: { content: e },
}))
}
onChange={(v) => updateNested('fasilitasPendukung', 'content', v)}
/>
</Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fw="bold" mb={5}>Prosedur Pendaftaran</Text>
<Text fw="bold" mb={5}>
Prosedur Pendaftaran
</Text>
<EditEditor
value={formData.prosedurPendaftaran.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
prosedurPendaftaran: { content: e },
}))
}
onChange={(v) => updateNested('prosedurPendaftaran', 'content', v)}
/>
</Box>
{/* Tarif dan Layanan */}
<Box>
<Text fw="bold" mb={5}>Tarif dan Layanan</Text>
<Text fw="bold" mb={5}>
Tarif dan Layanan
</Text>
<TextInput
label="Tarif"
defaultValue={formData.tarifDanLayanan.tarif}
onChange={(e) =>
setFormData(prev => ({
...prev,
tarifDanLayanan: { ...prev.tarifDanLayanan, tarif: e.target.value },
}))
}
value={formData.tarifDanLayanan.tarif}
onChange={(e) => updateNested('tarifDanLayanan', 'tarif', e.target.value)}
/>
<TextInput
label="Layanan"
defaultValue={formData.tarifDanLayanan.layanan}
onChange={(e) =>
setFormData(prev => ({
...prev,
tarifDanLayanan: { ...prev.tarifDanLayanan, layanan: e.target.value },
}))
}
value={formData.tarifDanLayanan.layanan}
onChange={(e) => updateNested('tarifDanLayanan', 'layanan', e.target.value)}
/>
</Box>
@@ -304,7 +238,7 @@ function EditFasilitasKesehatan() {
onClick={handleSubmit}
radius="md"
size="md"
loading={stateFasilitasKesehatan.edit.loading}
loading={state.edit.loading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors';
import {
@@ -24,13 +25,14 @@ function EditGrafikHasilKepuasan() {
const params = useParams();
const [formData, setFormData] = useState({
nama: editState.update.form.nama || '',
tanggal: editState.update.form.tanggal || '',
jenisKelamin: editState.update.form.jenisKelamin || '',
alamat: editState.update.form.alamat || '',
penyakit: editState.update.form.penyakit || '',
nama: '',
tanggal: '',
jenisKelamin: '',
alamat: '',
penyakit: '',
});
// Load data once
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
@@ -38,17 +40,15 @@ function EditGrafikHasilKepuasan() {
try {
const data = await editState.update.load(id);
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
penyakit: data.penyakit || '',
});
}
} catch (error) {
console.error("Error loading grafik hasil kepuasan:", error);
if (data) setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
penyakit: data.penyakit || '',
});
} catch (err) {
console.error("Error loading grafik hasil kepuasan:", err);
toast.error("Gagal memuat data grafik hasil kepuasan");
}
};
@@ -56,17 +56,19 @@ function EditGrafikHasilKepuasan() {
loadData();
}, [params?.id]);
// Generic handler for controlled inputs
const handleChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
editState.update.form = {
...editState.update.form,
...formData,
};
editState.update.form = { ...editState.update.form, ...formData };
await editState.update.submit();
toast.success('Grafik hasil kepuasan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
} catch (error) {
console.error('Error updating grafik hasil kepuasan:', error);
} catch (err) {
console.error('Error updating grafik hasil kepuasan:', err);
toast.error('Terjadi kesalahan saat memperbarui grafik hasil kepuasan');
}
};
@@ -100,44 +102,17 @@ function EditGrafikHasilKepuasan() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label="Nama"
placeholder="Masukkan nama"
required
/>
<TextInput
type="date"
defaultValue={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label="Tanggal"
placeholder="Masukkan tanggal"
required
/>
<TextInput
defaultValue={formData.jenisKelamin}
onChange={(e) =>
setFormData({ ...formData, jenisKelamin: e.target.value })
}
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
required
/>
<TextInput
defaultValue={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label="Alamat"
placeholder="Masukkan alamat"
required
/>
<TextInput
defaultValue={formData.penyakit}
onChange={(e) => setFormData({ ...formData, penyakit: e.target.value })}
label="Penyakit"
placeholder="Masukkan penyakit"
required
/>
{(['nama','tanggal','jenisKelamin','alamat','penyakit'] as const).map((field) => (
<TextInput
key={field}
value={formData[field]}
onChange={(e) => handleChange(field, e.target.value)}
type={field === 'tanggal' ? 'date' : 'text'}
label={field === 'jenisKelamin' ? 'Jenis Kelamin' : field.charAt(0).toUpperCase() + field.slice(1)}
placeholder={`Masukkan ${field}`}
required
/>
))}
<Group justify="right">
<Button

View File

@@ -4,17 +4,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -29,46 +19,49 @@ interface JadwalKegiatanFormBase {
waktu: string;
lokasi: string;
};
deskripsiJadwalKegiatan: {
deskripsi: string;
};
layananJadwalKegiatan: {
content: string;
};
syaratKetentuanJadwalKegiatan: {
content: string;
};
dokumenJadwalKegiatan: {
content: string;
};
deskripsiJadwalKegiatan: { deskripsi: string };
layananJadwalKegiatan: { content: string };
syaratKetentuanJadwalKegiatan: { content: string };
dokumenJadwalKegiatan: { content: string };
}
const emptyForm = (): JadwalKegiatanFormBase => ({
content: '',
informasiJadwalKegiatan: { name: '', tanggal: '', waktu: '', lokasi: '' },
deskripsiJadwalKegiatan: { deskripsi: '' },
layananJadwalKegiatan: { content: '' },
syaratKetentuanJadwalKegiatan: { content: '' },
dokumenJadwalKegiatan: { content: '' },
});
function EditJadwalKegiatan() {
const stateJadwalKegiatan = useProxy(jadwalKegiatanState);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState<JadwalKegiatanFormBase>({
content: stateJadwalKegiatan.edit.form.content || '',
informasiJadwalKegiatan: {
name: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.name || '',
tanggal: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.tanggal || '',
waktu: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.waktu || '',
lokasi: stateJadwalKegiatan.edit.form.informasiJadwalKegiatan?.lokasi || '',
},
deskripsiJadwalKegiatan: {
deskripsi: stateJadwalKegiatan.edit.form.deskripsiJadwalKegiatan?.deskripsi || '',
},
layananJadwalKegiatan: {
content: stateJadwalKegiatan.edit.form.layananJadwalKegiatan?.content || '',
},
syaratKetentuanJadwalKegiatan: {
content: stateJadwalKegiatan.edit.form.syaratKetentuanJadwalKegiatan?.content || '',
},
dokumenJadwalKegiatan: {
content: stateJadwalKegiatan.edit.form.dokumenJadwalKegiatan?.content || '',
},
});
const [formData, setFormData] = useState<JadwalKegiatanFormBase>(emptyForm());
// Helper untuk update nested state
const updateNested = <
K extends keyof JadwalKegiatanFormBase,
N extends keyof JadwalKegiatanFormBase[K]
>(
key: K,
subKey: N,
value: string
) => {
setFormData(prev => ({
...prev,
[key]: {
...(prev[key] as Record<string, unknown>),
[subKey]: value
}
}));
};
const updateSimple = (key: keyof JadwalKegiatanFormBase, value: string) => {
setFormData(prev => ({ ...prev, [key]: value }));
};
useEffect(() => {
const loadJadwalKegiatan = async () => {
@@ -80,25 +73,17 @@ function EditJadwalKegiatan() {
const { form } = stateJadwalKegiatan.edit;
if (form) {
setFormData({
content: form.content,
content: form.content || '',
informasiJadwalKegiatan: {
name: form.informasiJadwalKegiatan?.name || '',
tanggal: form.informasiJadwalKegiatan?.tanggal || '',
waktu: form.informasiJadwalKegiatan?.waktu || '',
lokasi: form.informasiJadwalKegiatan?.lokasi || '',
},
deskripsiJadwalKegiatan: {
deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '',
},
layananJadwalKegiatan: {
content: form.layananJadwalKegiatan?.content || '',
},
syaratKetentuanJadwalKegiatan: {
content: form.syaratKetentuanJadwalKegiatan?.content || '',
},
dokumenJadwalKegiatan: {
content: form.dokumenJadwalKegiatan?.content || '',
},
deskripsiJadwalKegiatan: { deskripsi: form.deskripsiJadwalKegiatan?.deskripsi || '' },
layananJadwalKegiatan: { content: form.layananJadwalKegiatan?.content || '' },
syaratKetentuanJadwalKegiatan: { content: form.syaratKetentuanJadwalKegiatan?.content || '' },
dokumenJadwalKegiatan: { content: form.dokumenJadwalKegiatan?.content || '' },
});
}
} catch (error) {
@@ -111,16 +96,7 @@ function EditJadwalKegiatan() {
const handleSubmit = async () => {
try {
stateJadwalKegiatan.edit.form = {
...stateJadwalKegiatan.edit.form,
content: formData.content,
informasiJadwalKegiatan: { ...formData.informasiJadwalKegiatan },
deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan },
layananJadwalKegiatan: { ...formData.layananJadwalKegiatan },
syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan },
dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan }
};
stateJadwalKegiatan.edit.form = { ...stateJadwalKegiatan.edit.form, ...formData };
const success = await stateJadwalKegiatan.edit.submit();
if (success) {
toast.success("Jadwal kegiatan berhasil diperbarui!");
@@ -160,8 +136,8 @@ function EditJadwalKegiatan() {
<TextInput
label="Nama Jadwal Kegiatan"
placeholder="Masukkan nama jadwal kegiatan"
defaultValue={formData.content}
onChange={(e) => setFormData((prev) => ({ ...prev, content: e.target.value }))}
value={formData.content}
onChange={(e) => updateSimple('content', e.target.value)}
/>
{/* Deskripsi */}
@@ -169,45 +145,22 @@ function EditJadwalKegiatan() {
<Text fz="sm" fw="bold">Deskripsi Jadwal Kegiatan</Text>
<EditEditor
value={formData.deskripsiJadwalKegiatan.deskripsi}
onChange={(val) => setFormData((prev) => ({
...prev,
deskripsiJadwalKegiatan: { deskripsi: val }
}))}
onChange={(val) => updateNested('deskripsiJadwalKegiatan', 'deskripsi', val)}
/>
</Box>
{/* Informasi Jadwal */}
<Box>
<Text fz="md" fw="bold">Informasi Jadwal Kegiatan</Text>
<TextInput
label="Nama"
defaultValue={formData.informasiJadwalKegiatan.name}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, name: e.target.value }
}))}
/>
<TextInput
type="date"
label="Tanggal"
defaultValue={formData.informasiJadwalKegiatan.tanggal}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, tanggal: e.target.value }
}))}
/>
<TextInput
label="Waktu"
defaultValue={formData.informasiJadwalKegiatan.waktu}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, waktu: e.target.value }
}))}
/>
<TextInput
label="Lokasi"
defaultValue={formData.informasiJadwalKegiatan.lokasi}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, lokasi: e.target.value }
}))}
/>
{(['name', 'tanggal', 'waktu', 'lokasi'] as const).map((field) => (
<TextInput
key={field}
label={field === 'name' ? 'Nama' : field.charAt(0).toUpperCase() + field.slice(1)}
type={field === 'tanggal' ? 'date' : 'text'}
value={formData.informasiJadwalKegiatan[field]}
onChange={(e) => updateNested('informasiJadwalKegiatan', field, e.target.value)}
/>
))}
</Box>
{/* Layanan */}
@@ -215,10 +168,7 @@ function EditJadwalKegiatan() {
<Text fz="md" fw="bold">Layanan Jadwal Kegiatan</Text>
<EditEditor
value={formData.layananJadwalKegiatan.content}
onChange={(val) => setFormData((prev) => ({
...prev,
layananJadwalKegiatan: { content: val }
}))}
onChange={(val) => updateNested('layananJadwalKegiatan', 'content', val)}
/>
</Box>
@@ -227,10 +177,7 @@ function EditJadwalKegiatan() {
<Text fz="md" fw="bold">Syarat dan Ketentuan</Text>
<EditEditor
value={formData.syaratKetentuanJadwalKegiatan.content}
onChange={(val) => setFormData((prev) => ({
...prev,
syaratKetentuanJadwalKegiatan: { content: val }
}))}
onChange={(val) => updateNested('syaratKetentuanJadwalKegiatan', 'content', val)}
/>
</Box>
@@ -239,10 +186,7 @@ function EditJadwalKegiatan() {
<Text fz="md" fw="bold">Dokumen Yang Perlu Dibawa</Text>
<EditEditor
value={formData.dokumenJadwalKegiatan.content}
onChange={(val) => setFormData((prev) => ({
...prev,
dokumenJadwalKegiatan: { content: val }
}))}
onChange={(val) => updateNested('dokumenJadwalKegiatan', 'content', val)}
/>
</Box>

View File

@@ -1,16 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -18,146 +19,130 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKelahiran() {
const editState = useProxy(persentaseKelahiranKematian.kelahiran);
const router = useRouter();
const params = useParams();
const editState = useProxy(persentaseKelahiranKematian.kelahiran);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
nama: '',
tanggal: '',
jenisKelamin: '',
alamat: '',
});
const [formData, setFormData] = useState({
nama: editState.edit.form.nama || '',
tanggal: editState.edit.form.tanggal || '',
jenisKelamin: editState.edit.form.jenisKelamin || '',
alamat: editState.edit.form.alamat || '',
});
// Load data saat mount atau params.id berubah
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id);
if (data) setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || ''
});
} catch (error) {
console.error('Error loading data kelahiran:', error);
toast.error('Gagal memuat data kelahiran');
}
};
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
loadKelahiran();
}, [params?.id]);
const handleChange = (key: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
try {
const data = await editState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
});
}
} catch (error) {
console.error('Error loading data kelahiran:', error);
toast.error('Gagal memuat data kelahiran');
}
};
const handleSubmit = async () => {
try {
// Update global state hanya saat submit
editState.edit.form = { ...editState.edit.form, ...formData };
await editState.edit.update();
toast.success('Data kelahiran berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran');
} catch (error) {
console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Data Kelahiran
</Title>
</Group>
loadKelahiran();
}, [params?.id]);
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label="Nama"
placeholder="Masukkan nama"
required
/>
<TextInput
type="date"
value={formData.tanggal}
onChange={(e) => handleChange('tanggal', e.target.value)}
label="Tanggal"
placeholder="Masukkan tanggal"
required
/>
<TextInput
value={formData.jenisKelamin}
onChange={(e) => handleChange('jenisKelamin', e.target.value)}
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
required
/>
<TextInput
value={formData.alamat}
onChange={(e) => handleChange('alamat', e.target.value)}
label="Alamat"
placeholder="Masukkan alamat"
required
/>
const handleSubmit = async () => {
try {
editState.edit.form = {
...editState.edit.form,
nama: formData.nama,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
};
await editState.edit.update();
toast.success('Data kelahiran berhasil diperbarui!');
router.push(
'/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran'
);
} catch (error) {
console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Data Kelahiran
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label="Nama"
placeholder="Masukkan nama"
required
/>
<TextInput
type="date"
defaultValue={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label="Tanggal"
placeholder="Masukkan tanggal"
required
/>
<TextInput
defaultValue={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
required
/>
<TextInput
defaultValue={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label="Alamat"
placeholder="Masukkan alamat"
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKelahiran;
export default EditKelahiran;

View File

@@ -1,18 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -20,160 +21,149 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKematian() {
const editState = useProxy(persentaseKelahiranKematian.kematian);
const router = useRouter();
const params = useParams();
const editState = useProxy(persentaseKelahiranKematian.kematian);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
nama: '',
tanggal: '',
jenisKelamin: '',
alamat: '',
penyebab: '',
});
const [formData, setFormData] = useState({
nama: editState.edit.form.nama || '',
tanggal: editState.edit.form.tanggal || '',
jenisKelamin: editState.edit.form.jenisKelamin || '',
alamat: editState.edit.form.alamat || '',
penyebab: editState.edit.form.penyebab || '',
});
// Load data saat mount
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
penyebab: data.penyebab || '',
});
}
} catch (error) {
console.error('Error loading data kematian:', error);
toast.error('Gagal memuat data kematian');
}
};
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
loadData();
}, [params?.id]);
const handleChange = (key: keyof typeof formData, value: string) => {
setFormData(prev => ({ ...prev, [key]: value }));
};
try {
const data = await editState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
tanggal: data.tanggal || '',
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
penyebab: data.penyebab || '',
});
}
} catch (error) {
console.error('Error loading data kematian:', error);
toast.error('Gagal memuat data kematian');
}
};
const handleSubmit = async () => {
try {
// Update global state saat submit
editState.edit.form = { ...editState.edit.form, ...formData };
await editState.edit.update();
toast.success('Data kematian berhasil diperbarui!');
router.push(
'/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian'
);
} catch (error) {
console.error('Error updating data kematian:', error);
toast.error('Terjadi kesalahan saat memperbarui data kematian');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Data Kematian
</Title>
</Group>
loadData();
}, [params?.id]);
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
required
/>
<TextInput
type="date"
label="Tanggal"
placeholder="Masukkan tanggal"
value={formData.tanggal}
onChange={(e) => handleChange('tanggal', e.target.value)}
required
/>
const handleSubmit = async () => {
try {
editState.edit.form = { ...editState.edit.form, ...formData };
await editState.edit.update();
toast.success('Data kematian berhasil diperbarui!');
router.push(
'/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian'
);
} catch (error) {
console.error('Error updating data kematian:', error);
toast.error('Terjadi kesalahan saat memperbarui data kematian');
}
};
<TextInput
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
value={formData.jenisKelamin}
onChange={(e) => handleChange('jenisKelamin', e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={formData.alamat}
onChange={(e) => handleChange('alamat', e.target.value)}
required
/>
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Data Kematian
</Title>
</Group>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Penyebab
</Text>
<EditEditor
value={formData.penyebab}
onChange={(htmlContent) => handleChange('penyebab', htmlContent)}
/>
</Box>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama"
placeholder="Masukkan nama"
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
required
/>
<TextInput
type="date"
label="Tanggal"
placeholder="Masukkan tanggal"
defaultValue={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
required
/>
<TextInput
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
defaultValue={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
defaultValue={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
required
/>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Penyebab
</Text>
<EditEditor
value={formData.penyebab}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, penyebab: htmlContent }));
editState.edit.form.penyebab = htmlContent;
}}
/>
</Box>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKematian;
export default EditKematian;

View File

@@ -19,7 +19,7 @@ import {
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, ChangeEvent } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
@@ -28,15 +28,21 @@ function EditInfoWabahPenyakit() {
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: '',
deskripsiSingkat: '',
deskripsi: '',
imageId: '',
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: infoWabahPenyakitState.edit.form.name || '',
deskripsiSingkat: infoWabahPenyakitState.edit.form.deskripsiSingkat || '',
deskripsi: infoWabahPenyakitState.edit.form.deskripsiLengkap || '',
imageId: infoWabahPenyakitState.edit.form.imageId || '',
});
// Helper untuk update field formData
const updateField = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
// Load data edit
useEffect(() => {
const loadInfoWabahPenyakit = async () => {
const id = params?.id as string;
@@ -52,12 +58,10 @@ function EditInfoWabahPenyakit() {
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error('Error loading info wabah penyakit:', error);
console.error(error);
toast.error('Gagal memuat data info wabah penyakit');
}
};
@@ -67,34 +71,43 @@ function EditInfoWabahPenyakit() {
const handleSubmit = async () => {
try {
let uploadedImageId = formData.imageId;
// Upload file kalau ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
uploadedImageId = uploaded.id;
}
// Update global state
infoWabahPenyakitState.edit.form = {
...infoWabahPenyakitState.edit.form,
name: formData.name,
deskripsiSingkat: formData.deskripsiSingkat,
deskripsiLengkap: formData.deskripsi,
imageId: formData.imageId,
imageId: uploadedImageId,
};
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal upload gambar');
}
infoWabahPenyakitState.edit.form.imageId = uploaded.id;
}
await infoWabahPenyakitState.edit.update();
toast.success('Info wabah penyakit berhasil diperbarui!');
router.push('/admin/kesehatan/info-wabah-penyakit');
} catch (error) {
console.error('Error updating info wabah penyakit:', error);
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui info wabah penyakit');
}
};
const handleDrop = (files: File[]) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
@@ -120,16 +133,18 @@ function EditInfoWabahPenyakit() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e: ChangeEvent<HTMLInputElement>) => updateField('name', e.target.value)}
label="Judul"
placeholder="Masukkan judul"
required
/>
<TextInput
defaultValue={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
value={formData.deskripsiSingkat}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
updateField('deskripsiSingkat', e.target.value)
}
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
required
@@ -141,7 +156,7 @@ function EditInfoWabahPenyakit() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => updateField('deskripsi', val)}
/>
</Box>
@@ -150,13 +165,7 @@ function EditInfoWabahPenyakit() {
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onDrop={handleDrop}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}

View File

@@ -1,5 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import colors from '@/con/colors';
@@ -23,7 +22,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditKontakDarurat() {
const kontakDaruratState = useProxy(kontakDarurat);
const router = useRouter();
@@ -32,11 +30,13 @@ function EditKontakDarurat() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: kontakDaruratState.edit.form.name || '',
deskripsi: kontakDaruratState.edit.form.deskripsi || '',
imageId: kontakDaruratState.edit.form.imageId || '',
name: '',
deskripsi: '',
imageId: '',
});
const [loading, setLoading] = useState(true);
// Load data sekali saat mount
useEffect(() => {
const loadKontakDarurat = async () => {
const id = params?.id as string;
@@ -50,40 +50,38 @@ function EditKontakDarurat() {
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error("Error loading kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat");
} finally {
setLoading(false);
}
};
loadKontakDarurat();
}, [params?.id]);
}, [params?.id, kontakDaruratState.edit]);
const handleSubmit = async () => {
try {
kontakDaruratState.edit.form = {
...kontakDaruratState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
};
let imageId = formData.imageId;
// Upload file baru jika ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
kontakDaruratState.edit.form.imageId = uploaded.id;
if (!uploaded?.id) return toast.error("Gagal upload gambar");
imageId = uploaded.id;
}
// Update global state sekaligus submit
kontakDaruratState.edit.form = {
...kontakDaruratState.edit.form,
...formData,
imageId,
};
await kontakDaruratState.edit.update();
toast.success("Kontak darurat berhasil diperbarui!");
router.push("/admin/kesehatan/kontak-darurat");
@@ -93,9 +91,10 @@ function EditKontakDarurat() {
}
};
if (loading) return <Text>Loading...</Text>;
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
@@ -107,7 +106,6 @@ function EditKontakDarurat() {
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
@@ -117,9 +115,10 @@ function EditKontakDarurat() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Controlled Input */}
<TextInput
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
label="Judul"
placeholder="Masukkan judul"
required
@@ -129,7 +128,7 @@ function EditKontakDarurat() {
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => setFormData(prev => ({ ...prev, deskripsi: val }))}
/>
</Box>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
import colors from '@/con/colors';
@@ -28,16 +29,19 @@ function EditPenangananDarurat() {
const router = useRouter();
const params = useParams()
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
imageId: '',
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: penangananDaruratState.edit.form.name || '',
deskripsi: penangananDaruratState.edit.form.deskripsi || '',
imageId: penangananDaruratState.edit.form.imageId || '',
})
const [loading, setLoading] = useState(true);
// Load data satu kali saat component mount
useEffect(() => {
const loadPenangananDarurat = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
@@ -48,49 +52,66 @@ function EditPenangananDarurat() {
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
})
});
if (data?.image?.link) {
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading penanganan darurat:', error);
} catch (err) {
console.error('Error loading penanganan darurat:', err);
toast.error('Gagal memuat data penanganan darurat');
} finally {
setLoading(false);
}
}
loadPenangananDarurat();
}, [params?.id])
loadData();
}, [params?.id]);
const handleChange = (key: keyof typeof formData, value: string) => {
setFormData(prev => ({ ...prev, [key]: value }));
};
const handleDrop = (files: File[]) => {
const selected = files[0];
if (!selected) return;
setFile(selected);
setPreviewImage(URL.createObjectURL(selected));
};
const handleSubmit = async () => {
try {
penangananDaruratState.edit.form = {
...penangananDaruratState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
if (!uploaded?.id) return toast.error("Gagal upload gambar");
penangananDaruratState.edit.form.imageId = uploaded.id;
imageId = uploaded.id;
}
// update global state sekali saat submit
penangananDaruratState.edit.form = {
...penangananDaruratState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId,
};
await penangananDaruratState.edit.update();
toast.success("Penanganan darurat berhasil diperbarui!");
router.push("/admin/kesehatan/penanganan-darurat");
} catch (error) {
console.error("Error updating penanganan darurat:", error);
} catch (err) {
console.error("Error updating penanganan darurat:", err);
toast.error("Gagal memperbarui data penanganan darurat");
}
}
};
if (loading) return <Text>Loading...</Text>;
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -117,8 +138,8 @@ function EditPenangananDarurat() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
label="Judul"
placeholder="Masukkan judul"
required
@@ -128,20 +149,14 @@ function EditPenangananDarurat() {
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => handleChange('deskripsi', val)}
/>
</Box>
<Box>
<Text fz="sm" fw="bold">Gambar</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onDrop={handleDrop}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}

View File

@@ -1,20 +1,21 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -23,234 +24,211 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPosyandu() {
const statePosyandu = useProxy(posyandustate);
const router = useRouter();
const params = useParams();
const statePosyandu = useProxy(posyandustate);
const router = useRouter();
const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: '',
nomor: '',
deskripsi: '',
imageId: '',
jadwalPelayanan: '',
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: statePosyandu.edit.form.name || '',
nomor: statePosyandu.edit.form.nomor || '',
deskripsi: statePosyandu.edit.form.deskripsi || '',
imageId: statePosyandu.edit.form.imageId || '',
jadwalPelayanan: statePosyandu.edit.form.jadwalPelayanan || '',
});
// Load data posyandu
useEffect(() => {
const loadPosyandu = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await statePosyandu.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
jadwalPelayanan: data.jadwalPelayanan || '',
});
if (data?.image?.link) setPreviewImage(data.image.link);
}
} catch (error) {
console.error('Error loading posyandu:', error);
toast.error('Gagal memuat data posyandu');
}
};
loadPosyandu();
}, [params?.id]);
useEffect(() => {
const loadPosyandu = async () => {
const id = params?.id as string;
if (!id) return;
const handleSubmit = async () => {
try {
// Update global state hanya saat submit
const updatedForm = { ...statePosyandu.edit.form, ...formData };
// Upload file jika ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
try {
const data = await statePosyandu.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
jadwalPelayanan: data.jadwalPelayanan || '',
});
if (!uploaded?.id) return toast.error('Gagal upload gambar');
updatedForm.imageId = uploaded.id;
}
statePosyandu.edit.form = updatedForm;
await statePosyandu.edit.update();
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
};
loadPosyandu();
}, [params?.id]);
toast.success('Posyandu berhasil diperbarui!');
router.push('/admin/kesehatan/posyandu');
} catch (error) {
console.error('Error updating posyandu:', error);
toast.error('Terjadi kesalahan saat memperbarui posyandu');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol Back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Posyandu
</Title>
</Group>
const handleSubmit = async () => {
try {
statePosyandu.edit.form = {
...statePosyandu.edit.form,
...formData,
};
{/* Card utama */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Posyandu
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
{/* Input Form */}
<TextInput
label="Nama Posyandu"
placeholder="Masukkan nama posyandu"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nomor Posyandu"
placeholder="Masukkan nomor posyandu"
value={formData.nomor}
onChange={(e) => setFormData({ ...formData, nomor: e.target.value })}
required
/>
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Posyandu
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Jadwal Pelayanan
</Text>
<EditEditor
value={formData.jadwalPelayanan}
onChange={(htmlContent) =>
setFormData({ ...formData, jadwalPelayanan: htmlContent })
}
/>
</Box>
statePosyandu.edit.form.imageId = uploaded.id;
}
await statePosyandu.edit.update();
toast.success("Posyandu berhasil diperbarui!");
router.push("/admin/kesehatan/posyandu");
} catch (error) {
console.error("Error updating posyandu:", error);
toast.error("Terjadi kesalahan saat memperbarui posyandu");
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Tombol Back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Posyandu
</Title>
</Group>
{/* Card utama */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Posyandu
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Input Form */}
<TextInput
label="Nama Posyandu"
placeholder="Masukkan nama posyandu"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nomor Posyandu"
placeholder="Masukkan nomor posyandu"
defaultValue={formData.nomor}
onChange={(e) => setFormData({ ...formData, nomor: e.target.value })}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>Deskripsi Posyandu</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
statePosyandu.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>Jadwal Pelayanan</Text>
<EditEditor
value={formData.jadwalPelayanan}
onChange={(htmlContent) => {
setFormData({ ...formData, jadwalPelayanan: htmlContent });
statePosyandu.edit.form.jadwalPelayanan = htmlContent;
}}
/>
</Box>
{/* Tombol Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
{/* Tombol Submit */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPosyandu;
export default EditPosyandu;

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
import colors from '@/con/colors';
@@ -31,73 +31,70 @@ function EditProgramKesehatan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: programKesehatanState.edit.form.name || '',
deskripsiSingkat: programKesehatanState.edit.form.deskripsiSingkat || '',
deskripsi: programKesehatanState.edit.form.deskripsi || '',
imageId: programKesehatanState.edit.form.imageId || '',
name: '',
deskripsiSingkat: '',
deskripsi: '',
imageId: '',
});
// Load data awal
useEffect(() => {
const loadProgramKesehatan = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await programKesehatanState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsiSingkat: data.deskripsiSingkat || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (!data) return;
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error('Error loading program kesehatan:', error);
setFormData({
name: data.name || '',
deskripsiSingkat: data.deskripsiSingkat || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) setPreviewImage(data.image.link);
} catch (err) {
console.error(err);
toast.error('Gagal memuat data program kesehatan');
}
};
loadProgramKesehatan();
loadData();
}, [params?.id]);
// Handler input controlled
const handleChange = (key: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
// Submit form
const handleSubmit = async () => {
try {
programKesehatanState.edit.form = {
...programKesehatanState.edit.form,
name: formData.name,
deskripsiSingkat: formData.deskripsiSingkat,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
};
const updatedForm = { ...programKesehatanState.edit.form, ...formData };
// Upload file kalau ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
if (!uploaded?.id) {
return toast.error('Gagal upload gambar');
}
programKesehatanState.edit.form.imageId = uploaded.id;
updatedForm.imageId = uploaded.id;
}
programKesehatanState.edit.form = updatedForm;
await programKesehatanState.edit.update();
toast.success('Program kesehatan berhasil diperbarui!');
router.push('/admin/kesehatan/program-kesehatan');
} catch (error) {
console.error('Error updating program kesehatan:', error);
} catch (err) {
console.error(err);
toast.error('Terjadi kesalahan saat memperbarui program kesehatan');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
@@ -109,7 +106,6 @@ function EditProgramKesehatan() {
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
@@ -119,21 +115,19 @@ function EditProgramKesehatan() {
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label="Judul"
placeholder="Masukkan judul"
required
/>
<TextInput
defaultValue={formData.deskripsiSingkat}
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
required
/>
{[
{ label: 'Judul', key: 'name', placeholder: 'Masukkan judul' },
{ label: 'Deskripsi Singkat', key: 'deskripsiSingkat', placeholder: 'Masukkan deskripsi singkat' },
].map((field) => (
<TextInput
key={field.key}
label={field.label}
placeholder={field.placeholder}
value={formData[field.key as keyof typeof formData]}
onChange={(e) => handleChange(field.key as keyof typeof formData, e.target.value)}
required
/>
))}
<Box>
<Text fz="sm" fw="bold" mb={6}>
@@ -141,7 +135,7 @@ function EditProgramKesehatan() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
onChange={(val) => handleChange('deskripsi', val)}
/>
</Box>
@@ -152,10 +146,9 @@ function EditProgramKesehatan() {
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
if (!selectedFile) return;
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}

View File

@@ -54,20 +54,11 @@ function EditPuskesmas() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState<PuskesmasFormData>({
name: statePuskesmas.edit.form.name || '',
alamat: statePuskesmas.edit.form.alamat || '',
jam: {
workDays: statePuskesmas.edit.form.jam?.workDays || '',
weekDays: statePuskesmas.edit.form.jam?.weekDays || '',
holiday: statePuskesmas.edit.form.jam?.holiday || '',
},
kontak: {
kontakPuskesmas: statePuskesmas.edit.form.kontak?.kontakPuskesmas || '',
email: statePuskesmas.edit.form.kontak?.email || '',
facebook: statePuskesmas.edit.form.kontak?.facebook || '',
kontakUGD: statePuskesmas.edit.form.kontak?.kontakUGD || '',
},
imageId: statePuskesmas.edit.form.imageId || '',
name: '',
alamat: '',
jam: { workDays: '', weekDays: '', holiday: '' },
kontak: { kontakPuskesmas: '', email: '', facebook: '', kontakUGD: '' },
imageId: '',
});
useEffect(() => {
@@ -183,7 +174,7 @@ function EditPuskesmas() {
label="Nama Puskesmas"
placeholder="Masukkan nama puskesmas"
name="name"
defaultValue={formData.name}
value={formData.name}
onChange={handleInputChange}
required
/>
@@ -192,7 +183,7 @@ function EditPuskesmas() {
label="Alamat"
placeholder="Masukkan alamat"
name="alamat"
defaultValue={formData.alamat}
value={formData.alamat}
onChange={handleInputChange}
required
/>
@@ -200,7 +191,7 @@ function EditPuskesmas() {
<TextInput
label="Jam Buka"
placeholder="Masukkan jam buka"
defaultValue={formData.jam.workDays}
value={formData.jam.workDays}
onChange={(e) => handleNestedChange('jam', 'workDays', e.target.value)}
required
/>
@@ -208,7 +199,7 @@ function EditPuskesmas() {
<TextInput
label="Jam Tutup"
placeholder="Masukkan jam tutup"
defaultValue={formData.jam.weekDays}
value={formData.jam.weekDays}
onChange={(e) => handleNestedChange('jam', 'weekDays', e.target.value)}
required
/>
@@ -216,7 +207,7 @@ function EditPuskesmas() {
<TextInput
label="Jam Libur"
placeholder="Masukkan jam libur"
defaultValue={formData.jam.holiday}
value={formData.jam.holiday}
onChange={(e) => handleNestedChange('jam', 'holiday', e.target.value)}
required
/>
@@ -224,28 +215,28 @@ function EditPuskesmas() {
<TextInput
label="Kontak Puskesmas"
placeholder="Masukkan kontak puskesmas"
defaultValue={formData.kontak.kontakPuskesmas}
value={formData.kontak.kontakPuskesmas}
onChange={(e) => handleNestedChange('kontak', 'kontakPuskesmas', e.target.value)}
/>
<TextInput
label="Email"
placeholder="Masukkan email"
defaultValue={formData.kontak.email}
value={formData.kontak.email}
onChange={(e) => handleNestedChange('kontak', 'email', e.target.value)}
/>
<TextInput
label="Facebook"
placeholder="Masukkan facebook"
defaultValue={formData.kontak.facebook}
value={formData.kontak.facebook}
onChange={(e) => handleNestedChange('kontak', 'facebook', e.target.value)}
/>
<TextInput
label="Kontak UGD"
placeholder="Masukkan kontak UGD"
defaultValue={formData.kontak.kontakUGD}
value={formData.kontak.kontakUGD}
onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)}
/>

View File

@@ -28,20 +28,21 @@ function EditAPBDes() {
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: '',
jumlah: '',
imageId: '',
fileId: ''
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewDoc, setPreviewDoc] = useState<string | null>(null);
const [imageFile, setImageFile] = useState<File | null>(null);
const [docFile, setDocFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: apbdesState.edit.form.name || '',
jumlah: apbdesState.edit.form.jumlah || '',
imageId: apbdesState.edit.form.imageId || '',
fileId: apbdesState.edit.form.fileId || ''
});
// Load APBDes data by id
// Load data on mount
useEffect(() => {
const loadAPBDes = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
@@ -54,62 +55,58 @@ function EditAPBDes() {
imageId: data.imageId || '',
fileId: data.fileId || ''
});
if (data.image?.link) setPreviewImage(data.image.link);
if (data.file?.link) setPreviewDoc(data.file.link);
setPreviewImage(data.image?.link || null);
setPreviewDoc(data.file?.link || null);
}
} catch (error) {
console.error('Error loading APBDes:', error);
} catch (err) {
console.error(err);
toast.error('Gagal memuat data APBDes');
}
};
loadAPBDes();
loadData();
}, [params?.id]);
// Generic Dropzone handler
const handleDrop = (fileType: 'image' | 'doc') => (files: File[]) => {
const file = files[0];
if (!file) return;
if (fileType === 'image') {
setImageFile(file);
setPreviewImage(URL.createObjectURL(file));
} else {
setDocFile(file);
setPreviewDoc(URL.createObjectURL(file));
}
};
const handleSubmit = async () => {
try {
// Update global state with form data
apbdesState.edit.form = {
...apbdesState.edit.form,
...formData,
// Update global state with local form data first
apbdesState.edit.form = { ...apbdesState.edit.form, ...formData };
// Helper function for uploading file
const uploadFile = async (file: File | null) => {
if (!file) return null;
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) throw new Error('Upload gagal');
return uploaded.id;
};
// Upload new image if exists
if (imageFile) {
const res = await ApiFetch.api.fileStorage.create.post({
file: imageFile,
name: imageFile.name
});
const uploaded = res.data?.data;
// Upload files if selected
const uploadedImageId = await uploadFile(imageFile);
const uploadedDocId = await uploadFile(docFile);
if (!uploaded?.id) {
return toast.error('Gagal upload gambar');
}
apbdesState.edit.form.imageId = uploaded.id;
}
// Upload new document if exists
if (docFile) {
const res = await ApiFetch.api.fileStorage.create.post({
file: docFile,
name: docFile.name
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal upload dokumen');
}
apbdesState.edit.form.fileId = uploaded.id;
}
if (uploadedImageId) apbdesState.edit.form.imageId = uploadedImageId;
if (uploadedDocId) apbdesState.edit.form.fileId = uploadedDocId;
await apbdesState.edit.update();
toast.success('APBDes berhasil diperbarui!');
router.push('/admin/landing-page/apbdes');
} catch (error) {
console.error('Error updating APBDes:', error);
} catch (err) {
console.error(err);
toast.error('Terjadi kesalahan saat memperbarui APBDes');
}
};
@@ -127,19 +124,13 @@ function EditAPBDes() {
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="md">
{/* Controlled Inputs */}
<TextInput
label="Nama APBDes"
placeholder="Masukkan nama APBDes"
defaultValue={formData.name}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
@@ -147,23 +138,16 @@ function EditAPBDes() {
<TextInput
label="Jumlah Anggaran"
placeholder="Masukkan jumlah anggaran"
defaultValue={formData.jumlah}
value={formData.jumlah}
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
required
/>
{/* Image Dropzone */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar APBDes
</Text>
<Text fw="bold" fz="sm" mb={6}>Gambar APBDes</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setImageFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onDrop={handleDrop('image')}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
@@ -171,57 +155,29 @@ function EditAPBDes() {
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Dropzone.Accept><IconUpload size={48} color={colors['blue-button']} stroke={1.5} /></Dropzone.Accept>
<Dropzone.Reject><IconX size={48} color="red" stroke={1.5} /></Dropzone.Reject>
<Dropzone.Idle><IconPhoto size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
<Text size="md" fw={500}>Seret gambar atau klik untuk memilih file</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar wajib</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 300,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`
}}
loading="lazy"
/>
<Image src={previewImage} alt="Preview Gambar" radius="md" style={{ maxHeight: 300, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} loading="lazy" />
</Box>
)}
</Box>
{/* Document Dropzone */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Dokumen APBDes
</Text>
<Text fw="bold" fz="sm" mb={6}>Dokumen APBDes</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setDocFile(selectedFile);
setPreviewDoc(URL.createObjectURL(selectedFile));
}
}}
onDrop={handleDrop('doc')}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={10 * 1024 ** 2} // 10MB
maxSize={10 * 1024 ** 2}
accept={{
'application/pdf': ['.pdf'],
'application/msword': ['.doc'],
@@ -233,40 +189,19 @@ function EditAPBDes() {
p="xl"
>
<Group justify="center" gap="xl" mih={150}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconFile size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Dropzone.Accept><IconUpload size={48} color={colors['blue-button']} stroke={1.5} /></Dropzone.Accept>
<Dropzone.Reject><IconX size={48} color="red" stroke={1.5} /></Dropzone.Reject>
<Dropzone.Idle><IconFile size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret dokumen atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX
</Text>
<Text size="md" fw={500}>Seret dokumen atau klik untuk memilih file</Text>
<Text size="sm" c="dimmed">Maksimal 10MB, format PDF/DOC/DOCX/XLS/XLSX</Text>
</Stack>
</Group>
</Dropzone>
{previewDoc && (
<Box mt="sm">
<Text size="sm" c="dimmed" mb="xs">
Dokumen terpilih: {docFile?.name || 'Dokumen'}
</Text>
<Button
component="a"
href={previewDoc}
target="_blank"
rel="noopener noreferrer"
variant="light"
leftSection={<IconFile size={16} />}
size="sm"
>
<Text size="sm" c="dimmed" mb="xs">Dokumen terpilih: {docFile?.name || 'Dokumen'}</Text>
<Button component="a" href={previewDoc} target="_blank" rel="noopener noreferrer" variant="light" leftSection={<IconFile size={16} />} size="sm">
Lihat Dokumen
</Button>
</Box>

View File

@@ -1,11 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
@@ -15,28 +16,25 @@ export default function EditKategoriDesaAntiKorupsi() {
const id = params?.id as string;
const stateKategori = useProxy(korupsiState.kategoriDesaAntiKorupsi);
const [formData, setFormData] = useState({
name: "",
});
// state lokal untuk form
const [formData, setFormData] = useState({ name: '' });
const [isLoading, setIsLoading] = useState(false);
// load data kategori saat mount atau id berubah
useEffect(() => {
if (!id) return;
const loadKategori = async () => {
if (!id) return;
setIsLoading(true);
try {
const data = await stateKategori.edit.load(id);
if (data) {
stateKategori.edit.id = id;
setFormData({
name: data.name || '',
});
setFormData({ name: data.name || '' });
}
} catch (error) {
console.error("Error loading kategori desa anti korupsi:", error);
toast.error("Gagal memuat data kategori desa anti korupsi");
} catch (err) {
console.error(err);
toast.error('Gagal memuat data kategori desa anti korupsi');
} finally {
setIsLoading(false);
}
@@ -45,36 +43,40 @@ export default function EditKategoriDesaAntiKorupsi() {
loadKategori();
}, [id]);
const handleSubmit = async () => {
if (!formData.name.trim()) {
return toast.error('Nama kategori tidak boleh kosong');
}
// handler controlled input
const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
},
[]
);
// submit form
const handleSubmit = useCallback(async () => {
if (!formData.name.trim()) return toast.error('Nama kategori tidak boleh kosong');
try {
setIsLoading(true);
stateKategori.edit.form = {
name: formData.name.trim(),
};
if (!stateKategori.edit.id) {
stateKategori.edit.id = id;
}
// update global state hanya saat submit
stateKategori.edit.form = { name: formData.name.trim() };
if (!stateKategori.edit.id) stateKategori.edit.id = id;
await stateKategori.edit.update();
toast.success('Kategori berhasil diperbarui');
router.push("/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi");
} catch (error) {
console.error("Error updating kategori desa anti korupsi:", error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui kategori');
router.push('/admin/landing-page/desa-anti-korupsi/kategori-desa-anti-korupsi');
} catch (err) {
console.error(err);
toast.error(err instanceof Error ? err.message : 'Gagal memperbarui kategori');
} finally {
setIsLoading(false);
}
};
}, [formData.name, id, router, stateKategori.edit]);
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
@@ -96,8 +98,8 @@ export default function EditKategoriDesaAntiKorupsi() {
<TextInput
label="Nama Kategori"
placeholder="Masukkan nama kategori"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name} // controlled
onChange={(e) => handleChange('name', e.currentTarget.value)}
required
disabled={isLoading}
/>

View File

@@ -1,18 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import { Box, Button, Group, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack, IconFile, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, ChangeEvent } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors';
import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-korupsi';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import { useShallowEffect } from '@mantine/hooks';
interface FormDesaAntiKorupsi {
name: string;
@@ -23,12 +24,9 @@ interface FormDesaAntiKorupsi {
export default function EditDesaAntiKorupsi() {
const desaAntiKorupsiState = useProxy(korupsiState.desaAntikorupsi);
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormDesaAntiKorupsi>({
name: '',
deskripsi: '',
@@ -36,10 +34,16 @@ export default function EditDesaAntiKorupsi() {
fileId: '',
});
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Load kategori
useShallowEffect(() => {
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
}, []);
// Load data existing
useEffect(() => {
const loadDesaAntiKorupsi = async () => {
const id = params?.id as string;
@@ -47,29 +51,21 @@ export default function EditDesaAntiKorupsi() {
try {
const data = await desaAntiKorupsiState.edit.load(id);
if (data) {
desaAntiKorupsiState.edit.id = id;
if (!data) return;
desaAntiKorupsiState.edit.form = {
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId,
};
desaAntiKorupsiState.edit.id = id;
desaAntiKorupsiState.edit.form = { ...data };
setFormData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId,
});
setFormData({
name: data.name,
deskripsi: data.deskripsi,
kategoriId: data.kategoriId,
fileId: data.fileId,
});
if (data?.file?.link) {
setPreviewFile(data.file.link);
}
}
} catch (error) {
console.error('Error loading data:', error);
if (data.file?.link) setPreviewFile(data.file.link);
} catch (err) {
console.error(err);
toast.error('Gagal memuat data Desa Anti Korupsi');
}
};
@@ -77,42 +73,47 @@ export default function EditDesaAntiKorupsi() {
loadDesaAntiKorupsi();
}, [params?.id]);
// Generic handler input
const handleInputChange = (key: keyof FormDesaAntiKorupsi) => (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string | null) => {
const value = typeof e === 'string' ? e : e?.target?.value ?? '';
setFormData((prev) => ({ ...prev, [key]: value || '' }));
};
// Special handler for Select component
const handleSelectChange = (key: keyof FormDesaAntiKorupsi) => (value: string | null) => {
setFormData((prev) => ({ ...prev, [key]: value || '' }));
};
const handleDrop = (files: File[]) => {
if (!files.length) return;
const selectedFile = files[0];
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile));
};
const handleSubmit = async () => {
if (!formData.name) {
return toast.warn('Masukkan judul dokumen');
}
if (!formData.kategoriId) {
return toast.warn('Pilih kategori dokumen');
}
if (!formData.name) return toast.warn('Masukkan judul dokumen');
if (!formData.kategoriId) return toast.warn('Pilih kategori dokumen');
setIsLoading(true);
try {
// Update global state with form data
desaAntiKorupsiState.edit.form = {
...desaAntiKorupsiState.edit.form,
...formData,
kategoriId: formData.kategoriId || '',
};
// Update global state
desaAntiKorupsiState.edit.form = { ...desaAntiKorupsiState.edit.form, ...formData };
// Upload new file if exists
// Upload file jika ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name
});
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) throw new Error('Gagal mengunggah dokumen');
if (!uploaded?.id) {
throw new Error('Gagal mengunggah dokumen');
}
desaAntiKorupsiState.edit.form.fileId = uploaded.id;
}
await desaAntiKorupsiState.edit.update();
toast.success('Data berhasil diperbarui');
router.push('/admin/landing-page/desa-anti-korupsi/list-desa-anti-korupsi');
} catch (error) {
console.error('Error updating data:', error);
} catch (err) {
console.error(err);
toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsLoading(false);
@@ -144,8 +145,8 @@ export default function EditDesaAntiKorupsi() {
<TextInput
label="Judul Dokumen"
placeholder="Masukkan judul dokumen"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={handleInputChange('name')}
required
/>
@@ -153,23 +154,18 @@ export default function EditDesaAntiKorupsi() {
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/>
<EditEditor value={formData.deskripsi} onChange={handleInputChange('deskripsi')} />
</Box>
<Select
label="Kategori"
placeholder="Pilih kategori"
value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val || '' })}
data={
korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
onChange={handleSelectChange('kategoriId')}
data={korupsiState.kategoriDesaAntiKorupsi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []}
required
searchable
clearable
@@ -180,13 +176,7 @@ export default function EditDesaAntiKorupsi() {
Dokumen
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewFile(URL.createObjectURL(selectedFile));
}
}}
onDrop={handleDrop}
onReject={() => toast.error('File tidak valid, gunakan format dokumen')}
maxSize={5 * 1024 ** 2}
accept={{
@@ -229,12 +219,7 @@ export default function EditDesaAntiKorupsi() {
width: '100%',
}}
>
<iframe
src={previewFile}
width="100%"
height="100%"
style={{ border: 'none' }}
/>
<iframe src={previewFile} width="100%" height="100%" style={{ border: 'none' }} />
</Box>
</Box>
)}
@@ -259,4 +244,4 @@ export default function EditDesaAntiKorupsi() {
</Paper>
</Box>
);
}
}

View File

@@ -1,10 +1,21 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Title, TextInput, Text, Select, Tooltip } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Title,
TextInput,
Text,
Select,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconDeviceFloppy } from '@tabler/icons-react';
import indeksKepuasanState from '@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan';
import { toast } from 'react-toastify';
@@ -18,64 +29,93 @@ interface FormResponden {
}
function EditResponden() {
const router = useRouter()
const params = useParams() as { id: string }
const state = useProxy(indeksKepuasanState.responden)
const id = params.id
const router = useRouter();
const params = useParams() as { id: string };
const state = useProxy(indeksKepuasanState.responden);
const id = params.id;
const [formData, setFormData] = useState<FormResponden>({
name: '',
tanggal: '',
jenisKelaminId: '',
ratingId: '',
kelompokUmurId: '',
})
useEffect(() => {
});
// Helper untuk load pilihan select
const loadSelectOptions = useCallback(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load();
indeksKepuasanState.pilihanRatingResponden.findMany.load();
indeksKepuasanState.kelompokUmurResponden.findMany.load();
}, []);
const loadResponden = async () => {
const id = params?.id as string;
if (!id) return;
// Load data responden
const loadResponden = useCallback(async () => {
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
state.update.id = id;
try {
const data = await state.update.load(id);
if (!data) return;
state.update.form = {
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
};
setFormData({
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
});
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
}
setFormData({
name: data.name,
tanggal: data.tanggal,
jenisKelaminId: data.jenisKelaminId,
ratingId: data.ratingId,
kelompokUmurId: data.kelompokUmurId,
});
} catch (error) {
console.error("Error loading responden:", error);
toast.error("Gagal memuat data responden");
}
}, [id]);
useEffect(() => {
loadSelectOptions();
loadResponden();
}, [params?.id]);
}, [loadSelectOptions, loadResponden]);
const handleSubmit = async () => {
state.update.id = id;
state.update.form = { ...formData }; // <-- sinkronisasi manual
state.update.form = { ...formData }; // sinkronisasi manual
await state.update.submit();
router.push('/admin/landing-page/indeks-kepuasan-masyarakat/responden')
}
router.push('/admin/landing-page/indeks-kepuasan-masyarakat/responden');
};
// Reusable Select component
const ControlledSelect = ({
label,
value,
onChange,
options,
error,
placeholder = 'Pilih',
loading = false,
}: {
label: string;
value: string;
onChange: (val: string) => void;
options: { value: string; label: string }[];
error?: string;
placeholder?: string;
loading?: boolean;
}) => {
return (
<Select
label={<Text fw="bold" fz="sm" mb={4}>{label}</Text>}
value={value}
onChange={(val) => onChange(val || '')}
data={options}
placeholder={placeholder}
disabled={loading}
clearable
searchable
required
radius="md"
error={error}
/>
);
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -101,116 +141,61 @@ function EditResponden() {
<Stack gap="md">
<TextInput
label="Nama Responden"
type='text'
placeholder="Masukkan nama responden"
defaultValue={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.currentTarget.value
})
}}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.currentTarget.value })}
radius="md"
required
/>
<TextInput
label="Tanggal"
type="date"
placeholder='Pilih tanggal'
defaultValue={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
onChange={(e) => {
const selectedDate = e.currentTarget.value;
setFormData({
...formData,
tanggal: selectedDate,
});
}}
value={formData.tanggal ? new Date(formData.tanggal).toISOString().split('T')[0] : ''}
onChange={(e) => setFormData({ ...formData, tanggal: e.currentTarget.value })}
radius="md"
required
/>
<Select
key="jenisKelamin"
label={
<Text fw="bold" fz="sm" mb={4}>
Jenis Kelamin
</Text>
}
placeholder="Pilih jenis kelamin"
value={formData.jenisKelaminId}
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val || "" })}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
clearable
searchable
required
radius="md"
error={!formData.jenisKelaminId ? "Pilih jenis kelamin" : undefined}
/>
<Select
key="rating"
value={formData.ratingId}
onChange={(val) => setFormData({ ...formData, ratingId: val || "" })}
label={
<Text fw="bold" fz="sm" mb={4}>
Rating
</Text>
}
placeholder='Pilih rating'
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
clearable
searchable
required
radius="md"
error={!formData.ratingId ? "Pilih rating" : undefined}
/>
<Select
key={"kelompokUmur"}
value={formData.kelompokUmurId}
onChange={(val) => setFormData({ ...formData, kelompokUmurId: val || "" })}
label={<Text fw={"bold"} fz={"sm"}>Kelompok Umur</Text>}
placeholder='Pilih kelompok umur'
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({
value: v.id || '',
label: typeof v.name === 'string' ? v.name : 'Tanpa Nama'
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
clearable
searchable
required
error={!formData.kelompokUmurId ? "Pilih kelompok umur" : undefined}
<ControlledSelect
label="Jenis Kelamin"
value={formData.jenisKelaminId}
onChange={(val) => setFormData({ ...formData, jenisKelaminId: val })}
options={(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
loading={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
error={!formData.jenisKelaminId ? 'Pilih jenis kelamin' : undefined}
/>
<ControlledSelect
label="Rating"
value={formData.ratingId}
onChange={(val) => setFormData({ ...formData, ratingId: val })}
options={(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
loading={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
error={!formData.ratingId ? 'Pilih rating' : undefined}
/>
<ControlledSelect
label="Kelompok Umur"
value={formData.kelompokUmurId}
onChange={(val) => setFormData({ ...formData, kelompokUmurId: val })}
options={(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean)
.map((v) => ({ value: v.id || '', label: v.name || 'Tanpa Nama' }))}
loading={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
error={!formData.kelompokUmurId ? 'Pilih kelompok umur' : undefined}
/>
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="red"
onClick={() => router.back()}
>
<Button variant="light" color="red" onClick={() => router.back()}>
Batal
</Button>
<Button
<Button
leftSection={<IconDeviceFloppy size={20} />}
onClick={handleSubmit}
onClick={handleSubmit}
loading={state.update.loading}
color={colors['blue-button']}
>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
@@ -15,57 +16,51 @@ function EditKategoriPrestasi() {
const id = params?.id as string;
const stateKategori = useProxy(prestasiState.kategoriPrestasi);
const [formData, setFormData] = useState({
name: "",
});
const [formData, setFormData] = useState({ name: '' });
const [loading, setLoading] = useState(false);
// Load data kategori prestasi saat component mount
useEffect(() => {
const loadKategoriprestasi = async () => {
if (!id) return;
if (!id) return;
const loadKategori = async () => {
setLoading(true);
try {
const data = await stateKategori.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
stateKategori.edit.id = id;
setFormData({
name: data.name || '',
});
setFormData({ name: data.name || '' });
}
} catch (error) {
console.error("Error loading kategori prestasi desa:", error);
toast.error("Gagal memuat data kategori prestasi desa");
} catch (err) {
console.error(err);
toast.error('Gagal memuat data kategori prestasi desa');
} finally {
setLoading(false);
}
};
loadKategoriprestasi();
loadKategori();
}, [id]);
// Submit: update global state hanya saat submit
const handleSubmit = async () => {
if (!formData.name.trim()) {
toast.error('Nama kategori prestasi tidak boleh kosong');
return;
}
try {
if (!formData.name.trim()) {
toast.error('Nama kategori prestasi tidak boleh kosong');
return;
}
stateKategori.edit.form = {
name: formData.name.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!stateKategori.edit.id) {
stateKategori.edit.id = id; // fallback
}
stateKategori.edit.form = { name: formData.name.trim() };
stateKategori.edit.id ||= id; // fallback jika id belum ada
const success = await stateKategori.edit.update();
if (success) {
router.push("/admin/landing-page/prestasi-desa/kategori-prestasi-desa");
toast.success('Kategori prestasi berhasil diperbarui');
router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa');
}
} catch (error) {
console.error("Error updating kategori prestasi desa:", error);
// toast akan ditampilkan dari fungsi update
} catch (err) {
console.error(err);
toast.error('Gagal memperbarui kategori prestasi desa');
}
};
@@ -94,9 +89,10 @@ function EditKategoriPrestasi() {
<TextInput
label="Nama Kategori Prestasi"
placeholder="Masukkan nama kategori prestasi"
defaultValue={formData.name}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
disabled={loading}
/>
<Group justify="right">
@@ -104,6 +100,7 @@ function EditKategoriPrestasi() {
onClick={handleSubmit}
radius="md"
size="md"
loading={loading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',

View File

@@ -1,19 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { toast } from 'react-toastify';
import colors from '@/con/colors';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import { toast } from 'react-toastify';
interface FormPrestasiDesa {
name: string;
@@ -22,31 +20,31 @@ interface FormPrestasiDesa {
imageId: string;
}
function EditPrestasiDesa() {
const editState = useProxy(prestasiState.prestasiDesa)
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const params = useParams()
const router = useRouter()
export default function EditPrestasiDesa() {
const editState = useProxy(prestasiState.prestasiDesa);
const [formData, setFormData] = useState<FormPrestasiDesa>({
name: '',
deskripsi: '',
kategoriId: '',
imageId: '',
})
});
const [previewFile, setPreviewFile] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const params = useParams();
const router = useRouter();
// Load kategori & prestasi desa
useEffect(() => {
prestasiState.kategoriPrestasi.findMany.load()
const loadDesaAntiKorupsi = async () => {
prestasiState.kategoriPrestasi.findMany.load();
const loadPrestasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.edit.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
editState.edit.id = id;
editState.edit.form = {
name: data.name,
deskripsi: data.deskripsi,
@@ -61,51 +59,37 @@ function EditPrestasiDesa() {
imageId: data.imageId,
});
if (data?.image?.link) {
setPreviewFile(data.image.link)
}
if (data.image?.link) setPreviewFile(data.image.link);
}
} catch (error) {
console.error("Error loading prestasi desa:", error);
toast.error("Gagal memuat data prestasi desa");
console.error('Error loading prestasi desa:', error);
toast.error('Gagal memuat data prestasi desa');
}
}
};
loadDesaAntiKorupsi();
loadPrestasi();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
editState.edit.form = {
...editState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
kategoriId: formData.kategoriId || '',
imageId: formData.imageId // Keep existing imageId if not changed
};
// Jika ada file baru, upload
// Jika ada file baru, upload dulu
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
editState.edit.form.imageId = uploaded.id;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
imageId = uploaded.id;
}
// Update global state sekaligus
editState.edit.form = { ...formData, imageId };
await editState.edit.update();
toast.success("prestasi desa berhasil diperbarui!");
router.push("/admin/landing-page/prestasi-desa/list-prestasi-desa");
toast.success('Prestasi desa berhasil diperbarui!');
router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa');
} catch (error) {
console.error("Error updating prestasi desa:", error);
toast.error("Terjadi kesalahan saat memperbarui prestasi desa");
console.error('Error updating prestasi desa:', error);
toast.error('Terjadi kesalahan saat memperbarui prestasi desa');
}
};
@@ -117,71 +101,35 @@ function EditPrestasiDesa() {
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Prestasi Desa
</Title>
<Title order={4} ml="sm" c="dark">Edit Prestasi Desa</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="md">
<TextInput
label="Judul Prestasi"
placeholder="Masukkan judul prestasi"
defaultValue={formData.name}
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
});
}}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => {
setFormData({
...formData,
deskripsi: val
});
}}
/>
<Text fw="bold" fz="sm" mb={6}>Deskripsi</Text>
<EditEditor value={formData.deskripsi} onChange={(val) => setFormData({ ...formData, deskripsi: val })} />
</Box>
<Select
label="Kategori"
placeholder="Pilih kategori"
value={formData.kategoriId}
onChange={(val) => {
setFormData({
...formData,
kategoriId: val ?? ""
});
}}
data={
prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({
value: v.id,
label: v.name,
})) || []
}
onChange={(val) => setFormData({ ...formData, kategoriId: val ?? '' })}
data={prestasiState.kategoriPrestasi.findMany.data?.map((v) => ({ value: v.id, label: v.name })) || []}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Prestasi
</Text>
<Text fw="bold" fz="sm" mb={6}>Gambar Prestasi</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
@@ -197,22 +145,12 @@ function EditPrestasiDesa() {
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Dropzone.Accept><IconUpload size={48} color={colors['blue-button']} stroke={1.5} /></Dropzone.Accept>
<Dropzone.Reject><IconX size={48} color="red" stroke={1.5} /></Dropzone.Reject>
<Dropzone.Idle><IconPhoto size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
<Text size="md" fw={500}>Seret gambar atau klik untuk memilih file</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar wajib</Text>
</Stack>
</Group>
</Dropzone>
@@ -224,7 +162,7 @@ function EditPrestasiDesa() {
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading='lazy'
loading="lazy"
/>
</Box>
)}
@@ -249,6 +187,3 @@ function EditPrestasiDesa() {
</Box>
);
}
export default EditPrestasiDesa;

View File

@@ -14,7 +14,7 @@ import {
TextInput,
Title,
Tooltip,
Image
Image,
} from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconDeviceFloppy, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
@@ -23,37 +23,35 @@ import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditKolaborasiInovasi() {
export default function EditKolaborasiInovasi() {
const sdgsState = useProxy(sdgsDesa);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: "",
jumlah: "",
imageId: "",
});
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: sdgsState.edit.form.name || '',
jumlah: sdgsState.edit.form.jumlah || '',
imageId: sdgsState.edit.form.imageId || ''
});
// Load sdgs desa by id saat pertama kali
// Load data sdgs desa by id
useEffect(() => {
const loadKolaborasi = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await sdgsState.edit.load(id); // akses langsung, bukan dari proxy
const data = await sdgsState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
jumlah: data.jumlah || '',
imageId: data.imageId || '',
name: data.name || "",
jumlah: data.jumlah || "",
imageId: data.imageId || "",
});
if (data.image) {
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
if (data.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
@@ -65,31 +63,26 @@ function EditKolaborasiInovasi() {
loadKolaborasi();
}, [params?.id]);
const handleInputChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
// edit global state with form data
sdgsState.edit.form = {
...sdgsState.edit.form,
name: formData.name,
jumlah: formData.jumlah,
imageId: formData.imageId // Keep existing imageId if not changed
};
let imageId = formData.imageId;
// Jika ada file baru, upload
// Upload file baru jika ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// edit imageId in global state
sdgsState.edit.form.imageId = uploaded.id;
if (!uploaded?.id) return toast.error("Gagal upload gambar");
imageId = uploaded.id;
}
// Update global state hanya saat submit
sdgsState.edit.form = { ...sdgsState.edit.form, ...formData, imageId };
await sdgsState.edit.update();
toast.success("sdgs desa berhasil diperbarui!");
router.push("/admin/landing-page/sdgs-desa");
} catch (error) {
@@ -99,11 +92,11 @@ function EditKolaborasiInovasi() {
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
@@ -112,12 +105,12 @@ function EditKolaborasiInovasi() {
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<Box>
@@ -132,15 +125,15 @@ function EditKolaborasiInovasi() {
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
onReject={() => toast.error("File tidak valid, gunakan format gambar")}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
accept={{ "image/*": [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
@@ -160,12 +153,12 @@ function EditKolaborasiInovasi() {
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
style={{ maxHeight: 220, objectFit: "contain", border: `1px solid ${colors["blue-button"]}` }}
loading="lazy"
/>
</Box>
@@ -175,16 +168,16 @@ function EditKolaborasiInovasi() {
<TextInput
label="Nama Sdgs Desa"
placeholder="Masukkan nama Sdgs Desa"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
required
/>
<TextInput
label="Jumlah"
placeholder="Masukkan jumlah"
defaultValue={formData.jumlah}
onChange={(e) => setFormData({ ...formData, jumlah: e.target.value })}
value={formData.jumlah}
onChange={(e) => handleInputChange("jumlah", e.target.value)}
required
type="number"
/>
@@ -197,9 +190,9 @@ function EditKolaborasiInovasi() {
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`,
color: "#fff",
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)",
}}
>
Simpan
@@ -210,5 +203,3 @@ function EditKolaborasiInovasi() {
</Box>
);
}
export default EditKolaborasiInovasi;

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import dataLingkunganDesaState from '@/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa';
import colors from '@/con/colors';
@@ -46,7 +47,7 @@ type IconKey =
| 'pohon'
| 'air';
function EditDataLingkunganDesa() {
export default function EditDataLingkunganDesa() {
const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState);
const params = useParams();
const router = useRouter();
@@ -58,8 +59,9 @@ function EditDataLingkunganDesa() {
icon: '',
});
// Load data saat komponen mount
useEffect(() => {
const loadProgramKreatif = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
@@ -67,14 +69,6 @@ function EditDataLingkunganDesa() {
const data = await stateDataLingkunganDesa.update.load(id);
if (data) {
stateDataLingkunganDesa.update.id = id;
stateDataLingkunganDesa.update.form = {
name: data.name,
deskripsi: data.deskripsi,
jumlah: data.jumlah,
icon: data.icon,
};
setFormData({
name: data.name,
deskripsi: data.deskripsi,
@@ -88,13 +82,14 @@ function EditDataLingkunganDesa() {
}
};
loadProgramKreatif();
loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state hanya saat submit
stateDataLingkunganDesa.update.form = {
...stateDataLingkunganDesa.update.form,
...formData,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
jumlah: formData.jumlah.trim(),
@@ -132,27 +127,21 @@ function EditDataLingkunganDesa() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.name}
value={formData.name}
label={<Text fz="sm" fw="bold">Nama Data Lingkungan Desa</Text>}
placeholder="Masukkan nama data lingkungan desa"
onChange={(val) =>
setFormData({
...formData,
name: val.target.value,
})
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
required
/>
<TextInput
defaultValue={formData.jumlah}
value={formData.jumlah}
label={<Text fz="sm" fw="bold">Jumlah Data Lingkungan Desa</Text>}
placeholder="Masukkan jumlah data lingkungan desa"
onChange={(val) =>
setFormData({
...formData,
jumlah: val.target.value,
})
onChange={(e) =>
setFormData((prev) => ({ ...prev, jumlah: e.target.value }))
}
required
/>
@@ -163,10 +152,9 @@ function EditDataLingkunganDesa() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateDataLingkunganDesa.update.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
@@ -176,10 +164,9 @@ function EditDataLingkunganDesa() {
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateDataLingkunganDesa.update.form.icon = value;
}}
onChange={(value) =>
setFormData((prev) => ({ ...prev, icon: value }))
}
/>
</Box>
@@ -202,5 +189,3 @@ function EditDataLingkunganDesa() {
</Box>
);
}
export default EditDataLingkunganDesa;

View File

@@ -1,4 +1,5 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
@@ -10,48 +11,67 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(
() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor),
() =>
import('../../_lib/edukasiLingkunganTextEditor').then(
(mod) => mod.EdukasiLingkunganTextEditor
),
{ ssr: false }
);
function EditContohKegiatanDesaDarmasaba() {
export default function EditContohKegiatanDesaDarmasaba() {
const router = useRouter();
const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
const contohEdukasiState = useProxy(
stateEdukasiLingkungan.stateContohEdukasiLingkungan
);
// state lokal untuk form
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
});
// load data awal
useShallowEffect(() => {
if (!contohEdukasiState.findById.data) {
contohEdukasiState.findById.initialize();
}
}, []);
// update state lokal saat data global sudah ada
useEffect(() => {
if (contohEdukasiState.findById.data) {
setJudul(contohEdukasiState.findById.data.judul ?? '');
setContent(contohEdukasiState.findById.data.deskripsi ?? '');
setFormData({
judul: contohEdukasiState.findById.data.judul ?? '',
deskripsi: contohEdukasiState.findById.data.deskripsi ?? '',
});
}
}, [contohEdukasiState.findById.data]);
const submit = () => {
// handler perubahan input
const handleChange = (field: 'judul' | 'deskripsi', value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
// submit update
const handleSubmit = () => {
if (contohEdukasiState.findById.data) {
contohEdukasiState.findById.data.judul = judul;
contohEdukasiState.findById.data.deskripsi = content;
contohEdukasiState.update.save(contohEdukasiState.findById.data);
const updatedData = {
...contohEdukasiState.findById.data,
judul: formData.judul,
deskripsi: formData.deskripsi,
};
contohEdukasiState.update.save(updatedData);
}
router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba');
router.push(
'/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba'
);
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
@@ -75,8 +95,8 @@ function EditContohKegiatanDesaDarmasaba() {
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
onChange={(value) => handleChange('judul', value)}
initialContent={formData.judul}
/>
</Box>
@@ -86,14 +106,14 @@ function EditContohKegiatanDesaDarmasaba() {
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
onChange={(value) => handleChange('deskripsi', value)}
initialContent={formData.deskripsi}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
onClick={handleSubmit}
radius="md"
size="md"
style={{
@@ -111,5 +131,3 @@ function EditContohKegiatanDesaDarmasaba() {
</Box>
);
}
export default EditContohKegiatanDesaDarmasaba;

View File

@@ -14,30 +14,43 @@ const EdukasiLingkunganTextEditor = dynamic(
{ ssr: false }
);
function EditMateriEdukasiYangDiberikan() {
export default function EditMateriEdukasiYangDiberikan() {
const router = useRouter();
const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
// State lokal gabungan untuk form
const [formData, setFormData] = useState({ judul: '', content: '' });
// Initialize data kalau belum ada
useShallowEffect(() => {
if (!materiEdukasiState.findById.data) {
materiEdukasiState.findById.initialize();
}
}, []);
// Set formData saat data tersedia
useEffect(() => {
if (materiEdukasiState.findById.data) {
setJudul(materiEdukasiState.findById.data.judul ?? '');
setContent(materiEdukasiState.findById.data.deskripsi ?? '');
setFormData({
judul: materiEdukasiState.findById.data.judul ?? '',
content: materiEdukasiState.findById.data.deskripsi ?? '',
});
}
}, [materiEdukasiState.findById.data]);
// Handler perubahan form
const handleChange = (field: 'judul' | 'content', value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const submit = () => {
if (materiEdukasiState.findById.data) {
materiEdukasiState.findById.data.judul = judul;
materiEdukasiState.findById.data.deskripsi = content;
materiEdukasiState.update.save(materiEdukasiState.findById.data);
const updatedData = {
...materiEdukasiState.findById.data,
judul: formData.judul,
deskripsi: formData.content,
};
materiEdukasiState.update.save(updatedData);
}
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan');
};
@@ -66,14 +79,22 @@ function EditMateriEdukasiYangDiberikan() {
<Text fw="bold" mb={6}>
Judul
</Text>
<EdukasiLingkunganTextEditor showSubmit={false} onChange={setJudul} initialContent={judul} />
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={(val) => handleChange('judul', val)}
initialContent={formData.judul}
/>
</Box>
<Box>
<Text fw="bold" mb={6}>
Konten
</Text>
<EdukasiLingkunganTextEditor showSubmit={false} onChange={setContent} initialContent={content} />
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={(val) => handleChange('content', val)}
initialContent={formData.content}
/>
</Box>
<Group justify="right" mt="md">
@@ -96,5 +117,3 @@ function EditMateriEdukasiYangDiberikan() {
</Box>
);
}
export default EditMateriEdukasiYangDiberikan;

View File

@@ -1,4 +1,5 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
@@ -14,30 +15,44 @@ const EdukasiLingkunganTextEditor = dynamic(
{ ssr: false }
);
function EditTujuanEdukasiLingkungan() {
export default function EditTujuanEdukasiLingkungan() {
const router = useRouter();
const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
// State lokal untuk form, gak tergantung global state
const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
// Initialize global state
useShallowEffect(() => {
if (!tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.initialize();
}
}, []);
// Sync initial values dari global state ke form lokal
useEffect(() => {
if (tujuanEdukasiState.findById.data) {
setJudul(tujuanEdukasiState.findById.data.judul ?? '');
setContent(tujuanEdukasiState.findById.data.deskripsi ?? '');
setFormData({
judul: tujuanEdukasiState.findById.data.judul ?? '',
deskripsi: tujuanEdukasiState.findById.data.deskripsi ?? '',
});
}
}, [tujuanEdukasiState.findById.data]);
// Handler input
const handleChange = (field: 'judul' | 'deskripsi', value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const submit = () => {
if (tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.data.judul = judul;
tujuanEdukasiState.findById.data.deskripsi = content;
tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data);
// Update global state hanya saat submit
const updatedData = {
...tujuanEdukasiState.findById.data,
judul: formData.judul,
deskripsi: formData.deskripsi,
};
tujuanEdukasiState.update.save(updatedData);
}
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan');
};
@@ -73,8 +88,8 @@ function EditTujuanEdukasiLingkungan() {
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
onChange={(value) => handleChange('judul', value)}
initialContent={formData.judul}
/>
</Box>
@@ -84,8 +99,8 @@ function EditTujuanEdukasiLingkungan() {
</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
onChange={(value) => handleChange('deskripsi', value)}
initialContent={formData.deskripsi}
/>
</Box>
@@ -109,5 +124,3 @@ function EditTujuanEdukasiLingkungan() {
</Box>
);
}
export default EditTujuanEdukasiLingkungan;

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
@@ -15,53 +16,59 @@ function EditKategoriKegiatan() {
const id = params?.id as string;
const stateKategori = useProxy(gotongRoyongState.kategoriKegiatan);
const [formData, setFormData] = useState({
nama: '',
});
const [formData, setFormData] = useState({ nama: '' });
const [loading, setLoading] = useState(true);
// Load data once
useEffect(() => {
const loadKategorikegiatan = async () => {
if (!id) return;
if (!id) return;
const loadKategori = async () => {
try {
const data = await stateKategori.edit.load(id);
if (data) {
stateKategori.edit.id = id;
setFormData({
nama: data.nama || '',
});
setFormData({ nama: data.nama || '' });
}
} catch (error) {
console.error('Error loading kategori kegiatan:', error);
} catch (err) {
console.error('Error loading kategori kegiatan:', err);
toast.error('Gagal memuat data kategori kegiatan');
} finally {
setLoading(false);
}
};
loadKategorikegiatan();
loadKategori();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama kategori kegiatan tidak boleh kosong');
return;
}
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
stateKategori.edit.form = { nama: formData.nama.trim() };
const handleSubmit = async () => {
const trimmedNama = formData.nama.trim();
if (!trimmedNama) {
toast.error('Nama kategori kegiatan tidak boleh kosong');
return;
}
try {
stateKategori.edit.form = { nama: trimmedNama };
if (!stateKategori.edit.id) stateKategori.edit.id = id;
const success = await stateKategori.edit.update();
if (success) {
toast.success('Kategori kegiatan berhasil diperbarui!');
router.push('/admin/lingkungan/gotong-royong/kategori-kegiatan');
}
} catch (error) {
console.error('Error updating kategori kegiatan:', error);
} catch (err) {
console.error('Error updating kategori kegiatan:', err);
toast.error('Gagal memperbarui kategori kegiatan');
}
};
if (loading) return <Text>Loading...</Text>;
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md" align="center">
@@ -85,8 +92,8 @@ function EditKategoriKegiatan() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
value={formData.nama}
onChange={(e) => handleChange('nama', e.target.value)}
label={<Text fw="bold" fz="sm">Nama Kategori Kegiatan</Text>}
placeholder="Masukkan nama kategori kegiatan"
required

View File

@@ -4,7 +4,19 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import gotongRoyongState from '@/app/admin/(dashboard)/_state/lingkungan/gotong-royong';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -23,12 +35,10 @@ interface FormKegiatanDesa {
kategoriKegiatanId: string;
}
function EditGotongRoyong() {
const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa)
const params = useParams()
const router = useRouter()
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
export default function EditKegiatanDesa() {
const kegiatanDesaState = useProxy(gotongRoyongState.kegiatanDesa);
const params = useParams();
const router = useRouter();
const [formData, setFormData] = useState<FormKegiatanDesa>({
judul: '',
@@ -39,16 +49,19 @@ function EditGotongRoyong() {
partisipan: 0,
imageId: '',
kategoriKegiatanId: '',
})
});
const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0];
return new Date(dateString).toISOString().split('T')[0];
};
// Load kategori & data kegiatan
useEffect(() => {
const loadKegiatanDesa = async () => {
const loadData = async () => {
gotongRoyongState.kategoriKegiatan.findMany.load();
const id = params?.id as string;
if (!id) return;
@@ -65,43 +78,48 @@ function EditGotongRoyong() {
imageId: data.imageId || '',
kategoriKegiatanId: data.kategoriKegiatanId || '',
});
if (data.imageId) {
// Optional: bisa fetch URL image dari backend
setPreviewImage(`/api/file/${data.imageId}`);
}
}
} catch (error) {
console.error("Error loading kegiatan desa:", error);
toast.error("Gagal memuat data kegiatan desa");
console.error(error);
toast.error('Gagal memuat data kegiatan desa');
}
}
gotongRoyongState.kategoriKegiatan.findMany.load()
loadKegiatanDesa();
};
loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
kegiatanDesaState.edit.form = {
...kegiatanDesaState.edit.form,
judul: formData.judul.trim(),
deskripsiSingkat: formData.deskripsiSingkat.trim(),
deskripsiLengkap: formData.deskripsiLengkap.trim(),
tanggal: new Date(formData.tanggal.trim()),
lokasi: formData.lokasi.trim(),
partisipan: formData.partisipan,
imageId: formData.imageId,
kategoriKegiatanId: formData.kategoriKegiatanId,
}
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error("Gagal upload gambar");
kegiatanDesaState.edit.form.imageId = uploaded.id;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
imageId = uploaded.id;
}
await kegiatanDesaState.edit.update()
toast.success("Kegiatan desa berhasil diperbarui!")
router.push("/admin/lingkungan/gotong-royong/kegiatan-desa");
kegiatanDesaState.edit.form = {
judul: formData.judul.trim(),
deskripsiSingkat: formData.deskripsiSingkat.trim(),
deskripsiLengkap: formData.deskripsiLengkap.trim(),
tanggal: new Date(formData.tanggal),
lokasi: formData.lokasi.trim(),
partisipan: formData.partisipan,
imageId,
kategoriKegiatanId: formData.kategoriKegiatanId,
};
await kegiatanDesaState.edit.update();
toast.success('Kegiatan desa berhasil diperbarui!');
router.push('/admin/lingkungan/gotong-royong/kegiatan-desa');
} catch (error) {
console.error("Error updating kegiatan desa:", error);
toast.error("Terjadi kesalahan saat memperbarui kegiatan desa");
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui kegiatan desa');
}
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -126,14 +144,14 @@ function EditGotongRoyong() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.judul}
value={formData.judul}
label={<Text fz="sm" fw="bold">Judul Kegiatan Desa</Text>}
placeholder="masukkan judul kegiatan desa"
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
required
/>
<TextInput
defaultValue={formData.deskripsiSingkat}
value={formData.deskripsiSingkat}
label={<Text fz="sm" fw="bold">Deskripsi Singkat Kegiatan Desa</Text>}
placeholder="masukkan deskripsi singkat kegiatan desa"
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
@@ -148,33 +166,27 @@ function EditGotongRoyong() {
value={formData.kategoriKegiatanId}
onChange={(val) => setFormData({ ...formData, kategoriKegiatanId: val ?? '' })}
/>
<Box>
<Text fw="bold" fz="sm">Deskripsi Lengkap Kegiatan Desa</Text>
<EditEditor
value={formData.deskripsiLengkap}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsiLengkap: htmlContent }));
kegiatanDesaState.edit.form.deskripsiLengkap = htmlContent;
}}
onChange={(htmlContent) => setFormData(prev => ({ ...prev, deskripsiLengkap: htmlContent }))}
/>
</Box>
<TextInput
label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>}
placeholder="masukkan tanggal kegiatan desa"
type="date"
defaultValue={formatDateForInput(formData.tanggal)}
label={<Text fz="sm" fw="bold">Tanggal Kegiatan Desa</Text>}
value={formatDateForInput(formData.tanggal)}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
/>
<TextInput
defaultValue={formData.lokasi}
value={formData.lokasi}
label={<Text fz="sm" fw="bold">Lokasi Kegiatan Desa</Text>}
placeholder="masukkan lokasi kegiatan desa"
onChange={(e) => setFormData({ ...formData, lokasi: e.target.value })}
/>
<TextInput
defaultValue={formData.partisipan}
value={formData.partisipan}
label={<Text fz="sm" fw="bold">Partisipan Kegiatan Desa</Text>}
placeholder="masukkan partisipan kegiatan desa"
onChange={(e) => {
@@ -187,11 +199,10 @@ function EditGotongRoyong() {
<Text fw="bold" fz="sm" mb={6}>Gambar Kegiatan Desa</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
const selected = files[0];
if (!selected) return;
setFile(selected);
setPreviewImage(URL.createObjectURL(selected));
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
@@ -248,5 +259,3 @@ function EditGotongRoyong() {
</Box>
);
}
export default EditGotongRoyong;

View File

@@ -17,9 +17,11 @@ const KonservasiAdatBaliTextEditor = dynamic(
function EditBentukKonservasiBerdasarkanAdat() {
const router = useRouter();
const bentukKonservasiState = useProxy(stateKonservasiAdatBali.stateBentukKonservasiBerdasarkanAdat);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
// Gabung semua field form jadi satu object
const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
// Initialize data dari global state
useShallowEffect(() => {
if (!bentukKonservasiState.findById.data) {
bentukKonservasiState.findById.initialize();
@@ -28,16 +30,22 @@ function EditBentukKonservasiBerdasarkanAdat() {
useEffect(() => {
if (bentukKonservasiState.findById.data) {
setJudul(bentukKonservasiState.findById.data.judul ?? '');
setContent(bentukKonservasiState.findById.data.deskripsi ?? '');
setFormData({
judul: bentukKonservasiState.findById.data.judul ?? '',
deskripsi: bentukKonservasiState.findById.data.deskripsi ?? '',
});
}
}, [bentukKonservasiState.findById.data]);
const handleChange = (field: 'judul' | 'deskripsi', value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const submit = () => {
if (bentukKonservasiState.findById.data) {
bentukKonservasiState.findById.data.judul = judul;
bentukKonservasiState.findById.data.deskripsi = content;
bentukKonservasiState.update.save(bentukKonservasiState.findById.data);
// Update global state cuma pas submit
const updatedData = { ...bentukKonservasiState.findById.data, ...formData };
bentukKonservasiState.update.save(updatedData);
}
router.push('/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat');
};
@@ -46,12 +54,7 @@ function EditBentukKonservasiBerdasarkanAdat() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
@@ -75,8 +78,8 @@ function EditBentukKonservasiBerdasarkanAdat() {
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
onChange={(value: string) => handleChange('judul', value)}
initialContent={formData.judul}
/>
</Box>
@@ -86,8 +89,8 @@ function EditBentukKonservasiBerdasarkanAdat() {
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
onChange={(value: string) => handleChange('deskripsi', value)}
initialContent={formData.deskripsi}
/>
</Box>

View File

@@ -1,4 +1,5 @@
'use client'
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
@@ -20,26 +21,35 @@ const KonservasiAdatBaliTextEditor = dynamic(
function EditFilosofiTriHitaKarana() {
const router = useRouter();
const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
// Local state form
const [formData, setFormData] = useState({ judul: '', content: '' });
// Load data dari global state kalau belum ada
useShallowEffect(() => {
if (!filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.initialize();
}
}, []);
// Set formData dari global state saat data tersedia
useEffect(() => {
if (filosofiTriHitaState.findById.data) {
setJudul(filosofiTriHitaState.findById.data.judul ?? '');
setContent(filosofiTriHitaState.findById.data.deskripsi ?? '');
setFormData({
judul: filosofiTriHitaState.findById.data.judul ?? '',
content: filosofiTriHitaState.findById.data.deskripsi ?? '',
});
}
}, [filosofiTriHitaState.findById.data]);
const submit = () => {
const handleChange = (field: 'judul' | 'content', value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = () => {
if (filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.data.judul = judul;
filosofiTriHitaState.findById.data.deskripsi = content;
filosofiTriHitaState.findById.data.judul = formData.judul;
filosofiTriHitaState.findById.data.deskripsi = formData.content;
filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data);
}
router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana');
@@ -49,12 +59,7 @@ function EditFilosofiTriHitaKarana() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
@@ -78,8 +83,8 @@ function EditFilosofiTriHitaKarana() {
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
onChange={(val) => handleChange('judul', val)}
initialContent={formData.judul}
/>
</Box>
@@ -89,14 +94,14 @@ function EditFilosofiTriHitaKarana() {
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
onChange={(val) => handleChange('content', val)}
initialContent={formData.content}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={submit}
onClick={handleSubmit}
radius="md"
size="md"
style={{

View File

@@ -17,26 +17,36 @@ const KonservasiAdatBaliTextEditor = dynamic(
function EditNilaiKonservasiAdat() {
const router = useRouter();
const nilaiKonservasiState = useProxy(stateKonservasiAdatBali.stateNilaiKonservasiAdat);
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
// state lokal untuk form
const [formData, setFormData] = useState({ judul: '', deskripsi: '' });
// load data awal
useShallowEffect(() => {
if (!nilaiKonservasiState.findById.data) {
nilaiKonservasiState.findById.initialize();
}
}, []);
// sync state lokal saat data backend siap
useEffect(() => {
if (nilaiKonservasiState.findById.data) {
setJudul(nilaiKonservasiState.findById.data.judul ?? '');
setContent(nilaiKonservasiState.findById.data.deskripsi ?? '');
setFormData({
judul: nilaiKonservasiState.findById.data.judul ?? '',
deskripsi: nilaiKonservasiState.findById.data.deskripsi ?? '',
});
}
}, [nilaiKonservasiState.findById.data]);
const handleChange = (field: 'judul' | 'deskripsi', value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const submit = () => {
if (nilaiKonservasiState.findById.data) {
nilaiKonservasiState.findById.data.judul = judul;
nilaiKonservasiState.findById.data.deskripsi = content;
// update global state saat submit
nilaiKonservasiState.findById.data.judul = formData.judul;
nilaiKonservasiState.findById.data.deskripsi = formData.deskripsi;
nilaiKonservasiState.update.save(nilaiKonservasiState.findById.data);
}
router.push('/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat');
@@ -46,12 +56,7 @@ function EditNilaiKonservasiAdat() {
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
@@ -75,8 +80,8 @@ function EditNilaiKonservasiAdat() {
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
onChange={val => handleChange('judul', val)}
initialContent={formData.judul}
/>
</Box>
@@ -86,8 +91,8 @@ function EditNilaiKonservasiAdat() {
</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
onChange={val => handleChange('deskripsi', val)}
initialContent={formData.deskripsi}
/>
</Box>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
@@ -13,9 +14,9 @@ import { useProxy } from 'valtio/utils';
const LeafletMapEdit = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapEdit'), { ssr: false });
function EditKeteranganBankSampahTerdekat() {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const router = useRouter();
const params = useParams()
const params = useParams();
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
const [formData, setFormData] = useState({
@@ -24,10 +25,11 @@ function EditKeteranganBankSampahTerdekat() {
namaTempatMaps: '',
lat: 0,
lng: 0,
})
});
// Load data ketika component mount
useEffect(() => {
const loadKeteranganBankSampahTerdekat = async () => {
const loadKeterangan = async () => {
const id = params?.id as string;
if (!id) return;
@@ -35,14 +37,8 @@ function EditKeteranganBankSampahTerdekat() {
const data = await keteranganState.edit.load(id);
if (data) {
keteranganState.edit.id = id;
keteranganState.edit.form = {
name: data.name,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
};
// Update local formData dan markerPosition
setFormData({
name: data.name,
alamat: data.alamat,
@@ -50,51 +46,47 @@ function EditKeteranganBankSampahTerdekat() {
lat: data.lat,
lng: data.lng,
});
setMarkerPosition({ lat: data.lat, lng: data.lng });
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
console.error(error);
toast.error('Gagal memuat data pengelolaan sampah');
}
}
};
loadKeteranganBankSampahTerdekat();
loadKeterangan();
}, [params?.id]);
const handleInputChange = (field: keyof typeof formData, value: string | number) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => {
try {
if (!formData.name.trim()) {
return toast.error('Nama bank sampah harus diisi');
}
if (!formData.alamat.trim()) {
return toast.error('Alamat harus diisi');
}
if (!formData.namaTempatMaps.trim()) {
return toast.error('Nama tempat di Maps harus diisi');
}
if (!markerPosition) {
return toast.error('Silakan pilih lokasi di peta');
}
if (!formData.name.trim()) return toast.error('Nama bank sampah harus diisi');
if (!formData.alamat.trim()) return toast.error('Alamat harus diisi');
if (!formData.namaTempatMaps.trim()) return toast.error('Nama tempat di Maps harus diisi');
if (!markerPosition) return toast.error('Silakan pilih lokasi di peta');
// Update global state hanya saat submit
keteranganState.edit.form = {
...keteranganState.edit.form,
name: formData.name.trim(),
alamat: formData.alamat.trim(),
namaTempatMaps: formData.namaTempatMaps.trim(),
lat: markerPosition.lat,
lng: markerPosition.lng,
};
await keteranganState.edit.update();
toast.success('Data bank sampah berhasil diperbarui');
keteranganState.findUnique.data = null;
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) {
console.error("Error updating pengelolaan sampah:", error);
console.error(error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah');
}
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
@@ -120,45 +112,35 @@ function EditKeteranganBankSampahTerdekat() {
<TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
defaultValue={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
defaultValue={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
value={formData.alamat}
onChange={(e) => handleInputChange('alamat', e.target.value)}
required
/>
<TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
defaultValue={formData.namaTempatMaps}
onChange={(e) => setFormData({ ...formData, namaTempatMaps: e.target.value })}
value={formData.namaTempatMaps}
onChange={(e) => handleInputChange('namaTempatMaps', e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Text fw="bold" fz="sm" mb={6}>Pilih Lokasi di Peta</Text>
<Text fz="xs" c="dimmed" mb={4}>Klik pada peta untuk menandai lokasi</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMapEdit
key={markerPosition?.lat ?? 'default'}
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
onChange={(pos) => {
setMarkerPosition(pos);
setFormData(prev => ({
...prev,
lat: pos.lat,
lng: pos.lng,
}));
handleInputChange('lat', pos.lat);
handleInputChange('lng', pos.lng);
}}
/>
</Box>

View File

@@ -17,15 +17,16 @@ interface FormProgramKreatif {
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash';
function EditProgramKreatifDesa() {
const stateSampah = useProxy(pengelolaanSampahState.pengelolaanSampah)
const params = useParams()
const router = useRouter();
// State lokal untuk form controlled
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
icon: '',
})
});
useEffect(() => {
const loadProgramKreatif = async () => {
@@ -35,14 +36,7 @@ function EditProgramKreatifDesa() {
try {
const data = await stateSampah.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateSampah.update.id = id;
stateSampah.update.form = {
name: data.name,
icon: data.icon,
};
stateSampah.update.id = id; // simpan id di global state
setFormData({
name: data.name,
icon: data.icon,
@@ -52,21 +46,19 @@ function EditProgramKreatifDesa() {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
};
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state HANYA saat submit
stateSampah.update.form = {
...stateSampah.update.form,
name: formData.name.trim(),
icon: formData.icon.trim(),
};
await stateSampah.update.submit();
toast.success('Data pengelolaan sampah berhasil diperbarui!');
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
@@ -74,17 +66,13 @@ function EditProgramKreatifDesa() {
console.error("Error updating pengelolaan sampah:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah");
}
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
@@ -105,15 +93,8 @@ function EditProgramKreatifDesa() {
<TextInput
label="Nama Pengelolaan Sampah"
placeholder="Masukkan nama pengelolaan sampah"
defaultValue={formData.name}
onChange={(e) => {
const value = e.target.value;
setFormData(prev => ({
...prev,
name: value
}));
stateSampah.update.form.name = value;
}}
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
required
/>
@@ -123,10 +104,7 @@ function EditProgramKreatifDesa() {
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData(prev => ({ ...prev, icon: value }));
stateSampah.update.form.icon = value;
}}
onChange={(value) => setFormData(prev => ({ ...prev, icon: value }))}
/>
</Box>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
@@ -50,13 +51,14 @@ function EditProgramPenghijauan() {
const [formData, setFormData] = useState<FormProgramPenghijauan>({
name: '',
deskripsi: '',
judul: '',
deskripsi: '',
icon: '',
});
// Load data program penghijauan
useEffect(() => {
const loadProgramPenghijauan = async () => {
const loadProgram = async () => {
const id = params?.id as string;
if (!id) return;
@@ -64,14 +66,6 @@ function EditProgramPenghijauan() {
const data = await stateProgramPenghijauan.update.load(id);
if (data) {
stateProgramPenghijauan.update.id = id;
stateProgramPenghijauan.update.form = {
name: data.name,
judul: data.judul,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
judul: data.judul,
@@ -85,19 +79,21 @@ function EditProgramPenghijauan() {
}
};
loadProgramPenghijauan();
loadProgram();
}, [params?.id]);
// Hanya update global state saat submit
const handleSubmit = async () => {
try {
stateProgramPenghijauan.update.form = {
...stateProgramPenghijauan.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
judul: formData.judul.trim(),
deskripsi: formData.deskripsi.trim(),
icon: formData.icon.trim(),
};
await stateProgramPenghijauan.update.submit();
toast.success('Program penghijauan berhasil diperbarui');
router.push('/admin/lingkungan/program-penghijauan');
} catch (error) {
console.error('Error updating program penghijauan:', error);
@@ -107,7 +103,7 @@ function EditProgramPenghijauan() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
@@ -135,27 +131,21 @@ function EditProgramPenghijauan() {
>
<Stack gap="md">
<TextInput
defaultValue={formData.name}
value={formData.name}
label="Nama Program Penghijauan"
placeholder="Masukkan nama program penghijauan"
onChange={(val) =>
setFormData({
...formData,
name: val.target.value,
})
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
required
/>
<TextInput
defaultValue={formData.judul}
value={formData.judul}
label="Judul Deskripsi Program Penghijauan"
placeholder="Masukkan judul deskripsi program penghijauan"
onChange={(val) =>
setFormData({
...formData,
judul: val.target.value,
})
onChange={(e) =>
setFormData((prev) => ({ ...prev, judul: e.target.value }))
}
required
/>
@@ -166,10 +156,9 @@ function EditProgramPenghijauan() {
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateProgramPenghijauan.update.form.deskripsi = htmlContent;
}}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
@@ -179,10 +168,9 @@ function EditProgramPenghijauan() {
</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramPenghijauan.update.form.icon = value;
}}
onChange={(value) =>
setFormData((prev) => ({ ...prev, icon: value }))
}
/>
</Box>