Fix QC Kak Inno Admin, Fix QC Keano UI User, Fix QC Pak jun tabel apbdes

This commit is contained in:
2025-11-12 17:42:31 +08:00
parent 417a8937f5
commit 9622eb5a9a
354 changed files with 11444 additions and 4012 deletions

View File

@@ -7,6 +7,7 @@ import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align'; import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript'; import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript'; import SubScript from '@tiptap/extension-subscript';
import { useEffect } from 'react';
type CreateEditorProps = { type CreateEditorProps = {
value: string; value: string;
@@ -32,6 +33,13 @@ export default function CreateEditor({ value, onChange }: CreateEditorProps) {
}, },
}); });
// 👇 Tambahkan efek untuk sinkronisasi value dari luar (resetForm)
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value || '');
}
}, [value, editor]);
return ( return (
<RichTextEditor editor={editor}> <RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)"> <RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">

View File

@@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) {
editor.off('update', updateHandler); editor.off('update', updateHandler);
}; };
}, [editor, onChange]); }, [editor, onChange]);
return ( return (
<RichTextEditor editor={editor}> <RichTextEditor editor={editor}>

View File

@@ -1,6 +1,6 @@
'use client' 'use client';
import { Box, rem, Select } from '@mantine/core'; import { Box, Group, rem, Select, SelectProps } from '@mantine/core';
import { import {
IconAmbulance, IconAmbulance,
IconCash, IconCash,
@@ -25,7 +25,7 @@ import {
IconTrophy, IconTrophy,
IconTruckFilled, IconTruckFilled,
IconBuilding, IconBuilding,
IconAlertTriangle IconAlertTriangle,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
const iconMap = { const iconMap = {
@@ -38,26 +38,26 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale }, scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled }, trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: {label: 'Rumah', icon: IconHome}, rumah: { label: 'Rumah', icon: IconHome },
pohon: {label: 'Pohon', icon: IconTree}, pohon: { label: 'Pohon', icon: IconTree },
air: {label: 'Air', icon: IconDroplet}, air: { label: 'Air', icon: IconDroplet },
bantuan: {label: 'Bantuan', icon: IconCash}, bantuan: { label: 'Bantuan', icon: IconCash },
pelatihan: {label: 'Pelatihan', icon: IconSchool}, pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: {label: 'Subsidi', icon: IconShoppingCart}, subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital}, layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
polisi: {label: 'Polisi', icon: IconShieldFilled}, polisi: { label: 'Polisi', icon: IconShieldFilled },
ambulans: {label: 'Ambulans', icon: IconAmbulance}, ambulans: { label: 'Ambulans', icon: IconAmbulance },
pemadam: {label: 'Pemadam', icon: IconFiretruck}, pemadam: { label: 'Pemadam', icon: IconFiretruck },
rumahSakit: {label: 'Rumah Sakit', icon: IconHospital}, rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
bangunan: {label: 'Bangunan', icon: IconBuilding}, bangunan: { label: 'Bangunan', icon: IconBuilding },
darurat: {label: 'Darurat', icon: IconAlertTriangle}, darurat: { label: 'Darurat', icon: IconAlertTriangle },
}; };
type IconKey = keyof typeof iconMap; export type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({ const iconList = Object.entries(iconMap).map(([value, data]) => ({
value, value,
@@ -67,44 +67,52 @@ const iconList = Object.entries(iconMap).map(([value, data]) => ({
export default function SelectIconProgramEdit({ export default function SelectIconProgramEdit({
onChange, onChange,
value, value,
...props
}: { }: {
onChange: (value: IconKey) => void; onChange: (value: IconKey | '') => void;
value: IconKey; value: IconKey | '';
}) { } & Omit<SelectProps, 'onChange' | 'value' | 'data'>) {
const IconComponent = iconMap[value]?.icon || null;
return ( return (
<Box maw={300}> <Box maw={300}>
<Select <Select
placeholder="Pilih ikon" placeholder="Pilih ikon"
value={value} value={value || ''}
onChange={(value) => { onChange={(val: string | null) => {
if (value) onChange(value as IconKey); if (val) {
onChange(val as IconKey);
} else {
onChange('');
}
}} }}
data={iconList} data={iconList}
renderOption={({ option }) => {
const Icon = iconMap[option.value as IconKey]?.icon;
return (
<Group gap="sm">
{Icon && <Icon size={18} stroke={1.5} />}
{option.label}
</Group>
);
}}
leftSection={ leftSection={
IconComponent && ( value && iconMap[value as IconKey] ? (
<Box> <Box ml={-4}>
<IconComponent size={24} stroke={1.5} /> {(() => {
const Icon = iconMap[value as IconKey].icon;
return <Icon size={20} stroke={1.5} />;
})()}
</Box> </Box>
) ) : null
} }
withCheckIcon={false} searchable
searchable={false}
rightSectionWidth={0}
styles={{ styles={{
input: { input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40, paddingLeft: 40,
}, fontSize: rem(16),
section: {
left: 10,
right: 'auto',
}, },
}} }}
{...props}
/> />
</Box> </Box>
); );
} }

View File

@@ -39,7 +39,7 @@ const penghargaanState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
penghargaanState.findMany.load(); penghargaanState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -287,7 +287,7 @@ const pengumuman = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
pengumuman.findMany.load(); pengumuman.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -101,6 +101,38 @@ const ApbDesa = proxy({
} }
}, },
}, },
findFirst: {
data: null as Prisma.ApbDesaGetPayload<{
include: { pendapatan: true; belanja: true; pembiayaan: true };
}> | null,
loading: false,
async load(params?: Record<string, any>) {
try {
this.loading = true;
// ✅ request ke endpoint find-first
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"find-first"
].get({ query: params || {} });
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? null;
} else {
this.data = null;
toast.error(res.data?.message || "Gagal memuat data pertama APB Desa");
}
} catch (error) {
console.error("Error findFirst APB Desa:", error);
toast.error("Gagal memuat data APB Desa pertama");
this.data = null;
} finally {
this.loading = false;
}
},
reset() {
this.data = null;
},
},
update: { update: {
id: "", id: "",
form: { ...ApbDesaDefaultForm }, form: { ...ApbDesaDefaultForm },

View File

@@ -49,7 +49,7 @@ const demografiPekerjaan = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
demografiPekerjaan.create.form = { ...defaultForm }; demografiPekerjaan.create.form = { ...defaultForm };
demografiPekerjaan.findMany.load(); demografiPekerjaan.findMany.load();
return id; return id;

View File

@@ -47,7 +47,7 @@ const jumlahPendudukMiskin = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
jumlahPendudukMiskin.create.form = { jumlahPendudukMiskin.create.form = {
year: 0, year: 0,
totalPoorPopulation: 0, totalPoorPopulation: 0,

View File

@@ -89,7 +89,7 @@ const jumlahPengangguran = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.id; const id = res.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm }; jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
jumlahPengangguran.findMany.load(); jumlahPengangguran.findMany.load();
return id; return id;

View File

@@ -47,7 +47,7 @@ const lowonganKerjaState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
lowonganKerjaState.create.loading = false; lowonganKerjaState.create.loading = false;
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -45,7 +45,7 @@ const programKemiskinanState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
programKemiskinanState.findMany.load(); programKemiskinanState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -46,7 +46,7 @@ const grafikSektorUnggulan = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
grafikSektorUnggulan.create.form = { grafikSektorUnggulan.create.form = {
name: "", name: "",
description: "", description: "",

View File

@@ -51,7 +51,7 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
grafikBerdasarkanUsiaKerjaNganggur.create.form = { grafikBerdasarkanUsiaKerjaNganggur.create.form = {
usia18_25: "", usia18_25: "",
usia26_35: "", usia26_35: "",
@@ -255,7 +255,7 @@ const grafikBerdasarkanPendidikan = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
grafikBerdasarkanPendidikan.create.form = { grafikBerdasarkanPendidikan.create.form = {
SD: "", SD: "",
SMP: "", SMP: "",

View File

@@ -37,7 +37,7 @@ const desaDigitalState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
desaDigitalState.findMany.load(); desaDigitalState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -37,7 +37,7 @@ const infoTeknoState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
infoTeknoState.findMany.load(); infoTeknoState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -41,7 +41,7 @@ const programKreatifState = proxy({
if (res.status === 200) { if (res.status === 200) {
programKreatifState.findMany.load(); programKreatifState.findMany.load();
toast.success("success create"); toast.success("Sukses menambahkan");
return true; return true;
} }

View File

@@ -37,7 +37,7 @@ const keamananLingkunganState = proxy({
].post(keamananLingkunganState.create.form); ].post(keamananLingkunganState.create.form);
if (res.status === 200) { if (res.status === 200) {
keamananLingkunganState.findMany.load(); keamananLingkunganState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -38,7 +38,7 @@ const kontakDaruratKeamananState = proxy({
].post(kontakDaruratKeamananState.create.form); ].post(kontakDaruratKeamananState.create.form);
if (res.status === 200) { if (res.status === 200) {
kontakDaruratKeamananState.findMany.load(); kontakDaruratKeamananState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");
@@ -294,7 +294,7 @@ const kontakDaruratItem = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
kontakDaruratItem.findMany.load(); kontakDaruratItem.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -88,7 +88,7 @@ const laporanPublikState = proxy({
if (res.status === 200) { if (res.status === 200) {
laporanPublikState.findMany.load(); laporanPublikState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);

View File

@@ -40,7 +40,7 @@ const pencegahanKriminalitasState = proxy({
].post(pencegahanKriminalitasState.create.form); ].post(pencegahanKriminalitasState.create.form);
if (res.status === 200) { if (res.status === 200) {
pencegahanKriminalitasState.findMany.load(); pencegahanKriminalitasState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -37,7 +37,7 @@ const tipsKeamananState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
tipsKeamananState.findMany.load(); tipsKeamananState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -351,7 +351,7 @@ const dokter = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
dokter.create.create.form = { ...defaultDokterForm }; dokter.create.create.form = { ...defaultDokterForm };
dokter.findMany.load(); dokter.findMany.load();
return id; return id;

View File

@@ -43,7 +43,7 @@ const grafikkepuasan = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
grafikkepuasan.create.form = { ...defaultForm }; grafikkepuasan.create.form = { ...defaultForm };
grafikkepuasan.findMany.load(); grafikkepuasan.findMany.load();
return id; return id;

View File

@@ -50,7 +50,7 @@ const persentasekelahiran = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data; const id = res.data?.data;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
persentasekelahiran.create.form = { ...defaultForm }; persentasekelahiran.create.form = { ...defaultForm };
persentasekelahiran.findMany.load(); persentasekelahiran.findMany.load();
return id; return id;

View File

@@ -53,7 +53,7 @@ const programInovasi = proxy({
].post(formData); ].post(formData);
if (res.status === 200) { if (res.status === 200) {
programInovasi.findMany.load(); programInovasi.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");
@@ -474,7 +474,7 @@ const mediaSosial = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
mediaSosial.findMany.load(); mediaSosial.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -39,7 +39,7 @@ const dataLingkunganDesaState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
dataLingkunganDesaState.findMany.load(); dataLingkunganDesaState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -35,7 +35,7 @@ const pengelolaanSampah = proxy({
].post(pengelolaanSampah.create.form); ].post(pengelolaanSampah.create.form);
if (res.status === 200) { if (res.status === 200) {
pengelolaanSampah.findMany.load(); pengelolaanSampah.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -39,7 +39,7 @@ const programPenghijauanState = proxy({
); );
if (res.status === 200) { if (res.status === 200) {
programPenghijauanState.findMany.load(); programPenghijauanState.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
console.log(res); console.log(res);
return toast.error("failed create"); return toast.error("failed create");

View File

@@ -42,7 +42,7 @@ const dataPendidikan = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
dataPendidikan.create.form = { dataPendidikan.create.form = {
name: "", name: "",
jumlah: "", jumlah: "",

View File

@@ -38,7 +38,7 @@ const daftarInformasiPublik = proxy({
].post(daftarInformasiPublik.create.form); ].post(daftarInformasiPublik.create.form);
if (res.status === 200) { if (res.status === 200) {
daftarInformasiPublik.findMany.load(); daftarInformasiPublik.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
return toast.error("failed create"); return toast.error("failed create");
} catch (error) { } catch (error) {

View File

@@ -41,7 +41,7 @@ const grafikBerdasarkanUmur = proxy({
if (res.status === 200) { if (res.status === 200) {
const id = res.data?.data?.id; const id = res.data?.data?.id;
if (id) { if (id) {
toast.success("Success create"); toast.success("Sukses menambahkan");
grafikBerdasarkanUmur.create.form = { grafikBerdasarkanUmur.create.form = {
remaja: "", remaja: "",
dewasa: "", dewasa: "",

View File

@@ -88,7 +88,7 @@ const statepermohonanInformasiPublik = proxy({
const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form); const res = await ApiFetch.api.ppid.permohonaninformasipublik["create"].post(statepermohonanInformasiPublik.create.form);
if (res.status === 200) { if (res.status === 200) {
statepermohonanInformasiPublik.findMany.load(); statepermohonanInformasiPublik.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
return toast.error("failed create"); return toast.error("failed create");
} catch (error) { } catch (error) {

View File

@@ -37,7 +37,7 @@ const permohonanKeberatanInformasi = proxy({
const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form); const res = await ApiFetch.api.ppid.permohonankeberataninformasipublik["create"].post(permohonanKeberatanInformasi.create.form);
if (res.status === 200) { if (res.status === 200) {
permohonanKeberatanInformasi.findMany.load(); permohonanKeberatanInformasi.findMany.load();
return toast.success("success create"); return toast.success("Sukses menambahkan");
} }
return toast.error("failed create"); return toast.error("failed create");
} catch (error) { } catch (error) {

View File

@@ -3,9 +3,6 @@ import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
/**
* Schema validasi form ProfilePPID menggunakan Zod.
*/
const templateForm = z.object({ const templateForm = z.object({
name: z.string().min(3, "Nama minimal 3 karakter"), name: z.string().min(3, "Nama minimal 3 karakter"),
biodata: z.string().min(3, "Biodata minimal 3 karakter"), biodata: z.string().min(3, "Biodata minimal 3 karakter"),
@@ -33,25 +30,16 @@ type ProfilePPIDForm = Prisma.ProfilePPIDGetPayload<{
pengalaman: true; pengalaman: true;
unggulan: true; unggulan: true;
imageId: true; imageId: true;
image?: { image?: { select: { link: true } };
select: {
link: true;
};
};
}; };
}>; }>;
/**
* Improved State Management - Consolidated and more robust
*/
const stateProfilePPID = proxy({ const stateProfilePPID = proxy({
// Consolidated data management
profile: { profile: {
data: null as ProfilePPIDForm | null, data: null as ProfilePPIDForm | null,
loading: false, loading: false,
error: null as string | null, error: null as string | null,
// Single method to load profile data
async load(id: string) { async load(id: string) {
if (!id) { if (!id) {
toast.warn("ID tidak valid"); toast.warn("ID tidak valid");
@@ -62,52 +50,42 @@ const stateProfilePPID = proxy({
this.error = null; this.error = null;
try { try {
const response = await fetch(`/api/ppid/profileppid/${id}`); const res = await fetch(`/api/ppid/profileppid/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json(); const result = await res.json();
if (result.success) { if (result.success) {
this.data = result.data; this.data = result.data;
return result.data; return result.data;
} else { } else throw new Error(result.message || "Gagal memuat data profile");
throw new Error(result.message || "Gagal mengambil data profile"); } catch (err) {
} const msg = (err as Error).message;
} catch (error) { this.error = msg;
const errorMessage = (error as Error).message; console.error("Load profile error:", msg);
this.error = errorMessage; toast.error("Gagal memuat data profile");
console.error("Load profile error:", errorMessage);
toast.error("Terjadi kesalahan saat mengambil data profile");
return null; return null;
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
// Reset profile data
reset() { reset() {
this.data = null; this.data = null;
this.error = null; this.error = null;
this.loading = false; this.loading = false;
} },
}, },
// Edit form management
editForm: { editForm: {
id: "", id: "",
form: { ...defaultForm }, form: { ...defaultForm },
originalForm: { ...defaultForm }, // ✅ Tambah field originalForm
loading: false, loading: false,
error: null as string | null, error: null as string | null,
isReadOnly: false, // Flag untuk data yang tidak bisa diedit
// Initialize form with profile data
initialize(profileData: ProfilePPIDForm) { initialize(profileData: ProfilePPIDForm) {
this.id = profileData.id; this.id = profileData.id;
this.isReadOnly = false; // Semua data bisa diedit const data = {
this.form = {
name: profileData.name || "", name: profileData.name || "",
biodata: profileData.biodata || "", biodata: profileData.biodata || "",
riwayat: profileData.riwayat || "", riwayat: profileData.riwayat || "",
@@ -115,23 +93,20 @@ const stateProfilePPID = proxy({
unggulan: profileData.unggulan || "", unggulan: profileData.unggulan || "",
imageId: profileData.imageId || "", imageId: profileData.imageId || "",
}; };
this.form = { ...data };
this.originalForm = { ...data }; // ✅ Simpan versi original
}, },
// Update form field
updateField(field: keyof typeof defaultForm, value: string) { updateField(field: keyof typeof defaultForm, value: string) {
this.form[field] = value; this.form[field] = value;
}, },
// Submit form
async submit() { async submit() {
// Validate form const check = templateForm.safeParse(this.form);
const validation = templateForm.safeParse(this.form); if (!check.success) {
toast.error(
if (!validation.success) { check.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
const errors = validation.error.issues );
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return false; return false;
} }
@@ -139,63 +114,54 @@ const stateProfilePPID = proxy({
this.error = null; this.error = null;
try { try {
const response = await fetch(`/api/ppid/profileppid/${this.id}`, { const res = await fetch(`/api/ppid/profileppid/${this.id}`, {
method: "PUT", method: "PUT",
headers: { headers: { "Content-Type": "application/json" },
"Content-Type": "application/json",
},
body: JSON.stringify(this.form), body: JSON.stringify(this.form),
}); });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!response.ok) { const result = await res.json();
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) { if (result.success) {
toast.success("Berhasil update profile"); toast.success("Berhasil update profile");
// Refresh profile data this.originalForm = { ...this.form }; // ✅ Update original setelah sukses
await stateProfilePPID.profile.load(this.id);
return true; return true;
} else { } else throw new Error(result.message || "Gagal update profile");
throw new Error(result.message || "Gagal update profile"); } catch (err) {
} const msg = (err as Error).message;
} catch (error) { this.error = msg;
const errorMessage = (error as Error).message; toast.error(msg);
this.error = errorMessage;
console.error("Update profile error:", errorMessage);
toast.error("Terjadi kesalahan saat update profile");
return false; return false;
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
// Reset form // ✅ Tambahan reset ke original data
resetToOriginal() {
this.form = { ...this.originalForm };
toast.info("Data dikembalikan ke kondisi awal");
},
reset() { reset() {
this.id = ""; this.id = "";
this.form = { ...defaultForm }; this.form = { ...defaultForm };
this.originalForm = { ...defaultForm };
this.error = null; this.error = null;
this.loading = false; this.loading = false;
this.isReadOnly = false; },
}
}, },
// Helper methods
async loadForEdit(id: string) { async loadForEdit(id: string) {
const profileData = await this.profile.load(id); const data = await this.profile.load(id);
if (profileData) { if (data) this.editForm.initialize(data);
this.editForm.initialize(profileData); return data;
}
return profileData;
}, },
reset() { reset() {
this.profile.reset(); this.profile.reset();
this.editForm.reset(); this.editForm.reset();
} },
}); });
export default stateProfilePPID; export default stateProfilePPID;

View File

@@ -10,7 +10,8 @@ import {
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -22,6 +23,11 @@ function EditKategoriBerita() {
const editState = useProxy(stateDashboardBerita.kategoriBerita); const editState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: '',
});
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -38,6 +44,9 @@ function EditKategoriBerita() {
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
}); });
setOriginalData({
name: data.name || '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading kategori Berita:', error); console.error('Error loading kategori Berita:', error);
@@ -55,8 +64,16 @@ function EditKategoriBerita() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// update global state hanya saat submit // update global state hanya saat submit
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
@@ -69,6 +86,8 @@ function EditKategoriBerita() {
} catch (error) { } catch (error) {
console.error('Error updating kategori Berita:', error); console.error('Error updating kategori Berita:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Berita'); toast.error('Terjadi kesalahan saat memperbarui kategori Berita');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -109,6 +128,17 @@ function EditKategoriBerita() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -119,7 +149,7 @@ function EditKategoriBerita() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -8,15 +8,19 @@ import {
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriBerita() { function CreateKategoriBerita() {
const createState = useProxy(stateDashboardBerita.kategoriBerita); const createState = useProxy(stateDashboardBerita.kategoriBerita);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
@@ -25,9 +29,17 @@ function CreateKategoriBerita() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); setIsSubmitting(true);
resetForm(); try {
router.push('/admin/desa/berita/kategori-berita'); await createState.create.create();
resetForm();
router.push('/admin/desa/berita/kategori-berita');
} catch (error) {
console.error('Error creating kategori berita:', error);
toast.error('Gagal menambahkan kategori berita');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -60,12 +72,23 @@ function CreateKategoriBerita() {
<TextInput <TextInput
label="Nama Kategori Berita" label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita" placeholder="Masukkan nama kategori berita"
defaultValue={createState.create.form.name || ''} value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)} onChange={(e) => (createState.create.form.name = e.target.value)}
required required
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -76,7 +99,7 @@ function CreateKategoriBerita() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import stateDashboardBerita from "@/app/admin/(dashboard)/_state/desa/berita";
import colors from "@/con/colors"; import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
@@ -15,7 +16,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader
} from "@mantine/core"; } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { import {
@@ -44,6 +46,17 @@ function EditBerita() {
imageId: "", imageId: "",
}); });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
kategoriBeritaId: "",
content: "",
imageId: "",
imageUrl: ""
});
// Load kategori + berita // Load kategori + berita
useEffect(() => { useEffect(() => {
beritaState.kategoriBerita.findMany.load(); beritaState.kategoriBerita.findMany.load();
@@ -63,6 +76,15 @@ function EditBerita() {
imageId: data.imageId || "", imageId: data.imageId || "",
}); });
setOriginalData({
judul: data.judul || "",
deskripsi: data.deskripsi || "",
kategoriBeritaId: data.kategoriBeritaId || "",
content: data.content || "",
imageId: data.imageId || "",
imageUrl: data.image?.link || ""
});
if (data?.image?.link) { if (data?.image?.link) {
setPreviewImage(data.image.link); setPreviewImage(data.image.link);
} }
@@ -82,6 +104,7 @@ function EditBerita() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// Update global state hanya sekali di sini // Update global state hanya sekali di sini
beritaState.berita.edit.form = { beritaState.berita.edit.form = {
...beritaState.berita.edit.form, ...beritaState.berita.edit.form,
@@ -108,21 +131,36 @@ function EditBerita() {
} catch (error) { } catch (error) {
console.error("Error updating berita:", error); console.error("Error updating berita:", error);
toast.error("Terjadi kesalahan saat memperbarui berita"); toast.error("Terjadi kesalahan saat memperbarui berita");
} finally {
setIsSubmitting(false);
} }
}; };
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
deskripsi: originalData.deskripsi,
kategoriBeritaId: originalData.kategoriBeritaId,
content: originalData.content,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
return ( return (
<Box px={{ base: "sm", md: "lg" }} py="md"> <Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors["blue-button"]} size={24} /> <IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Berita Edit Berita
</Title> </Title>
@@ -216,14 +254,14 @@ function EditBerita() {
Seret gambar atau klik untuk memilih file Seret gambar atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
@@ -235,6 +273,24 @@ function EditBerita() {
}} }}
loading="lazy" loading="lazy"
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -254,17 +310,29 @@ function EditBerita() {
{/* Action */} {/* Action */}
<Group justify="right"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
size="md" size="md"
style={{ style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: "#fff", color: '#fff',
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)", boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -13,7 +13,9 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader,
ActionIcon
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
@@ -28,6 +30,7 @@ export default function CreateBerita() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
useShallowEffect(() => { useShallowEffect(() => {
beritaState.kategoriBerita.findMany.load(); beritaState.kategoriBerita.findMany.load();
@@ -46,40 +49,48 @@ export default function CreateBerita() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { try {
return toast.warn('Silakan pilih file gambar terlebih dahulu'); setIsSubmitting(true);
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
beritaState.berita.create.form.imageId = uploaded.id;
await beritaState.berita.create.create();
resetForm();
router.push('/admin/desa/berita/list-berita');
} catch (error) {
console.error('Error creating berita:', error);
toast.error('Terjadi kesalahan saat membuat berita');
} finally {
setIsSubmitting(false);
} }
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
beritaState.berita.create.form.imageId = uploaded.id;
await beritaState.berita.create.create();
resetForm();
router.push('/admin/desa/berita/list-berita');
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */} {/* Header dengan tombol kembali */}
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Berita Tambah Berita
</Title> </Title>
@@ -97,7 +108,7 @@ export default function CreateBerita() {
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul berita" placeholder="Masukkan judul berita"
defaultValue={beritaState.berita.create.form.judul} value={beritaState.berita.create.form.judul}
onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)} onChange={(e) => (beritaState.berita.create.form.judul = e.target.value)}
required required
/> />
@@ -109,7 +120,7 @@ export default function CreateBerita() {
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
defaultValue={beritaState.berita.create.form.kategoriBeritaId || null} value={beritaState.berita.create.form.kategoriBeritaId || null}
onChange={(val: string | null) => { onChange={(val: string | null) => {
if (val) { if (val) {
const selected = beritaState.kategoriBerita.findMany.data?.find( const selected = beritaState.kategoriBerita.findMany.data?.find(
@@ -154,7 +165,7 @@ export default function CreateBerita() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -175,7 +186,7 @@ export default function CreateBerita() {
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
@@ -187,6 +198,26 @@ export default function CreateBerita() {
}} }}
loading="lazy" loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -204,6 +235,17 @@ export default function CreateBerita() {
</Box> </Box>
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -214,7 +256,7 @@ export default function CreateBerita() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -4,15 +4,17 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -24,6 +26,14 @@ function EditVideo() {
const videoState = useProxy(stateGallery.video); const videoState = useProxy(stateGallery.video);
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
linkVideo: "",
});
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
deskripsi: '', deskripsi: '',
@@ -44,6 +54,11 @@ function EditVideo() {
deskripsi: data.deskripsi ?? '', deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo ?? '', linkVideo: data.linkVideo ?? '',
}); });
setOriginalData({
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo ?? '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading video:', error); console.error('Error loading video:', error);
@@ -61,25 +76,42 @@ function EditVideo() {
[] []
); );
const handleSubmit = async () => { const handleResetForm = () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo); setFormData({
if (!converted) { name: originalData.name,
toast.error("Link YouTube tidak valid. Pastikan formatnya benar."); deskripsi: originalData.deskripsi,
return; linkVideo: originalData.linkVideo,
} });
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try { try {
videoState.update.form = { setIsSubmitting(true);
name: formData.name, const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
deskripsi: formData.deskripsi, if (!converted) {
linkVideo: formData.linkVideo, toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
}; return;
await videoState.update.update(); }
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video'); try {
videoState.update.form = {
name: formData.name,
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
};
await videoState.update.update();
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video');
} catch (error) {
console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video');
}
} catch (error) { } catch (error) {
console.error('Error updating video:', error); console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video'); toast.error('Terjadi kesalahan saat memperbarui video');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -88,14 +120,14 @@ function EditVideo() {
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Video Edit Video
</Title> </Title>
@@ -127,7 +159,7 @@ function EditVideo() {
required required
/> />
{embedLink && ( {embedLink && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <Box mt="sm" pos="relative" style={{ display: 'flex', justifyContent: 'center' }}>
<iframe <iframe
className="rounded" className="rounded"
width="100%" width="100%"
@@ -135,7 +167,27 @@ function EditVideo() {
src={embedLink} src={embedLink}
title="Preview Video" title="Preview Video"
allowFullScreen allowFullScreen
></iframe> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setFormData({
...formData,
linkVideo: '',
});
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -151,6 +203,17 @@ function EditVideo() {
</Box> </Box>
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -161,7 +224,7 @@ function EditVideo() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -3,6 +3,7 @@ import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
@@ -10,9 +11,10 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -24,6 +26,7 @@ function CreateVideo() {
const router = useRouter(); const router = useRouter();
const [link, setLink] = useState(''); const [link, setLink] = useState('');
const embedLink = convertYoutubeUrlToEmbed(link); const embedLink = convertYoutubeUrlToEmbed(link);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
videoState.create.form = { videoState.create.form = {
@@ -35,29 +38,37 @@ function CreateVideo() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!embedLink) { try {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); setIsSubmitting(true);
return; if (!embedLink) {
} toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return;
}
videoState.create.form.linkVideo = embedLink; videoState.create.form.linkVideo = embedLink;
await videoState.create.create(); await videoState.create.create();
resetForm(); resetForm();
router.push('/admin/desa/gallery/video'); router.push('/admin/desa/gallery/video');
} catch (error) {
console.error("Error creating video:", error);
toast.error("Terjadi kesalahan saat menambahkan video");
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header Back Button + Title */} {/* Header Back Button + Title */}
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Video Tambah Video
</Title> </Title>
@@ -77,7 +88,7 @@ function CreateVideo() {
<TextInput <TextInput
label="Judul Video" label="Judul Video"
placeholder="Masukkan judul video" placeholder="Masukkan judul video"
defaultValue={videoState.create.form.name} value={videoState.create.form.name}
onChange={(e) => { onChange={(e) => {
videoState.create.form.name = e.currentTarget.value; videoState.create.form.name = e.currentTarget.value;
}} }}
@@ -88,14 +99,14 @@ function CreateVideo() {
<TextInput <TextInput
label="Link Video YouTube" label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123" placeholder="https://www.youtube.com/watch?v=abc123"
defaultValue={link} value={link}
onChange={(e) => setLink(e.currentTarget.value)} onChange={(e) => setLink(e.currentTarget.value)}
required required
/> />
{/* Preview Video */} {/* Preview Video */}
{embedLink && ( {embedLink && (
<Box mt="sm"> <Box mt="sm" pos="relative">
<iframe <iframe
style={{ style={{
borderRadius: 10, borderRadius: 10,
@@ -106,7 +117,24 @@ function CreateVideo() {
src={embedLink} src={embedLink}
title="Preview Video" title="Preview Video"
allowFullScreen allowFullScreen
></iframe> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setLink('');
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
@@ -125,6 +153,17 @@ function CreateVideo() {
{/* Button Submit */} {/* Button Submit */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -135,7 +174,7 @@ function CreateVideo() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Select, Select,
Stack, Stack,
@@ -23,6 +24,16 @@ function EditAjukanPermohonan() {
const params = useParams(); const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan); const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
nama: "",
nik: "",
alamat: "",
nomorKk: "",
kategoriId: "",
});
// State lokal form // State lokal form
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: '', nama: '',
@@ -50,6 +61,13 @@ function EditAjukanPermohonan() {
nomorKk: data.nomorKk || '', nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '', kategoriId: data.kategoriId || '',
}); });
setOriginalData({
nama: data.nama || '',
nik: data.nik || '',
alamat: data.alamat || '',
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading ajukan:', error); console.error('Error loading ajukan:', error);
@@ -68,8 +86,20 @@ function EditAjukanPermohonan() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
nik: originalData.nik,
alamat: originalData.alamat,
nomorKk: originalData.nomorKk,
kategoriId: originalData.kategoriId,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateAjukan.edit.form = { stateAjukan.edit.form = {
...stateAjukan.edit.form, ...stateAjukan.edit.form,
...formData, ...formData,
@@ -79,6 +109,8 @@ function EditAjukanPermohonan() {
} catch (error) { } catch (error) {
console.error('Error updating ajukan:', error); console.error('Error updating ajukan:', error);
toast.error('Terjadi kesalahan saat memperbarui ajukan'); toast.error('Terjadi kesalahan saat memperbarui ajukan');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -86,9 +118,9 @@ function EditAjukanPermohonan() {
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */} {/* Back Button */}
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Ajukan Permohonan Edit Ajukan Permohonan
</Title> </Title>
@@ -153,6 +185,17 @@ function EditAjukanPermohonan() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -163,7 +206,7 @@ function EditAjukanPermohonan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -8,6 +8,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -32,6 +33,14 @@ function EditPelayananPendudukNonPermanent() {
deskripsi: '', deskripsi: '',
}); });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: '',
deskripsi: '',
});
// Load data sekali dari backend // Load data sekali dari backend
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -45,6 +54,10 @@ function EditPelayananPendudukNonPermanent() {
name: data.name || '', name: data.name || '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
}); });
setOriginalData({
name: data.name || '',
deskripsi: data.deskripsi || '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error);
@@ -57,39 +70,55 @@ function EditPelayananPendudukNonPermanent() {
const handleChange = const handleChange =
(field: keyof typeof formData) => (field: keyof typeof formData) =>
(value: string) => { (value: string) => {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[field]: value, [field]: value,
})); }));
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!statePendudukNonPermanent.findById.data) return; try {
setIsSubmitting(true);
if (!statePendudukNonPermanent.findById.data) return;
// Update global state hanya di submit // Update global state hanya di submit
const updated = { const updated = {
...statePendudukNonPermanent.findById.data, ...statePendudukNonPermanent.findById.data,
name: formData.name, name: formData.name,
deskripsi: formData.deskripsi, deskripsi: formData.deskripsi,
}; };
await statePendudukNonPermanent.update.update(updated); await statePendudukNonPermanent.update.update(updated);
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent'); router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memuat data pelayanan penduduk non permanent');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Pelayanan Penduduk Non Permanent Edit Pelayanan Penduduk Non Permanent
</Title> </Title>
@@ -127,25 +156,31 @@ function EditPelayananPendudukNonPermanent() {
</Box> </Box>
{/* Submit Button */} {/* Submit Button */}
<Group> <Group justify="right">
<Button {/* Tombol Batal */}
bg={colors['blue-button']}
onClick={handleSubmit}
loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name}
>
{statePendudukNonPermanent.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => router.back()} color="gray"
disabled={statePendudukNonPermanent.update.loading} radius="md"
size="md"
onClick={handleResetForm}
> >
Batal Batal
</Button> </Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -8,6 +8,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
@@ -34,13 +35,21 @@ function EditPelayananPerizinanBerusaha() {
link: '', link: '',
}); });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
id: '',
name: '',
deskripsi: '',
link: '',
});
// Load data detail // Load data detail
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
toast.error("ID tidak valid"); toast.error("ID tidak valid");
return; return;
} }
const loadData = async () => { const loadData = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -52,6 +61,12 @@ function EditPelayananPerizinanBerusaha() {
deskripsi: data.deskripsi || "", deskripsi: data.deskripsi || "",
link: data.link || "", link: data.link || "",
}); });
setOriginalData({
id: data.id,
name: data.name || "",
deskripsi: data.deskripsi || "",
link: data.link || "",
});
} else { } else {
toast.error("Data tidak ditemukan"); toast.error("Data tidak ditemukan");
} }
@@ -62,10 +77,10 @@ function EditPelayananPerizinanBerusaha() {
setLoading(false); setLoading(false);
} }
}; };
loadData(); loadData();
}, [id]); }, [id]);
const handleChange = const handleChange =
(field: keyof typeof formData) => (field: keyof typeof formData) =>
@@ -76,13 +91,26 @@ function EditPelayananPerizinanBerusaha() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
id: originalData.id,
name: originalData.name,
deskripsi: originalData.deskripsi,
link: originalData.link,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
await state.update.update(formData); await state.update.update(formData);
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha'); router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
} catch (error) { } catch (error) {
console.error('Error updating pelayanan perizinan berusaha:', error); console.error('Error updating pelayanan perizinan berusaha:', error);
toast.error('Terjadi kesalahan saat update data'); toast.error('Terjadi kesalahan saat update data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -99,14 +127,14 @@ function EditPelayananPerizinanBerusaha() {
<Stack gap="xs"> <Stack gap="xs">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Pelayanan Perizinan Berusaha Edit Pelayanan Perizinan Berusaha
</Title> </Title>
@@ -147,23 +175,31 @@ function EditPelayananPerizinanBerusaha() {
/> />
</Box> </Box>
<Group> <Group justify="right">
<Button {/* Tombol Batal */}
bg={colors['blue-button']}
onClick={handleSubmit}
loading={state.update.loading}
disabled={!formData.name}
>
{state.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => router.back()} color="gray"
disabled={state.update.loading} radius="md"
size="md"
onClick={handleResetForm}
> >
Batal Batal
</Button> </Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,124 +1,280 @@
'use client' /* eslint-disable @typescript-eslint/no-unused-vars */
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import * as React from 'react';
// 🔹 Types
interface FormData {
name: string;
deskripsi: string;
imageId: string;
image2Id: string;
imageUrl: string;
image2Url: string;
}
interface FileUploaderProps {
title: string;
file: File | null;
setFile: React.Dispatch<React.SetStateAction<File | null>>;
preview: string | null;
setPreview: React.Dispatch<React.SetStateAction<string | null>>;
}
// 🔹 File Uploader Component
const FileUploader: React.FC<FileUploaderProps> = ({
title,
file,
setFile,
preview,
setPreview
}) => {
const handleDrop = (files: File[]) => {
const selected = files[0];
if (selected) {
setFile(selected);
setPreview(URL.createObjectURL(selected));
}
};
const handleRemove = () => {
setPreview(null);
setFile(null);
};
return (
<Box>
<Text fw="bold" fz="sm" mb={6}>
{title}
</Text>
<Dropzone
onDrop={handleDrop}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
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 .png, .jpg, .jpeg, .webp
</Text>
</Stack>
</Group>
</Dropzone>
{preview && (
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={preview}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
/>
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={handleRemove}
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
);
};
// 🔹 Main Component
function EditSuratKeterangan() { function EditSuratKeterangan() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
// state lokal untuk form // 🧩 State
const [formData, setFormData] = useState({ const [formData, setFormData] = useState<FormData>({
name: '', name: '',
deskripsi: '', deskripsi: '',
imageId: '', imageId: '',
image2Id: '', image2Id: '',
imageUrl: '',
image2Url: '',
}); });
const [originalData, setOriginalData] = useState<FormData>(formData);
// state file upload
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null); const [file2, setFile2] = useState<File | null>(null);
// state preview gambar
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null); const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// load data awal // 🧭 Load Initial Data
useEffect(() => { useEffect(() => {
const loadSurat = async () => { const loadSurat = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateSurat.edit.load(id); const data = await stateLayananDesa.suratKeterangan.edit.load(id);
if (!data) return; if (!data) return;
setFormData((prev) => ({ const mapped: FormData = {
...prev, name: data.name || '',
...{ deskripsi: data.deskripsi || '',
name: prev.name || data.name || "", imageId: data.imageId || '',
deskripsi: prev.deskripsi || data.deskripsi || "", image2Id: data.image2Id || '',
imageId: prev.imageId || data.imageId || "", imageUrl: data.image?.link || '',
image2Id: prev.image2Id || data.image2Id || "", image2Url: data.image2?.link || ''
}, };
}));
if (data.image?.link && !previewImage) setPreviewImage(data.image.link); setFormData(mapped);
if (data.image2?.link && !previewImage2) setPreviewImage2(data.image2.link); setOriginalData(mapped);
if (data.image?.link) setPreviewImage(data.image.link);
if (data.image2?.link) setPreviewImage2(data.image2.link);
} catch (error) { } catch (error) {
console.error("Error loading surat:", error); console.error('Error loading surat:', error);
toast.error("Gagal memuat data surat"); toast.error('Gagal memuat data surat');
} }
}; };
loadSurat(); loadSurat();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]); }, [params?.id]);
// 📤 Upload File Helper
const uploadFile = async (file: File): Promise<string | null> => {
try {
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
return uploaded?.id || null;
} catch (error) {
console.error('Error uploading file:', error);
return null;
}
};
// 🔁 Reset Form
const handleResetForm = () => {
setFormData(originalData);
setPreviewImage(originalData.imageUrl || null);
setPreviewImage2(originalData.image2Url || null);
setFile(null);
setFile2(null);
toast.info('Form dikembalikan ke data awal');
};
// handler untuk submit // 💾 Submit Handler
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(async () => {
try { try {
// update form global hanya saat submit setIsSubmitting(true);
stateSurat.edit.form = { ...stateSurat.edit.form, ...formData };
// upload file 1 // ✅ Access original state directly (not proxy)
const originalState = stateLayananDesa.suratKeterangan;
// Update form data properties individually
originalState.edit.form.name = formData.name;
originalState.edit.form.deskripsi = formData.deskripsi;
originalState.edit.form.imageId = formData.imageId;
originalState.edit.form.image2Id = formData.image2Id;
// Upload file 1 if exists
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const uploadedId = await uploadFile(file);
const uploaded = res.data?.data; if (!uploadedId) {
if (!uploaded?.id) return toast.error('Gagal upload gambar'); toast.error('Gagal upload gambar pertama');
stateSurat.edit.form.imageId = uploaded.id; return;
}
originalState.edit.form.imageId = uploadedId;
} }
// upload file 2 // Upload file 2 if exists
if (file2) { if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name }); const uploadedId = await uploadFile(file2);
const uploaded = res.data?.data; if (!uploadedId) {
if (!uploaded?.id) return toast.error('Gagal upload gambar'); toast.error('Gagal upload gambar kedua');
stateSurat.edit.form.image2Id = uploaded.id; return;
}
originalState.edit.form.image2Id = uploadedId;
} }
await stateSurat.edit.update(); // Submit update
await originalState.edit.update();
toast.success('Surat berhasil diperbarui!'); toast.success('Surat berhasil diperbarui!');
router.push('/admin/desa/layanan/pelayanan_surat_keterangan'); router.push('/admin/desa/layanan/pelayanan_surat_keterangan');
} catch (error) { } catch (error) {
console.error('Error updating surat:', error); console.error('Error updating surat:', error);
toast.error('Terjadi kesalahan saat memperbarui surat'); toast.error('Terjadi kesalahan saat memperbarui surat');
} finally {
setIsSubmitting(false);
} }
}, [formData, file, file2, router, stateSurat.edit]); }, [formData, file, file2, router]);
// 📝 Form Field Handlers
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, name: e.target.value }));
};
const handleDeskripsiChange = (html: string) => {
setFormData(prev => ({ ...prev, deskripsi: html }));
};
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */} {/* Header */}
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Surat Keterangan Edit Surat Keterangan
</Title> </Title>
</Group> </Group>
{/* Form */}
<Paper <Paper
w={{ base: '100%', md: '50%' }} w={{ base: '100%', md: '50%' }}
bg={colors['white-1']} bg={colors['white-1']}
@@ -128,154 +284,66 @@ function EditSuratKeterangan() {
style={{ border: '1px solid #e0e0e0' }} style={{ border: '1px solid #e0e0e0' }}
> >
<Stack gap="md"> <Stack gap="md">
{/* Input nama */} {/* Nama Surat */}
<TextInput <TextInput
label="Nama Surat Keterangan" label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan" placeholder="Masukkan nama surat keterangan"
value={formData.name} value={formData.name}
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))} onChange={handleNameChange}
required required
/> />
{/* Input deskripsi */} {/* Deskripsi */}
<Box> <Box>
<Text fz="sm" fw="bold" mb={6}> <Text fz="sm" fw="bold" mb={6}>
Konten Konten
</Text> </Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => onChange={handleDeskripsiChange}
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/> />
</Box> </Box>
{/* Upload Gambar 1 */} {/* Gambar 1 */}
<Box> <FileUploader
<Text fw="bold" fz="sm" mb={6}> title="Gambar Konten Pelayanan"
Gambar Konten Pelayanan file={file}
</Text> setFile={setFile}
<Dropzone preview={previewImage}
onDrop={(files) => { setPreview={setPreviewImage}
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 && ( {/* Gambar 2 */}
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <FileUploader
<Image title="Gambar Alur Pelayanan Surat"
src={previewImage} file={file2}
alt="Preview Gambar 1" setFile={setFile2}
radius="md" preview={previewImage2}
style={{ setPreview={setPreviewImage2}
maxHeight: 220, />
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Upload Gambar 2 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Alur Pelayanan Surat
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile2(selectedFile);
setPreviewImage2(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>
{previewImage2 && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage2}
alt="Preview Gambar 2"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
)}
</Box>
{/* Action Buttons */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
onClick={handleResetForm}
disabled={isSubmitting}
>
Batal
</Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
size="md" disabled={isSubmitting}
style={{ style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff', color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79,172,254,0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -284,4 +352,4 @@ function EditSuratKeterangan() {
); );
} }
export default EditSuratKeterangan; export default EditSuratKeterangan;

View File

@@ -5,10 +5,12 @@ import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -27,6 +29,7 @@ function CreateSuratKeterangan() {
const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null); const [previewImage2, setPreviewImage2] = useState<{ preview: string; file: File } | null>(null);
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null); const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateSurat.create.form = { stateSurat.create.form = {
@@ -45,6 +48,7 @@ function CreateSuratKeterangan() {
} }
try { try {
setIsSubmitting(true);
// Upload gambar utama // Upload gambar utama
const res1 = await ApiFetch.api.fileStorage.create.post({ const res1 = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file, file: previewImage.file,
@@ -77,6 +81,8 @@ function CreateSuratKeterangan() {
} catch (error) { } catch (error) {
console.error('Error creating surat keterangan:', error); console.error('Error creating surat keterangan:', error);
toast.error('Terjadi kesalahan saat menambahkan surat keterangan'); toast.error('Terjadi kesalahan saat menambahkan surat keterangan');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -84,9 +90,9 @@ function CreateSuratKeterangan() {
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Surat Keterangan Tambah Surat Keterangan
</Title> </Title>
@@ -103,7 +109,7 @@ function CreateSuratKeterangan() {
<Stack gap="md"> <Stack gap="md">
{/* Nama Surat */} {/* Nama Surat */}
<TextInput <TextInput
defaultValue={stateSurat.create.form.name} value={stateSurat.create.form.name}
onChange={(val) => (stateSurat.create.form.name = val.target.value)} onChange={(val) => (stateSurat.create.form.name = val.target.value)}
label="Nama Surat Keterangan" label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan" placeholder="Masukkan nama surat keterangan"
@@ -140,7 +146,7 @@ function CreateSuratKeterangan() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -161,7 +167,7 @@ function CreateSuratKeterangan() {
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage.preview} src={previewImage.preview}
alt="Preview Gambar Utama" alt="Preview Gambar Utama"
@@ -169,6 +175,23 @@ function CreateSuratKeterangan() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy" loading="lazy"
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -190,7 +213,7 @@ function CreateSuratKeterangan() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -211,7 +234,7 @@ function CreateSuratKeterangan() {
</Dropzone> </Dropzone>
{previewImage2 ? ( {previewImage2 ? (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage2.preview} src={previewImage2.preview}
alt="Preview Gambar Tambahan" alt="Preview Gambar Tambahan"
@@ -219,6 +242,23 @@ function CreateSuratKeterangan() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy" loading="lazy"
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage2(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
) : ( ) : (
<Text size="sm" c="dimmed" mt="sm" ta="center"> <Text size="sm" c="dimmed" mt="sm" ta="center">
@@ -229,6 +269,17 @@ function CreateSuratKeterangan() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -239,7 +290,7 @@ function CreateSuratKeterangan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -21,6 +22,7 @@ function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -28,6 +30,12 @@ function EditPelayananTelunjukSakti() {
link: '', link: '',
}); });
const [originalData, setOriginalData] = useState({
name: '',
deskripsi: '',
link: '',
});
// Load data awal hanya sekali (pas ada id) // Load data awal hanya sekali (pas ada id)
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -42,6 +50,11 @@ function EditPelayananTelunjukSakti() {
deskripsi: data.deskripsi ?? '', deskripsi: data.deskripsi ?? '',
link: data.link ?? '', link: data.link ?? '',
}); });
setOriginalData({
name: data.name ?? '',
deskripsi: data.deskripsi ?? '',
link: data.link ?? '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading pelayanan telunjuk sakti:', error); console.error('Error loading pelayanan telunjuk sakti:', error);
@@ -60,9 +73,19 @@ function EditPelayananTelunjukSakti() {
[] []
); );
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
link: originalData.link,
});
toast.info("Form dikembalikan ke data awal");
};
// Submit: update global state hanya saat simpan // Submit: update global state hanya saat simpan
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateTelunjukDesa.edit.form = { stateTelunjukDesa.edit.form = {
...stateTelunjukDesa.edit.form, ...stateTelunjukDesa.edit.form,
...formData, ...formData,
@@ -73,6 +96,8 @@ function EditPelayananTelunjukSakti() {
} catch (error) { } catch (error) {
console.error('Error updating pelayanan telunjuk sakti:', error); console.error('Error updating pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti'); toast.error('Terjadi kesalahan saat memperbarui pelayanan telunjuk sakti');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -125,6 +150,17 @@ function EditPelayananTelunjukSakti() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -135,7 +171,7 @@ function EditPelayananTelunjukSakti() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -13,12 +14,14 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePelayananTelunjukDesa() { function CreatePelayananTelunjukDesa() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateTelunjukDesa.create.form = { stateTelunjukDesa.create.form = {
@@ -30,6 +33,7 @@ function CreatePelayananTelunjukDesa() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
await stateTelunjukDesa.create.create(); await stateTelunjukDesa.create.create();
resetForm(); resetForm();
toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan'); toast.success('Data pelayanan telunjuk sakti berhasil ditambahkan');
@@ -37,6 +41,8 @@ function CreatePelayananTelunjukDesa() {
} catch (error) { } catch (error) {
console.error('Error create pelayanan telunjuk sakti:', error); console.error('Error create pelayanan telunjuk sakti:', error);
toast.error('Terjadi kesalahan saat menambahkan data'); toast.error('Terjadi kesalahan saat menambahkan data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -64,7 +70,7 @@ function CreatePelayananTelunjukDesa() {
<Stack gap="md"> <Stack gap="md">
{/* Nama */} {/* Nama */}
<TextInput <TextInput
defaultValue={stateTelunjukDesa.create.form.name} value={stateTelunjukDesa.create.form.name}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.name = val.target.value; stateTelunjukDesa.create.form.name = val.target.value;
}} }}
@@ -75,7 +81,7 @@ function CreatePelayananTelunjukDesa() {
{/* Deskripsi */} {/* Deskripsi */}
<TextInput <TextInput
defaultValue={stateTelunjukDesa.create.form.deskripsi} value={stateTelunjukDesa.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value; stateTelunjukDesa.create.form.deskripsi = val.target.value;
}} }}
@@ -86,7 +92,7 @@ function CreatePelayananTelunjukDesa() {
{/* Link */} {/* Link */}
<TextInput <TextInput
defaultValue={stateTelunjukDesa.create.form.link} value={stateTelunjukDesa.create.form.link}
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.link = val.target.value; stateTelunjukDesa.create.form.link = val.target.value;
}} }}
@@ -97,6 +103,17 @@ function CreatePelayananTelunjukDesa() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -107,7 +124,7 @@ function CreatePelayananTelunjukDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,10 +5,12 @@ import penghargaanState from '@/app/admin/(dashboard)/_state/desa/penghargaan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -29,6 +31,15 @@ function EditPenghargaan() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
juara: "",
deskripsi: "",
imageId: "",
imageUrl: "",
});
// Lokal formData // Lokal formData
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -46,17 +57,25 @@ function EditPenghargaan() {
try { try {
const data = await statePenghargaan.edit.load(id); const data = await statePenghargaan.edit.load(id);
if (data) { if (data) {
setFormData({ const newForm = {
name: data.name || '', name: data.name || "",
juara: data.juara || '', juara: data.juara || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
imageId: data.imageId || '', imageId: data.imageId || "",
};
setFormData(newForm);
// simpan juga versi original
const imageUrl = data.image?.link || "";
setOriginalData({
...newForm,
imageUrl: imageUrl,
}); });
if (data?.image?.link) { setPreviewImage(imageUrl || null);
setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error('Error loading penghargaan:', error); console.error('Error loading penghargaan:', error);
@@ -67,33 +86,49 @@ function EditPenghargaan() {
loadPenghargaan(); loadPenghargaan();
}, [params?.id]); }, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name,
juara: originalData.juara,
deskripsi: originalData.deskripsi,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
// Submit // Submit
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// Sync ke global state saat submit // Sync ke global state saat submit
statePenghargaan.edit.form = { let imageId = formData.imageId;
...statePenghargaan.edit.form,
...formData,
};
// Upload file baru (kalau ada)
if (file) { 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; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error('Gagal upload gambar'); return toast.error("Gagal upload gambar");
} }
imageId = uploaded.id;
statePenghargaan.edit.form.imageId = uploaded.id;
} }
// Update global state form (baru di sini)
statePenghargaan.edit.form = {
...statePenghargaan.edit.form,
name: formData.name,
juara: formData.juara,
deskripsi: formData.deskripsi,
imageId,
}
await statePenghargaan.edit.update(); await statePenghargaan.edit.update();
toast.success('Penghargaan berhasil diperbarui!'); toast.success('Penghargaan berhasil diperbarui!');
router.push('/admin/desa/penghargaan'); router.push('/admin/desa/penghargaan');
} catch (error) { } catch (error) {
console.error('Error updating penghargaan:', error); console.error('Error updating penghargaan:', error);
toast.error('Terjadi kesalahan saat memperbarui penghargaan'); toast.error('Terjadi kesalahan saat memperbarui penghargaan');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -152,7 +187,7 @@ function EditPenghargaan() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -171,25 +206,47 @@ function EditPenghargaan() {
Seret gambar atau klik untuk memilih file Seret gambar atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <Box pos="relative" mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Box>
src={previewImage} <Image
alt="Preview Gambar" src={previewImage.startsWith('http') ? previewImage : `${window.location.origin}${previewImage}`}
radius="md" alt="Preview Gambar"
style={{ radius="md"
maxHeight: 220, style={{
objectFit: 'contain', maxHeight: 200,
border: `1px solid ${colors['blue-button']}`, objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}} }}
loading="lazy" style={{
/> boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -209,6 +266,17 @@ function EditPenghargaan() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -219,7 +287,7 @@ function EditPenghargaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -2,10 +2,12 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -26,6 +28,7 @@ function CreatePenghargaan() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
statePenghargaan.create.form = { statePenghargaan.create.form = {
@@ -39,26 +42,34 @@ function CreatePenghargaan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { try {
return toast.warn('Silakan pilih file gambar terlebih dahulu'); setIsSubmitting(true);
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
statePenghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.create.create();
resetForm();
router.push('/admin/desa/penghargaan');
} catch (error) {
console.error('Error creating penghargaan:', error);
toast.error('Terjadi kesalahan saat menambahkan penghargaan');
} finally {
setIsSubmitting(false);
} }
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
statePenghargaan.create.form.imageId = uploaded.id;
await statePenghargaan.create.create();
resetForm();
router.push('/admin/desa/penghargaan');
}; };
return ( return (
@@ -84,7 +95,7 @@ function CreatePenghargaan() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
defaultValue={statePenghargaan.create.form.name} value={statePenghargaan.create.form.name}
onChange={(val) => (statePenghargaan.create.form.name = val.target.value)} onChange={(val) => (statePenghargaan.create.form.name = val.target.value)}
label="Nama Penghargaan" label="Nama Penghargaan"
placeholder="Masukkan nama penghargaan" placeholder="Masukkan nama penghargaan"
@@ -92,7 +103,7 @@ function CreatePenghargaan() {
/> />
<TextInput <TextInput
defaultValue={statePenghargaan.create.form.juara} value={statePenghargaan.create.form.juara}
onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)} onChange={(val) => (statePenghargaan.create.form.juara = val.target.value)}
label="Juara" label="Juara"
placeholder="Masukkan juara" placeholder="Masukkan juara"
@@ -122,7 +133,7 @@ function CreatePenghargaan() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -143,7 +154,7 @@ function CreatePenghargaan() {
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
@@ -151,12 +162,41 @@ function CreatePenghargaan() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy" loading="lazy"
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
{/* Button Submit */} {/* Button Submit */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -167,7 +207,7 @@ function CreatePenghargaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -10,7 +10,8 @@ import {
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -22,8 +23,9 @@ function EditKategoriPengumuman() {
const editState = useProxy(stateDesaPengumuman.category); const editState = useProxy(stateDesaPengumuman.category);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ name: '' }); const [formData, setFormData] = useState({ name: '' });
const [originalData, setOriginalData] = useState({ name: '' });
// Load data awal sekali aja // Load data awal sekali aja
useEffect(() => { useEffect(() => {
@@ -35,6 +37,7 @@ function EditKategoriPengumuman() {
const data = await editState.update.load(id); const data = await editState.update.load(id);
if (data) { if (data) {
setFormData({ name: data.name || '' }); setFormData({ name: data.name || '' });
setOriginalData({ name: data.name || '' });
} }
} catch (error) { } catch (error) {
console.error('Error loading kategori Pengumuman:', error); console.error('Error loading kategori Pengumuman:', error);
@@ -54,6 +57,7 @@ function EditKategoriPengumuman() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// Update global state hanya di sini // Update global state hanya di sini
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
@@ -66,9 +70,19 @@ function EditKategoriPengumuman() {
} catch (error) { } catch (error) {
console.error('Error updating kategori Pengumuman:', error); console.error('Error updating kategori Pengumuman:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman'); toast.error('Terjadi kesalahan saat memperbarui kategori Pengumuman');
} finally {
setIsSubmitting(false);
} }
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
});
toast.info("Form dikembalikan ke data awal");
};
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
@@ -105,6 +119,17 @@ function EditKategoriPengumuman() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -115,7 +140,7 @@ function EditKategoriPengumuman() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -8,15 +8,19 @@ import {
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { toast } from 'react-toastify';
function CreateKategoriPengumuman() { function CreateKategoriPengumuman() {
const createState = useProxy(stateDesaPengumuman.category); const createState = useProxy(stateDesaPengumuman.category);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
@@ -25,9 +29,16 @@ function CreateKategoriPengumuman() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); try {
resetForm(); await createState.create.create();
router.push('/admin/desa/pengumuman/kategori-pengumuman'); resetForm();
router.push('/admin/desa/pengumuman/kategori-pengumuman');
} catch (error) {
console.error(error);
toast.error('Gagal menambahkan kategori pengumuman');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -60,12 +71,23 @@ function CreateKategoriPengumuman() {
<TextInput <TextInput
label="Nama Kategori Pengumuman" label="Nama Kategori Pengumuman"
placeholder="Masukkan nama kategori pengumuman" placeholder="Masukkan nama kategori pengumuman"
defaultValue={createState.create.form.name || ''} value={createState.create.form.name || ''}
onChange={(e) => (createState.create.form.name = e.target.value)} onChange={(e) => (createState.create.form.name = e.target.value)}
required required
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -76,7 +98,7 @@ function CreateKategoriPengumuman() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -13,7 +13,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader
} from "@mantine/core"; } from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react"; import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@@ -33,6 +34,15 @@ function EditPengumuman() {
content: "", content: "",
}); });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
categoryPengumumanId: "",
content: "",
});
// Load kategori & pengumuman by id saat pertama kali // Load kategori & pengumuman by id saat pertama kali
useEffect(() => { useEffect(() => {
editState.category.findMany.load(); editState.category.findMany.load();
@@ -50,6 +60,12 @@ function EditPengumuman() {
categoryPengumumanId: data.categoryPengumumanId || "", categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || "", content: data.content || "",
}); });
setOriginalData({
judul: data.judul || "",
deskripsi: data.deskripsi || "",
categoryPengumumanId: data.categoryPengumumanId || "",
content: data.content || "",
});
} }
} catch (error) { } catch (error) {
console.error("Error loading pengumuman:", error); console.error("Error loading pengumuman:", error);
@@ -66,6 +82,7 @@ function EditPengumuman() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// update global state hanya sekali pas submit // update global state hanya sekali pas submit
editState.pengumuman.edit.form = { editState.pengumuman.edit.form = {
...editState.pengumuman.edit.form, ...editState.pengumuman.edit.form,
@@ -78,9 +95,21 @@ function EditPengumuman() {
} catch (error) { } catch (error) {
console.error("Error updating pengumuman:", error); console.error("Error updating pengumuman:", error);
toast.error("Terjadi kesalahan saat memperbarui pengumuman"); toast.error("Terjadi kesalahan saat memperbarui pengumuman");
} finally {
setIsSubmitting(false);
} }
}; };
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
deskripsi: originalData.deskripsi,
categoryPengumumanId: originalData.categoryPengumumanId,
content: originalData.content,
});
toast.info("Form dikembalikan ke data awal");
};
return ( return (
<Box px={{ base: "sm", md: "lg" }} py="md"> <Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md"> <Group mb="md">
@@ -152,17 +181,29 @@ function EditPengumuman() {
</Box> </Box>
<Group justify="right"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
size="md" size="md"
style={{ style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: "#fff", color: '#fff',
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)", boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -12,25 +12,37 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePengumuman() { function CreatePengumuman() {
const pengumumanState = useProxy(stateDesaPengumuman); const pengumumanState = useProxy(stateDesaPengumuman);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
useShallowEffect(() => { useShallowEffect(() => {
pengumumanState.category.findMany.load(); pengumumanState.category.findMany.load();
}, []); }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
await pengumumanState.pengumuman.create.create(); try {
resetForm(); setIsSubmitting(true);
router.push('/admin/desa/pengumuman/list-pengumuman'); await pengumumanState.pengumuman.create.create();
resetForm();
router.push('/admin/desa/pengumuman/list-pengumuman');
} catch (error) {
console.error('Error creating pengumuman:', error);
toast.error('Terjadi kesalahan saat membuat pengumuman');
} finally {
setIsSubmitting(false);
}
}; };
const resetForm = () => { const resetForm = () => {
@@ -46,9 +58,9 @@ function CreatePengumuman() {
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Pengumuman Tambah Pengumuman
</Title> </Title>
@@ -65,7 +77,7 @@ function CreatePengumuman() {
<Stack gap="md"> <Stack gap="md">
{/* Judul */} {/* Judul */}
<TextInput <TextInput
defaultValue={pengumumanState.pengumuman.create.form.judul} value={pengumumanState.pengumuman.create.form.judul}
onChange={(val) => (pengumumanState.pengumuman.create.form.judul = val.target.value)} onChange={(val) => (pengumumanState.pengumuman.create.form.judul = val.target.value)}
label="Judul" label="Judul"
placeholder="Masukkan judul pengumuman" placeholder="Masukkan judul pengumuman"
@@ -76,21 +88,32 @@ function CreatePengumuman() {
<Select <Select
label="Kategori" label="Kategori"
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={pengumumanState.pengumuman.create.form.categoryPengumumanId || ""}
onChange={(val) => {
pengumumanState.pengumuman.create.form.categoryPengumumanId = val ?? "";
}}
data={pengumumanState.category.findMany.data?.map((item) => ({ data={pengumumanState.category.findMany.data?.map((item) => ({
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} })) || []}
value={pengumumanState.pengumuman.create.form.categoryPengumumanId || null}
onChange={(val: string | null) => {
if (val) {
const selected = pengumumanState.category.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
pengumumanState.pengumuman.create.form.categoryPengumumanId = selected.id;
}
} else {
pengumumanState.pengumuman.create.form.categoryPengumumanId = '';
}
}}
searchable searchable
clearable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
required
/> />
{/* Deskripsi Singkat */} {/* Deskripsi Singkat */}
<TextInput <TextInput
defaultValue={pengumumanState.pengumuman.create.form.deskripsi} value={pengumumanState.pengumuman.create.form.deskripsi}
onChange={(val) => (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)} onChange={(val) => (pengumumanState.pengumuman.create.form.deskripsi = val.target.value)}
label="Deskripsi Singkat" label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat" placeholder="Masukkan deskripsi singkat"
@@ -112,6 +135,17 @@ function CreatePengumuman() {
{/* Tombol Submit */} {/* Tombol Submit */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -122,7 +156,7 @@ function CreatePengumuman() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -9,7 +9,8 @@ import {
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -26,6 +27,12 @@ function EditKategoriPotensi() {
nama: '', nama: '',
}); });
const [originalData, setOriginalData] = useState({
nama: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data dari backend -> isi ke formData lokal // Load data dari backend -> isi ke formData lokal
useEffect(() => { useEffect(() => {
const loadKategori = async () => { const loadKategori = async () => {
@@ -38,6 +45,9 @@ function EditKategoriPotensi() {
setFormData({ setFormData({
nama: data.nama || '', nama: data.nama || '',
}); });
setOriginalData({
nama: data.nama || '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading kategori potensi:', error); console.error('Error loading kategori potensi:', error);
@@ -55,8 +65,16 @@ function EditKategoriPotensi() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// Update global state hanya pas submit // Update global state hanya pas submit
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
@@ -69,6 +87,8 @@ function EditKategoriPotensi() {
} catch (error) { } catch (error) {
console.error('Error updating kategori potensi:', error); console.error('Error updating kategori potensi:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori potensi'); toast.error('Terjadi kesalahan saat memperbarui kategori potensi');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -106,6 +126,17 @@ function EditKategoriPotensi() {
/> />
<Group justify="flex-end"> <Group justify="flex-end">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -116,7 +147,7 @@ function EditKategoriPotensi() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -8,15 +8,18 @@ import {
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKategoriPotensi() { function CreateKategoriPotensi() {
const createState = useProxy(potensiDesaState.kategoriPotensi); const createState = useProxy(potensiDesaState.kategoriPotensi);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
createState.create.form = { createState.create.form = {
@@ -25,23 +28,30 @@ function CreateKategoriPotensi() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await createState.create.create(); try {
resetForm(); setIsSubmitting(true);
router.push('/admin/desa/potensi/kategori-potensi'); await createState.create.create();
resetForm();
router.push('/admin/desa/potensi/kategori-potensi');
} catch (error) {
console.error('Error creating kategori potensi:', error);
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */} {/* Header dengan back button */}
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Kategori Potensi Tambah Kategori Potensi
</Title> </Title>
@@ -60,12 +70,23 @@ function CreateKategoriPotensi() {
<TextInput <TextInput
label="Nama Kategori Potensi" label="Nama Kategori Potensi"
placeholder="Masukkan nama kategori potensi" placeholder="Masukkan nama kategori potensi"
defaultValue={createState.create.form.nama || ''} value={createState.create.form.nama || ''}
onChange={(e) => (createState.create.form.nama = e.target.value)} onChange={(e) => (createState.create.form.nama = e.target.value)}
required required
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -76,7 +97,7 @@ function CreateKategoriPotensi() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -15,7 +15,9 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader,
ActionIcon
} from "@mantine/core"; } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone"; import { Dropzone } from "@mantine/dropzone";
import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react"; import { IconArrowBack, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
@@ -38,6 +40,16 @@ function EditPotensi() {
content: "", content: "",
imageId: "", imageId: "",
}); });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
deskripsi: "",
kategoriId: "",
content: "",
imageId: "",
imageUrl: "",
});
// handle input changes // handle input changes
const handleChange = (field: string, value: string) => { const handleChange = (field: string, value: string) => {
@@ -46,11 +58,11 @@ function EditPotensi() {
useEffect(() => { useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load(); potensiDesaState.kategoriPotensi.findMany.load();
const loadPotensi = async () => { const loadPotensi = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await potensiState.edit.load(id); const data = await potensiState.edit.load(id);
if (data) { if (data) {
@@ -61,35 +73,45 @@ function EditPotensi() {
content: data.content || "", content: data.content || "",
imageId: data.imageId || "", imageId: data.imageId || "",
}); });
// // merge, bukan replace setOriginalData({
// setFormData((prev) => ({ name: data.name || "",
// ...prev, deskripsi: data.deskripsi || "",
// name: data.name ?? prev.name, kategoriId: data.kategoriId || "",
// deskripsi: data.deskripsi ?? prev.deskripsi, content: data.content || "",
// kategoriId: data.kategoriId ?? prev.kategoriId, imageId: data.imageId || "",
// content: data.content ?? prev.content, imageUrl: data.image?.link || "",
// imageId: data.imageId ?? prev.imageId, });
// })); setPreviewImage(data.image.link);
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading potensi:", error); console.error("Error loading potensi:", error);
toast.error("Gagal memuat data potensi"); toast.error("Gagal memuat data potensi");
} }
}; };
loadPotensi(); loadPotensi();
}, [params?.id]); }, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name || "",
deskripsi: originalData.deskripsi || "",
kategoriId: originalData.kategoriId || "",
content: originalData.content || "",
imageId: originalData.imageId || ""
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
let imageId = formData.imageId; let imageId = formData.imageId;
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
@@ -115,20 +137,22 @@ function EditPotensi() {
} catch (error) { } catch (error) {
console.error("Error updating potensi:", error); console.error("Error updating potensi:", error);
toast.error("Terjadi kesalahan saat memperbarui potensi"); toast.error("Terjadi kesalahan saat memperbarui potensi");
} finally {
setIsSubmitting(false);
} }
}; };
return ( return (
<Box px={{ base: "sm", md: "lg" }} py="md"> <Box px={{ base: "sm", md: "lg" }} py="md">
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors["blue-button"]} size={24} /> <IconArrowBack color={colors["blue-button"]} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Potensi Desa Edit Potensi Desa
</Title> </Title>
@@ -164,6 +188,32 @@ function EditPotensi() {
</Box> </Box>
<Select <Select
label="Kategori"
placeholder="Pilih kategori"
data={potensiDesaState.kategoriPotensi.findMany.data?.map((item) => ({
label: item.nama,
value: item.id,
})) || []}
value={formData.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = potensiDesaState.kategoriPotensi.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
handleChange("kategoriId", selected.id);
}
} else {
handleChange("kategoriId", "");
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
{/* <Select
value={formData.kategoriId} value={formData.kategoriId}
onChange={(val) => handleChange("kategoriId", val || "")} onChange={(val) => handleChange("kategoriId", val || "")}
label="Kategori" label="Kategori"
@@ -178,7 +228,7 @@ function EditPotensi() {
searchable searchable
required required
error={!formData.kategoriId ? "Pilih kategori" : undefined} error={!formData.kategoriId ? "Pilih kategori" : undefined}
/> /> */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
@@ -219,25 +269,45 @@ function EditPotensi() {
Seret gambar atau klik untuk memilih file Seret gambar atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: "flex", justifyContent: "center" }}> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ style={{
maxHeight: 220, maxHeight: 200,
objectFit: "contain", objectFit: 'contain',
border: `1px solid ${colors["blue-button"]}`, border: '1px solid #ddd',
}} }}
loading="lazy" loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -255,17 +325,29 @@ function EditPotensi() {
</Box> </Box>
<Group justify="right"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
size="md" size="md"
style={{ style={{
background: `linear-gradient(135deg, ${colors["blue-button"]}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: "#fff", color: '#fff',
boxShadow: "0 4px 15px rgba(79, 172, 254, 0.4)", boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -14,7 +14,9 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader,
ActionIcon
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -28,30 +30,39 @@ function CreatePotensi() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => { useEffect(() => {
potensiDesaState.kategoriPotensi.findMany.load(); potensiDesaState.kategoriPotensi.findMany.load();
}, []); }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); try {
setIsSubmitting(true);
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
}); });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error('Gagal upload gambar'); return toast.error('Gagal upload gambar');
}
potensiState.create.form.imageId = uploaded.id;
await potensiState.create.create();
resetForm();
router.push('/admin/desa/potensi/list-potensi');
} catch (error) {
console.error('Error creating potensi:', error);
toast.error('Terjadi kesalahan saat menambahkan potensi');
} finally {
setIsSubmitting(false);
} }
potensiState.create.form.imageId = uploaded.id;
await potensiState.create.create();
resetForm();
router.push('/admin/desa/potensi/list-potensi');
}; };
const resetForm = () => { const resetForm = () => {
@@ -71,9 +82,9 @@ function CreatePotensi() {
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Potensi Desa Tambah Potensi Desa
</Title> </Title>
@@ -90,7 +101,7 @@ function CreatePotensi() {
<Stack gap="md"> <Stack gap="md">
{/* Judul */} {/* Judul */}
<TextInput <TextInput
defaultValue={potensiState.create.form.name} value={potensiState.create.form.name}
onChange={(val) => (potensiState.create.form.name = val.target.value)} onChange={(val) => (potensiState.create.form.name = val.target.value)}
label="Judul" label="Judul"
placeholder="Masukkan judul potensi" placeholder="Masukkan judul potensi"
@@ -112,6 +123,32 @@ function CreatePotensi() {
{/* Kategori */} {/* Kategori */}
<Select <Select
label="Kategori"
placeholder="Pilih kategori"
data={potensiDesaState.kategoriPotensi.findMany.data?.map((item) => ({
label: item.nama,
value: item.id,
})) || []}
value={potensiState.create.form.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = potensiDesaState.kategoriPotensi.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
potensiState.create.form.kategoriId = selected.id;
}
} else {
potensiState.create.form.kategoriId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
{/* <Select
label="Kategori" label="Kategori"
placeholder="Pilih kategori" placeholder="Pilih kategori"
value={potensiState.create.form.kategoriId || ""} value={potensiState.create.form.kategoriId || ""}
@@ -122,7 +159,7 @@ function CreatePotensi() {
value: item.id, value: item.id,
label: item.nama, label: item.nama,
}))} }))}
/> /> */}
{/* Upload Gambar */} {/* Upload Gambar */}
<Box> <Box>
@@ -139,7 +176,7 @@ function CreatePotensi() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -157,17 +194,44 @@ function CreatePotensi() {
<Text ta="center" mt="sm" size="sm" color="dimmed"> <Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB) Seret gambar atau klik untuk memilih file (maks 5MB)
</Text> </Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{
loading='lazy' maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -187,6 +251,17 @@ function CreatePotensi() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -197,7 +272,7 @@ function CreatePotensi() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -1,152 +1,244 @@
/* eslint-disable react-hooks/exhaustive-deps */ 'use client';
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Alert,
Box,
Button,
Center,
Group,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// 🧩 Type untuk form
interface FormData {
judul: string;
deskripsi: string;
}
// 🧩 Main Component
function Page() { function Page() {
const lambangState = useProxy(stateProfileDesa.lambangDesa) const router = useRouter();
const router = useRouter() const params = useParams();
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
try { const [formData, setFormData] = useState<FormData>({ judul: '', deskripsi: '' });
const data = await lambangState.findUnique.load(id); const [originalData, setOriginalData] = useState<FormData>({ judul: '', deskripsi: '' });
lambangState.update.initialize(data); const [isLoading, setIsLoading] = useState(true);
} catch (error) { const [isSubmitting, setIsSubmitting] = useState(false);
console.error("Error loading lambang:", error); const [loadError, setLoadError] = useState<string | null>(null);
toast.error("Gagal memuat data lambang desa");
}
};
loadData(); // 🧭 Load data awal
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
return;
}
return () => { setIsLoading(true);
lambangState.update.reset(); setLoadError(null);
lambangState.findUnique.reset();
};
}, [params?.id, router]);
const handleSubmit = async () => { try {
if (isSubmitting || !lambangState.update.form.judul.trim()) { const data = await stateProfileDesa.lambangDesa.findUnique.load(id);
toast.error("Judul wajib diisi");
return; if (data) {
} const initial: FormData = {
judul: data.judul || '',
setIsSubmitting(true); deskripsi: data.deskripsi || '',
};
try { setFormData(initial);
const success = await lambangState.update.submit(); setOriginalData(initial);
if (success) { // Penting untuk isi id di state sebelum submit
toast.success("Data berhasil disimpan"); stateProfileDesa.lambangDesa.update.initialize(data);
router.push("/admin/desa/profile/profile-desa"); } else {
} setLoadError('Data tidak ditemukan');
} catch (error) {
console.error("Error update lambang desa:", error);
toast.error("Terjadi kesalahan saat update lambang desa");
} finally {
setIsSubmitting(false);
} }
} catch (error) {
console.error('Error loading lambang:', error);
setLoadError('Gagal memuat data lambang desa');
toast.error('Gagal memuat data lambang desa');
} finally {
setIsLoading(false);
}
}; };
const handleBack = () => router.back(); loadData();
// Loading state return () => {
if (lambangState.findUnique.loading || lambangState.update.loading) { stateProfileDesa.lambangDesa.update.reset();
return ( stateProfileDesa.lambangDesa.findUnique.reset();
<Box> };
<Center h={400}> }, [params?.id, router]);
<Text>Memuat data...</Text>
</Center> // 🔁 Reset form
</Box> const handleResetForm = () => {
); setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit handler
const handleSubmit = async () => {
if (!formData.judul.trim()) {
toast.error('Judul wajib diisi');
return;
} }
// Error state setIsSubmitting(true);
if (lambangState.findUnique.error) { try {
return ( const state = stateProfileDesa.lambangDesa;
<Box> state.update.form.judul = formData.judul;
<Stack gap="md"> state.update.form.deskripsi = formData.deskripsi;
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{lambangState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
const success = await state.update.submit();
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
} else {
toast.error('Gagal menyimpan data');
}
} catch (error) {
console.error('Error update lambang desa:', error);
toast.error('Terjadi kesalahan saat update lambang desa');
} finally {
setIsSubmitting(false);
}
};
// 📝 Handlers
const handleJudulChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, judul: e.target.value }));
};
const handleDeskripsiChange = (html: string) => {
setFormData(prev => ({ ...prev, deskripsi: html }));
};
const handleBack = () => router.back();
// 🔄 Loading
if (isLoading) {
return ( return (
<Box> <Box>
<Stack gap="xs"> <Center h={400}>
<Group mb="md"> <Stack align="center" gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Loader size="lg" color={colors['blue-button']} />
<IconArrowBack color={colors['blue-button']} size={24} /> <Text size="lg" fw={500} c="dimmed">
</Button> Memuat data lambang desa...
<Title order={4} ml="sm" c="dark">Edit Lambang Desa</Title> </Text>
</Group> </Stack>
</Center>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}> </Box>
<Stack gap="xs">
<Title order={3}>Edit Lambang Desa</Title>
{/* Judul */}
<TextInput
label={<Text fw="bold">Judul</Text>}
placeholder="Judul lambang"
defaultValue={lambangState.update.form.judul}
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
error={!lambangState.update.form.judul && "Judul wajib diisi"}
/>
{/* Deskripsi */}
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={lambangState.update.form.deskripsi}
onChange={(val) => lambangState.update.form.deskripsi = val}
/>
</Box>
{/* Buttons */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || lambangState.update.loading}
disabled={!lambangState.update.form.judul}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || lambangState.update.loading}>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
); );
}
// ❌ Error
if (loadError) {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
{loadError}
</Alert>
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
Kembali ke Halaman Utama
</Button>
</Stack>
</Box>
);
}
// 🧱 UI utama
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
{/* Header */}
<Group mb="sm">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Lambang Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
{/* Judul */}
<TextInput
label={<Text fw="bold" size="sm">Judul</Text>}
placeholder="Masukkan judul lambang desa"
value={formData.judul}
onChange={handleJudulChange}
error={!formData.judul.trim() && 'Judul wajib diisi'}
required
size="md"
radius="md"
/>
{/* Deskripsi */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Deskripsi
</Text>
<EditEditor value={formData.deskripsi} onChange={handleDeskripsiChange} />
</Box>
{/* Tombol Aksi */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
disabled={isSubmitting}
>
Batal
</Button>
<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)',
}}
loading={isSubmitting}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -5,7 +5,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Alert, Box, Button, Center, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { Alert, Box, Button, Center, Group, Image, Loader, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -19,8 +19,8 @@ function Page() {
const params = useParams(); const params = useParams();
const [images, setImages] = useState< const [images, setImages] = useState<
Array<{ file: File | null; preview: string; label: string; imageId?: string }> Array<{ file: File | null; preview: string; label: string; imageId?: string }>
>([]); >([]);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: '', judul: '',
deskripsi: '', deskripsi: '',
@@ -28,6 +28,12 @@ function Page() {
}); });
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
images: [] as Array<{ label: string; imageId: string }>
});
// Load data // Load data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -52,6 +58,17 @@ function Page() {
})), })),
}); });
setOriginalData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
images: (data.images || []).map((img: any) => ({
label: img.label,
imageId: img.image?.id ?? '',
preview: img.image?.link ?? '',
})),
});
if (data?.images?.length > 0 && data.images[0].image?.link) { if (data?.images?.length > 0 && data.images[0].image?.link) {
setImages(data.images.map((img: any) => ({ setImages(data.images.map((img: any) => ({
file: null, file: null,
@@ -77,15 +94,36 @@ function Page() {
const handleBack = () => router.back(); const handleBack = () => router.back();
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
deskripsi: originalData.deskripsi,
images: originalData.images.map((img) => ({
label: img.label,
imageId: img.imageId,
})),
});
setImages(
originalData.images.map((img: any) => ({
file: null,
preview: img.preview, // pakai preview masing-masing, bukan cuma satu
label: img.label,
imageId: img.imageId,
}))
);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !formData.judul.trim()) { if (isSubmitting || !formData.judul.trim()) {
toast.error("Judul wajib diisi"); toast.error("Judul wajib diisi");
return; return;
} }
setIsSubmitting(true);
try { try {
setIsSubmitting(true);
const uploadedImages = []; const uploadedImages = [];
// Upload semua gambar baru // Upload semua gambar baru
@@ -95,7 +133,7 @@ function Page() {
uploadedImages.push({ imageId: img.imageId, label: img.label }); uploadedImages.push({ imageId: img.imageId, label: img.label });
continue; continue;
} }
// upload baru // upload baru
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file: img.file, file: img.file,
@@ -108,7 +146,7 @@ function Page() {
} }
uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" }); uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
} }
// Update ke global state // Update ke global state
maskotState.update.updateField("judul", formData.judul); maskotState.update.updateField("judul", formData.judul);
@@ -161,9 +199,9 @@ function Page() {
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Group mb="md"> <Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title> <Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title>
</Group> </Group>
@@ -175,7 +213,7 @@ function Page() {
<TextInput <TextInput
label={<Text fw="bold">Judul</Text>} label={<Text fw="bold">Judul</Text>}
placeholder="Masukkan judul maskot" placeholder="Masukkan judul maskot"
defaultValue={formData.judul} value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })} onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })}
error={!formData.judul && "Judul wajib diisi"} error={!formData.judul && "Judul wajib diisi"}
/> />
@@ -231,7 +269,7 @@ function Page() {
setImages(updated); setImages(updated);
}} }}
> >
Hapus <IconX size={16} />
</Button> </Button>
</Group> </Group>
<Image <Image
@@ -260,18 +298,31 @@ function Page() {
</SimpleGrid> </SimpleGrid>
{/* Buttons */} {/* Buttons */}
<Group> <Group justify="right">
{/* Tombol Batal */}
<Button <Button
bg={colors['blue-button']} variant="outline"
onClick={handleSubmit} color="gray"
loading={isSubmitting || maskotState.update.loading} radius="md"
disabled={!formData.judul} size="md"
onClick={handleResetForm}
> >
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || maskotState.update.loading}>
Batal Batal
</Button> </Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,146 +1,272 @@
/* eslint-disable react-hooks/exhaustive-deps */ 'use client';
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Alert,
Box,
Button,
Center,
Group,
Loader,
Paper,
Stack,
Text,
TextInput,
Title
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// 🔹 Types
interface FormData {
judul: string;
deskripsi: string;
}
// 🔹 Main Component
function Page() { function Page() {
const sejarahState = useProxy(stateProfileDesa.sejarahDesa) const router = useRouter();
const router = useRouter() const params = useParams();
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data // 🧩 Local State
const [formData, setFormData] = useState<FormData>({
judul: '',
deskripsi: '',
});
const [originalData, setOriginalData] = useState<FormData>({
judul: '',
deskripsi: '',
});
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
// 🧭 Load Initial Data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error("ID tidak valid"); toast.error('ID tidak valid');
router.push("/admin/desa/profile/profile-desa"); router.push('/admin/desa/profile/profile-desa');
return; return;
} }
setIsLoading(true);
setLoadError(null);
try { try {
const data = await sejarahState.findUnique.load(id); const data = await stateProfileDesa.sejarahDesa.findUnique.load(id);
if (data) { if (data) {
sejarahState.update.initialize(data); const initialData: FormData = {
judul: data.judul || '',
deskripsi: data.deskripsi || '',
};
setFormData(initialData);
setOriginalData(initialData);
stateProfileDesa.sejarahDesa.update.initialize(data);
} else {
setLoadError('Data tidak ditemukan');
} }
} catch (error) { } catch (error) {
console.error("Error loading sejarah:", error); console.error('Error loading sejarah:', error);
toast.error("Gagal memuat data sejarah desa"); setLoadError('Gagal memuat data sejarah desa');
toast.error('Gagal memuat data sejarah desa');
} finally {
setIsLoading(false);
} }
}; };
loadData(); loadData();
return () => { return () => {
sejarahState.update.reset(); stateProfileDesa.sejarahDesa.update.reset();
sejarahState.findUnique.reset(); stateProfileDesa.sejarahDesa.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
// 🔄 Check if form has changes
// 🔁 Reset Form to Original Data
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit Handler
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !sejarahState.update.form.judul.trim()) { // Validation
toast.error("Judul wajib diisi"); if (!formData.judul.trim()) {
toast.error('Judul wajib diisi');
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const success = await sejarahState.update.submit(); // Access original state directly (not proxy)
const originalState = stateProfileDesa.sejarahDesa;
// Update form data
originalState.update.form.judul = formData.judul;
originalState.update.form.deskripsi = formData.deskripsi;
// Submit
const success = await originalState.update.submit();
if (success) { if (success) {
toast.success("Data berhasil disimpan"); toast.success('Data berhasil disimpan');
router.push("/admin/desa/profile/profile-desa"); router.push('/admin/desa/profile/profile-desa');
} else {
toast.error('Gagal menyimpan data');
} }
} catch (error) { } catch (error) {
console.error("Error update sejarah desa:", error); console.error('Error update sejarah desa:', error);
toast.error("Terjadi kesalahan saat update sejarah desa"); toast.error('Terjadi kesalahan saat update sejarah desa');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
const handleBack = () => router.back(); // 📝 Form Field Handlers
const handleJudulChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, judul: e.target.value }));
};
// Loading state const handleDeskripsiChange = (html: string) => {
if (sejarahState.findUnique.loading || sejarahState.update.loading) { setFormData(prev => ({ ...prev, deskripsi: html }));
};
const handleBack = () => {
router.back();
};
// 🔄 Loading State
if (isLoading) {
return ( return (
<Box> <Box>
<Center h={400}> <Center h={400}>
<Text>Memuat data...</Text> <Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} />
<Text size="lg" fw={500} c="dimmed">
Memuat data...
</Text>
</Stack>
</Center> </Center>
</Box> </Box>
); );
} }
// Error state // Error State
if (sejarahState.findUnique.error) { if (loadError) {
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md"> <Stack gap="md">
<Button variant="subtle" onClick={handleBack}> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={20} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Alert icon={<IconAlertCircle size={16} />} color="red"> <Alert
<Text fw="bold">Error</Text> icon={<IconAlertCircle size={20} />}
<Text>{sejarahState.findUnique.error}</Text> color="red"
title="Terjadi Kesalahan"
radius="md"
>
{loadError}
</Alert> </Alert>
<Button
onClick={() => router.push('/admin/desa/profile/profile-desa')}
variant="outline"
>
Kembali ke Halaman Utama
</Button>
</Stack> </Stack>
</Box> </Box>
); );
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="xs"> <Stack gap="md">
<Group mb="md"> {/* Header */}
<Group mb="sm">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark">Edit Sejarah Desa</Title> <Title order={4} ml="sm" c="dark">
Edit Sejarah Desa
</Title>
</Group> </Group>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}> {/* Form */}
<Stack gap="xs"> <Paper
<Title order={3}>Edit Sejarah Desa</Title> w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
{/* Judul */} p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
{/* Form Title */}
<Box>
<Title order={3} mb={4}>
Informasi Sejarah Desa
</Title>
</Box>
{/* Judul Field */}
<TextInput <TextInput
label={<Text fw="bold">Judul</Text>} label={<Text fw="bold" size="sm">Judul Sejarah</Text>}
placeholder="Judul sejarah" placeholder="Masukkan judul sejarah desa"
defaultValue={sejarahState.update.form.judul} value={formData.judul}
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} onChange={handleJudulChange}
error={!sejarahState.update.form.judul && "Judul wajib diisi"} error={!formData.judul.trim() && 'Judul wajib diisi'}
required
size="md"
radius="md"
/> />
{/* Deskripsi */} {/* Deskripsi Field */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" size="sm" mb={8}>
Deskripsi Sejarah
</Text>
<EditEditor <EditEditor
value={sejarahState.update.form.deskripsi} value={formData.deskripsi}
onChange={(val) => sejarahState.update.form.deskripsi = val} onChange={handleDeskripsiChange}
/> />
</Box> </Box>
{/* Buttons */} {/* Action Buttons */}
<Group> <Group justify="right">
{/* Tombol Batal */}
<Button <Button
bg={colors['blue-button']} variant="outline"
onClick={handleSubmit} color="gray"
loading={isSubmitting || sejarahState.update.loading} radius="md"
disabled={!sejarahState.update.form.judul} size="md"
onClick={handleResetForm}
> >
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} Batal
</Button> </Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || sejarahState.update.loading}> {/* Tombol Simpan */}
Batal <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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -150,4 +276,4 @@ function Page() {
); );
} }
export default Page; export default Page;

View File

@@ -1,155 +1,247 @@
/* eslint-disable react-hooks/exhaustive-deps */ 'use client';
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title } from '@mantine/core'; import {
Alert,
Box,
Button,
Center,
Group,
Loader,
Paper,
Stack,
Text,
Title,
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react'; import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// 🔹 Types
interface FormData {
visi: string;
misi: string;
}
// 🔹 Main Component
function Page() { function Page() {
const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa) const router = useRouter();
const router = useRouter() const params = useParams();
const params = useParams() const [formData, setFormData] = useState<FormData>({ visi: '', misi: '' });
const [isSubmitting, setIsSubmitting] = useState(false); const [originalData, setOriginalData] = useState<FormData>({ visi: '', misi: '' });
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
// Load data // 🧭 Load Data
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) { if (!id) {
toast.error("ID tidak valid"); toast.error('ID tidak valid');
router.push("/admin/desa/profile/profile-desa"); router.push('/admin/desa/profile/profile-desa');
return; return;
} }
try { setIsLoading(true);
const data = await visiMisiState.findUnique.load(id); setLoadError(null);
visiMisiState.update.initialize(data);
} catch (error) {
console.error("Error loading visi misi:", error);
toast.error("Gagal memuat data visi misi desa");
}
};
loadData(); try {
const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id);
if (data) {
const initialData: FormData = {
visi: data.visi || '',
misi: data.misi || '',
};
setFormData(initialData);
setOriginalData(initialData);
return () => { // set id ke state agar submit pakai endpoint benar
visiMisiState.update.reset(); stateProfileDesa.visiMisiDesa.update.initialize(data);
visiMisiState.findUnique.reset(); } else {
}; setLoadError('Data tidak ditemukan');
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
toast.error("Visi wajib diisi");
return;
}
setIsSubmitting(true);
try {
const success = await visiMisiState.update.submit();
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update visi misi desa:", error);
toast.error("Terjadi kesalahan saat update visi misi desa");
} finally {
setIsSubmitting(false);
} }
} catch (error) {
console.error('Error load visi misi:', error);
setLoadError('Gagal memuat data visi misi desa');
toast.error('Gagal memuat data visi misi desa');
} finally {
setIsLoading(false);
}
}; };
const handleBack = () => router.back(); loadData();
// Loading state return () => {
if (visiMisiState.findUnique.loading || visiMisiState.update.loading) { stateProfileDesa.visiMisiDesa.update.reset();
return ( stateProfileDesa.visiMisiDesa.findUnique.reset();
<Box> };
<Center h={400}> }, [params?.id, router]);
<Text>Memuat data...</Text>
</Center> // 🔄 Reset Form
</Box> const handleResetForm = () => {
); setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit
const handleSubmit = async () => {
if (!formData.visi.trim()) {
toast.error('Visi wajib diisi');
return;
} }
// Error state setIsSubmitting(true);
if (visiMisiState.findUnique.error) { try {
return ( const originalState = stateProfileDesa.visiMisiDesa;
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{visiMisiState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
// update data form ke state sebelum submit
originalState.update.form.visi = formData.visi;
originalState.update.form.misi = formData.misi;
const success = await originalState.update.submit();
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
} else {
toast.error('Gagal menyimpan data');
}
} catch (error) {
console.error('Error update visi misi desa:', error);
toast.error('Terjadi kesalahan saat update visi misi desa');
} finally {
setIsSubmitting(false);
}
};
// 🧭 Field handlers
const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html }));
const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html }));
const handleBack = () => router.back();
// ⏳ Loading
if (isLoading) {
return ( return (
<Box> <Box>
<Stack gap="xs"> <Center h={400}>
<Group mb="md"> <Stack align="center" gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Loader size="lg" color={colors['blue-button']} />
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <Text size="lg" fw={500} c="dimmed">
<IconArrowBack color={colors['blue-button']} size={24} /> Memuat data...
</Button> </Text>
</Button> </Stack>
<Title order={4} ml="sm" c="dark">Edit Visi Misi Desa</Title> </Center>
</Group> </Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="xs">
<Title order={3}>Edit Visi Misi Desa</Title>
{/* Visi */}
<Box>
<Text fz={"md"} fw={"bold"}>Visi</Text>
<EditEditor
value={visiMisiState.update.form.visi}
onChange={(val) => visiMisiState.update.form.visi = val}
/>
</Box>
{/* Misi */}
<Box>
<Text fz={"md"} fw={"bold"}>Misi</Text>
<EditEditor
value={visiMisiState.update.form.misi}
onChange={(val) => visiMisiState.update.form.misi = val}
/>
</Box>
{/* Buttons */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || visiMisiState.update.loading}
disabled={!visiMisiState.update.form.visi}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || visiMisiState.update.loading}>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
); );
}
// ❌ Error
if (loadError) {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Alert
icon={<IconAlertCircle size={20} />}
color="red"
title="Terjadi Kesalahan"
radius="md"
>
{loadError}
</Alert>
<Button
onClick={() => router.push('/admin/desa/profile/profile-desa')}
variant="outline"
>
Kembali ke Halaman Utama
</Button>
</Stack>
</Box>
);
}
// ✅ UI
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
{/* Header */}
<Group mb="sm">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Visi & Misi Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
<Box>
<Title order={3} mb={4}>
Informasi Visi & Misi Desa
</Title>
</Box>
{/* Visi */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Visi
</Text>
<EditEditor value={formData.visi} onChange={handleVisiChange} />
</Box>
{/* Misi */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Misi
</Text>
<EditEditor value={formData.misi} onChange={handleMisiChange} />
</Box>
{/* Actions */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
<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)',
}}
loading={isSubmitting}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
} }
export default Page; export default Page;

View File

@@ -4,6 +4,7 @@ import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
@@ -12,7 +13,8 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Loader
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -35,6 +37,16 @@ function EditPerbekelDariMasaKeMasa() {
imageId: '' imageId: ''
}); });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
nama: '',
daerah: '',
periode: '',
imageId: '',
imageUrl: "",
});
// load data pertama kali // load data pertama kali
useEffect(() => { useEffect(() => {
const loadFoto = async () => { const loadFoto = async () => {
@@ -49,9 +61,14 @@ function EditPerbekelDariMasaKeMasa() {
periode: data.periode || '', periode: data.periode || '',
imageId: data.imageId || '' imageId: data.imageId || ''
}); });
if (data?.imageGalleryFoto?.link) { setOriginalData({
setPreviewImage(data.imageGalleryFoto.link); nama: data.nama || '',
} daerah: data.daerah || '',
periode: data.periode || '',
imageId: data.imageId || '',
imageUrl: data.image.link || '',
})
setPreviewImage(data.image.link);
} }
} catch (error) { } catch (error) {
console.error('Error loading foto:', error); console.error('Error loading foto:', error);
@@ -69,8 +86,22 @@ function EditPerbekelDariMasaKeMasa() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
daerah: originalData.daerah,
periode: originalData.periode,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// update global state hanya sekali pas submit // update global state hanya sekali pas submit
state.update.form = { ...state.update.form, ...formData }; state.update.form = { ...state.update.form, ...formData };
@@ -90,15 +121,17 @@ function EditPerbekelDariMasaKeMasa() {
} catch (error) { } catch (error) {
console.error('Error updating perbekel dari masa ke masa:', error); console.error('Error updating perbekel dari masa ke masa:', error);
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa'); toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
} finally {
setIsSubmitting(false);
} }
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Perbekel Dari Masa Ke Masa Edit Perbekel Dari Masa Ke Masa
</Title> </Title>
@@ -135,7 +168,7 @@ function EditPerbekelDariMasaKeMasa() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -154,25 +187,45 @@ function EditPerbekelDariMasaKeMasa() {
Seret gambar atau klik untuk memilih file Seret gambar atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ style={{
maxHeight: 220, maxHeight: 200,
objectFit: 'contain', objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`, border: '1px solid #ddd',
}} }}
loading="lazy" loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -194,6 +247,17 @@ function EditPerbekelDariMasaKeMasa() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -204,7 +268,7 @@ function EditPerbekelDariMasaKeMasa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -2,7 +2,7 @@
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile'; import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Loader, ActionIcon, Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -14,6 +14,7 @@ function CreatePerbekelDariMasaKeMasa() {
const state = useProxy(stateProfileDesa.mantanPerbekel); const state = useProxy(stateProfileDesa.mantanPerbekel);
const router = useRouter(); const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const resetForm = () => { const resetForm = () => {
@@ -28,31 +29,39 @@ function CreatePerbekelDariMasaKeMasa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { try {
return toast.warn('Pilih file gambar terlebih dahulu'); setIsSubmitting(true);
if (!file) {
return toast.warn('Pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
} catch (error) {
console.error(error);
toast.error('Gagal menambahkan perbekel dari masa ke masa');
} finally {
setIsSubmitting(false);
} }
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) return toast.error('Gagal upload gambar');
state.create.form.imageId = uploaded.id;
await state.create.create();
resetForm();
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back button + Title */} {/* Back button + Title */}
<Group mb="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} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Create Perbekel Dari Masa Ke Masa Create Perbekel Dari Masa Ke Masa
</Title> </Title>
@@ -70,21 +79,21 @@ function CreatePerbekelDariMasaKeMasa() {
<TextInput <TextInput
label="Nama Perbekel" label="Nama Perbekel"
placeholder="Masukkan nama perbekel" placeholder="Masukkan nama perbekel"
defaultValue={state.create.form.nama} value={state.create.form.nama}
onChange={(e) => (state.create.form.nama = e.target.value)} onChange={(e) => (state.create.form.nama = e.target.value)}
required required
/> />
<TextInput <TextInput
label="Daerah" label="Daerah"
placeholder="Masukkan daerah" placeholder="Masukkan daerah"
defaultValue={state.create.form.daerah} value={state.create.form.daerah}
onChange={(e) => (state.create.form.daerah = e.target.value)} onChange={(e) => (state.create.form.daerah = e.target.value)}
required required
/> />
<TextInput <TextInput
label="Periode" label="Periode"
placeholder="Masukkan periode" placeholder="Masukkan periode"
defaultValue={state.create.form.periode} value={state.create.form.periode}
onChange={(e) => (state.create.form.periode = e.target.value)} onChange={(e) => (state.create.form.periode = e.target.value)}
required required
/> />
@@ -102,7 +111,7 @@ function CreatePerbekelDariMasaKeMasa() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -118,25 +127,63 @@ function CreatePerbekelDariMasaKeMasa() {
</Dropzone.Idle> </Dropzone.Idle>
</Group> </Group>
<Text ta="center" mt="sm" size="sm" color="dimmed"> <Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB) Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{
loading='lazy' maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
{/* Submit */} {/* Submit */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -147,7 +194,7 @@ function CreatePerbekelDariMasaKeMasa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -126,8 +126,8 @@ function ProfilePerbekel() {
<Dropzone <Dropzone
onDrop={(files) => handleFileChange(files[0])} onDrop={(files) => handleFileChange(files[0])}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ '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} /></Dropzone.Accept> <Dropzone.Accept><IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /></Dropzone.Accept>

View File

@@ -1,19 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Alert,
Box, Box,
Button, Button,
Group, Group,
Loader,
MultiSelect, MultiSelect,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
@@ -22,67 +24,120 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
// ==================== HELPERS ====================
const safeStringArray = (arr: any[]): string[] => {
if (!Array.isArray(arr)) return [];
return arr
.filter(item => item != null && item !== '')
.map(item => String(item))
.filter(item => item.trim() !== '');
};
const createEmptyForm = () => ({
tahun: '',
pendapatanIds: [] as string[],
belanjaIds: [] as string[],
pembiayaanIds: [] as string[],
});
// ==================== COMPONENT ====================
function EditAPBDesa() { function EditAPBDesa() {
const apbState = useProxy(PendapatanAsliDesa.ApbDesa); const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState(createEmptyForm());
tahun: '', const [originalData, setOriginalData] = useState(createEmptyForm());
pendapatanIds: [] as string[], const [isSubmitting, setIsSubmitting] = useState(false);
belanjaIds: [] as string[], const [isLoading, setIsLoading] = useState(true);
pembiayaanIds: [] as string[],
});
// Load APB desa by id → hanya update formData, bukan global state // ==================== LOAD DATA ====================
useEffect(() => { useEffect(() => {
const loadAPBdesa = async () => { const loadAPBdesa = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) {
toast.error('ID tidak valid');
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
return;
}
try { try {
setIsLoading(true);
const data = await apbState.update.load(id); const data = await apbState.update.load(id);
if (data) {
setFormData({ if (!data) {
tahun: String(data.tahun || ''), toast.error('Data APB Desa tidak ditemukan');
pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [], return;
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
});
} }
} catch (error) {
console.error("Error loading APBdesa:", error); const normalized = {
toast.error("Gagal memuat data APBdesa"); tahun: String(data.tahun || ''),
pendapatanIds: safeStringArray(data.pendapatan?.map((p: any) => p.id) || []),
belanjaIds: safeStringArray(data.belanja?.map((b: any) => b.id) || []),
pembiayaanIds: safeStringArray(data.pembiayaan?.map((p: any) => p.id) || []),
};
setFormData(normalized);
setOriginalData(normalized);
} catch (err) {
console.error('Error loading APBdesa:', err);
toast.error('Gagal memuat data APB Desa');
} finally {
setIsLoading(false);
} }
}; };
loadAPBdesa(); loadAPBdesa();
}, [params?.id]); }, [params?.id]);
// ==================== HANDLERS ====================
const handleChange = (field: keyof typeof formData, value: any) => { const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
}; };
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// update global state cuma pas submit setIsSubmitting(true);
if (!formData.tahun.trim()) {
toast.warning('Tahun harus diisi');
return;
}
apbState.update.form = { apbState.update.form = {
...apbState.update.form, ...apbState.update.form,
tahun: Number(formData.tahun), tahun: Number(formData.tahun),
pendapatanIds: formData.pendapatanIds, pendapatanIds: safeStringArray(formData.pendapatanIds),
belanjaIds: formData.belanjaIds, belanjaIds: safeStringArray(formData.belanjaIds),
pembiayaanIds: formData.pembiayaanIds, pembiayaanIds: safeStringArray(formData.pembiayaanIds),
}; };
await apbState.update.update(); await apbState.update.update();
toast.success("APB Desa berhasil diperbarui!"); toast.success('APB Desa berhasil diperbarui!');
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"); router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
} catch (error) { } catch (error) {
console.error("Error updating APBdesa:", error); console.error('Error updating APBdesa:', error);
toast.error("Terjadi kesalahan saat memperbarui APBdesa"); toast.error('Terjadi kesalahan saat memperbarui APB Desa');
} finally {
setIsSubmitting(false);
} }
}; };
// ==================== UI ====================
if (isLoading) {
return (
<Stack align="center" py="xl">
<Loader size="lg" type="dots" />
<Text c="dimmed">Memuat data APB Desa...</Text>
</Stack>
);
}
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
@@ -114,30 +169,46 @@ function EditAPBDesa() {
<TextInput <TextInput
type="number" type="number"
value={formData.tahun} value={formData.tahun}
onChange={(e) => handleChange("tahun", e.target.value)} onChange={(e) => handleChange('tahun', e.target.value)}
label={<Text fz="sm" fw="bold">Tahun</Text>} label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Masukkan tahun anggaran" placeholder="Masukkan tahun anggaran"
required required
/> />
{/* Selects */} {/* Selects */}
<SelectPendapatan <SelectAPBItem
label="Pendapatan"
state={PendapatanAsliDesa.pendapatan}
selectedIds={formData.pendapatanIds} selectedIds={formData.pendapatanIds}
onSelectionChange={(ids) => handleChange("pendapatanIds", ids)} onSelectionChange={(ids) => handleChange('pendapatanIds', ids)}
/> />
<SelectBelanja <SelectAPBItem
label="Belanja"
state={PendapatanAsliDesa.belanja}
selectedIds={formData.belanjaIds} selectedIds={formData.belanjaIds}
onSelectionChange={(ids) => handleChange("belanjaIds", ids)} onSelectionChange={(ids) => handleChange('belanjaIds', ids)}
/> />
<SelectPembiayaan <SelectAPBItem
label="Pembiayaan"
state={PendapatanAsliDesa.pembiayaan}
selectedIds={formData.pembiayaanIds} selectedIds={formData.pembiayaanIds}
onSelectionChange={(ids) => handleChange("pembiayaanIds", ids)} onSelectionChange={(ids) => handleChange('pembiayaanIds', ids)}
/> />
{/* Save Button */} {/* Save Button */}
<Group justify="right"> <Group justify="right" mt="md">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -148,117 +219,75 @@ function EditAPBDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
); );
}
/* --- Sub Components --- */ // ==================== SUB COMPONENT ====================
function SelectAPBItem({
label,
state,
selectedIds,
onSelectionChange,
}: {
label: string;
state: any;
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const proxyState = useProxy(state);
function SelectPendapatan({ useShallowEffect(() => {
selectedIds, proxyState.findMany.load();
onSelectionChange, }, []);
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => { const data = proxyState.findMany.data;
pendapatanState.findMany.load(); const isLoading = !data;
}, []);
if (!pendapatanState.findMany.data) { const options =
return <Skeleton height={38} />; data
} ?.filter((item: any) => item?.id)
.map((item: any) => ({
value: String(item.id),
label: String(item?.name || '(Tanpa Nama)'),
})) || [];
if (isLoading) {
return ( return (
<MultiSelect <Box>
label={<Text fz="sm" fw="bold">Pendapatan</Text>} <Text fz="sm" fw="bold" mb={4}>{label}</Text>
data={pendapatanState.findMany.data.map((p: any) => ({ <Skeleton height={38} radius="sm" />
value: p.id, </Box>
label: p.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih pendapatan..."
nothingFoundMessage="Tidak ditemukan"
/>
); );
} }
function SelectBelanja({ if (options.length === 0) {
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
belanjaState.findMany.load();
}, []);
if (!belanjaState.findMany.data) {
return <Skeleton height={38} />;
}
return ( return (
<MultiSelect <Alert color="gray" variant="light">
label={<Text fz="sm" fw="bold">Belanja</Text>} <Text size="sm">
data={belanjaState.findMany.data.map((b: any) => ({ Tidak ada data {label.toLowerCase()} tersedia.
value: b.id, </Text>
label: b.name, </Alert>
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih belanja..."
nothingFoundMessage="Tidak ditemukan"
/>
); );
} }
function SelectPembiayaan({ return (
selectedIds, <MultiSelect
onSelectionChange, label={<Text fz="sm" fw="bold">{label}</Text>}
}: { data={options}
selectedIds: string[]; value={selectedIds}
onSelectionChange: (ids: string[]) => void; onChange={(ids) => onSelectionChange(safeStringArray(ids))}
}) { searchable
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); clearable
placeholder={`Pilih ${label.toLowerCase()}...`}
useShallowEffect(() => { nothingFoundMessage="Tidak ditemukan"
pembiayaanState.findMany.load(); />
}, []); );
if (!pembiayaanState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz="sm" fw="bold">Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map((p: any) => ({
value: p.id,
label: p.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih pembiayaan..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
} }
export default EditAPBDesa; export default EditAPBDesa;

View File

@@ -80,7 +80,7 @@ function DetailAPBDesa() {
Detail APB Desa Detail APB Desa
</Text> </Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs"> <Paper bg="#EEF3FBFF" p="md" radius="md" shadow="xs">
<Stack gap="sm"> <Stack gap="sm">
<Box> <Box>
<Text fz="lg" fw="bold"> <Text fz="lg" fw="bold">

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
MultiSelect, MultiSelect,
Paper, Paper,
Skeleton, Skeleton,
@@ -17,11 +18,14 @@ import {
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateAPBDesa() { function CreateAPBDesa() {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa); const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
apbDesaState.create.form = { apbDesaState.create.form = {
@@ -33,9 +37,17 @@ function CreateAPBDesa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await apbDesaState.create.submit(); try {
resetForm(); setIsSubmitting(true);
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); await apbDesaState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa');
} catch (error) {
console.error('Error creating APB Desa:', error);
toast.error('Gagal membuat APB Desa');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -62,7 +74,7 @@ function CreateAPBDesa() {
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
type="number" type="number"
defaultValue={apbDesaState.create.form.tahun} value={apbDesaState.create.form.tahun}
onChange={(val) => { onChange={(val) => {
apbDesaState.create.form.tahun = Number(val.target.value); apbDesaState.create.form.tahun = Number(val.target.value);
}} }}
@@ -94,6 +106,17 @@ function CreateAPBDesa() {
{/* Action */} {/* Action */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -104,7 +127,7 @@ function CreateAPBDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -22,12 +23,18 @@ function EditBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja); const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
value: '', value: '',
}); });
const [originalData, setOriginalData] = useState({
name: '',
value: '',
});
// format angka ke rupiah // format angka ke rupiah
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = const number =
@@ -58,6 +65,10 @@ function EditBelanja() {
name: data.name || '', name: data.name || '',
value: String(data.value || ''), value: String(data.value || ''),
}); });
setOriginalData({
name: data.name || '',
value: String(data.value || ''),
});
} }
} catch (error) { } catch (error) {
console.error("Error loading belanja:", error); console.error("Error loading belanja:", error);
@@ -70,6 +81,7 @@ function EditBelanja() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
belanjaState.update.form = { belanjaState.update.form = {
...belanjaState.update.form, ...belanjaState.update.form,
name: formData.name, name: formData.name,
@@ -82,9 +94,19 @@ function EditBelanja() {
} catch (error) { } catch (error) {
console.error("Error updating jenis belanja:", error); console.error("Error updating jenis belanja:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis belanja"); toast.error("Terjadi kesalahan saat memperbarui jenis belanja");
} finally {
setIsSubmitting(false);
} }
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
value: originalData.value,
});
toast.info("Form dikembalikan ke data awal");
};
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
@@ -135,6 +157,17 @@ function EditBelanja() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -145,7 +178,7 @@ function EditBelanja() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -14,12 +15,14 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateBelanja() { function CreateBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja); const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = const number =
@@ -47,9 +50,17 @@ function CreateBelanja() {
return toast.warn('Lengkapi semua field terlebih dahulu'); return toast.warn('Lengkapi semua field terlebih dahulu');
} }
await belanjaState.create.submit(); try {
resetForm(); setIsSubmitting(true);
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); await belanjaState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
} catch (error) {
console.error('Error creating belanja:', error);
toast.error('Gagal menambahkan jenis belanja');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -82,7 +93,7 @@ function CreateBelanja() {
<TextInput <TextInput
label={<Text fw="bold" fz="sm">Nama Jenis Belanja</Text>} label={<Text fw="bold" fz="sm">Nama Jenis Belanja</Text>}
placeholder="Masukkan nama jenis belanja" placeholder="Masukkan nama jenis belanja"
defaultValue={belanjaState.create.form.name} value={belanjaState.create.form.name}
onChange={(e) => (belanjaState.create.form.name = e.target.value)} onChange={(e) => (belanjaState.create.form.name = e.target.value)}
required required
/> />
@@ -91,7 +102,7 @@ function CreateBelanja() {
type="text" type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>} label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai belanja" placeholder="Masukkan nilai belanja"
defaultValue={formatRupiah(belanjaState.create.form.value)} value={formatRupiah(belanjaState.create.form.value)}
onChange={(e) => { onChange={(e) => {
const raw = e.currentTarget.value; const raw = e.currentTarget.value;
belanjaState.create.form.value = unformatRupiah(raw); belanjaState.create.form.value = unformatRupiah(raw);
@@ -100,6 +111,17 @@ function CreateBelanja() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -110,7 +132,7 @@ function CreateBelanja() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -21,12 +22,18 @@ function EditPembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
value: '', value: '',
}); });
const [originalData, setOriginalData] = useState({
name: '',
value: '',
});
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = const number =
typeof value === 'number' typeof value === 'number'
@@ -55,6 +62,10 @@ function EditPembiayaan() {
name: data.name || '', name: data.name || '',
value: String(data.value || ''), value: String(data.value || ''),
}); });
setOriginalData({
name: data.name || '',
value: String(data.value || ''),
});
} }
} catch (error) { } catch (error) {
console.error('Error loading pembiayaan:', error); console.error('Error loading pembiayaan:', error);
@@ -65,8 +76,17 @@ function EditPembiayaan() {
loadPembiayaan(); loadPembiayaan();
}, [params?.id]); }, [params?.id]);
const handleResetForm = () => {
setFormData({
name: originalData.name,
value: originalData.value,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
pembiayaanState.update.form = { pembiayaanState.update.form = {
...pembiayaanState.update.form, ...pembiayaanState.update.form,
name: formData.name, name: formData.name,
@@ -79,6 +99,8 @@ function EditPembiayaan() {
} catch (error) { } catch (error) {
console.error('Error updating jenis pembiayaan:', error); console.error('Error updating jenis pembiayaan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan'); toast.error('Terjadi kesalahan saat memperbarui jenis pembiayaan');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -132,6 +154,17 @@ function EditPembiayaan() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -142,7 +175,7 @@ function EditPembiayaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,6 +5,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -13,12 +14,14 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePembiayaan() { function CreatePembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const formatRupiah = (value: number | string) => { const formatRupiah = (value: number | string) => {
const number = const number =
@@ -42,13 +45,21 @@ function CreatePembiayaan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { try {
return toast.warn('Nama dan nilai wajib diisi'); setIsSubmitting(true);
} if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
return toast.warn('Nama dan nilai wajib diisi');
}
await pembiayaanState.create.submit(); await pembiayaanState.create.submit();
resetForm(); resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan'); router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan');
} catch (error) {
console.error('Error creating pembiayaan:', error);
toast.error('Gagal menambahkan jenis pembiayaan');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -81,7 +92,7 @@ function CreatePembiayaan() {
<TextInput <TextInput
label={<Text fw="bold" fz="sm">Nama Jenis Pembiayaan</Text>} label={<Text fw="bold" fz="sm">Nama Jenis Pembiayaan</Text>}
placeholder="Masukkan nama jenis pembiayaan" placeholder="Masukkan nama jenis pembiayaan"
defaultValue={pembiayaanState.create.form.name} value={pembiayaanState.create.form.name}
onChange={(e) => { onChange={(e) => {
pembiayaanState.create.form.name = e.currentTarget.value; pembiayaanState.create.form.name = e.currentTarget.value;
}} }}
@@ -92,7 +103,7 @@ function CreatePembiayaan() {
type="text" type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>} label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai" placeholder="Masukkan nilai"
defaultValue={formatRupiah(pembiayaanState.create.form.value)} value={formatRupiah(pembiayaanState.create.form.value)}
onChange={(e) => { onChange={(e) => {
const raw = e.currentTarget.value; const raw = e.currentTarget.value;
pembiayaanState.create.form.value = unformatRupiah(raw); pembiayaanState.create.form.value = unformatRupiah(raw);
@@ -101,6 +112,17 @@ function CreatePembiayaan() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -111,7 +133,7 @@ function CreatePembiayaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -21,6 +22,12 @@ function EditPendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
name: "",
value: "",
});
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
@@ -55,6 +62,10 @@ function EditPendapatan() {
name: data.name ?? '', name: data.name ?? '',
value: data.value?.toString() ?? '', value: data.value?.toString() ?? '',
}); });
setOriginalData({
name: data.name ?? '',
value: data.value?.toString() ?? '',
});
} }
} catch (error) { } catch (error) {
console.error('Error loading pendapatan:', error); console.error('Error loading pendapatan:', error);
@@ -72,8 +83,18 @@ function EditPendapatan() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
value: originalData.value,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
pendapatanState.update.form = { pendapatanState.update.form = {
...pendapatanState.update.form, ...pendapatanState.update.form,
name: formData.name, name: formData.name,
@@ -86,7 +107,10 @@ function EditPendapatan() {
} catch (error) { } catch (error) {
console.error('Error updating jenis pendapatan:', error); console.error('Error updating jenis pendapatan:', error);
toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan'); toast.error('Terjadi kesalahan saat memperbarui jenis pendapatan');
} finally {
setIsSubmitting(false);
} }
}; };
return ( return (
@@ -137,6 +161,17 @@ function EditPendapatan() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -147,7 +182,7 @@ function EditPendapatan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,6 +5,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -12,11 +13,14 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePendapatan() { function CreatePendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const formatRupiah = (value: number | string) => { 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, ''));
@@ -39,9 +43,17 @@ function CreatePendapatan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await pendapatanState.create.submit(); try {
resetForm(); setIsSubmitting(true);
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); await pendapatanState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan');
} catch (error) {
console.error('Error creating pendapatan:', error);
toast.error('Gagal menambahkan jenis pendapatan');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -72,7 +84,7 @@ function CreatePendapatan() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
defaultValue={pendapatanState.create.form.name} value={pendapatanState.create.form.name}
onChange={(val) => { onChange={(val) => {
pendapatanState.create.form.name = val.target.value; pendapatanState.create.form.name = val.target.value;
}} }}
@@ -83,7 +95,7 @@ function CreatePendapatan() {
<TextInput <TextInput
type="text" type="text"
defaultValue={formatRupiah(pendapatanState.create.form.value)} value={formatRupiah(pendapatanState.create.form.value)}
onChange={(val) => { onChange={(val) => {
const raw = val.currentTarget.value; const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw); const cleanValue = unformatRupiah(raw);
@@ -95,6 +107,17 @@ function CreatePendapatan() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -105,7 +128,7 @@ function CreatePendapatan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -28,12 +29,17 @@ export default function EditDemografiPekerjaan() {
const router = useRouter(); const router = useRouter();
const { id } = useParams() as { id: string }; const { id } = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan); const stateDemografi = useProxy(demografiPekerjaan);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({ const [formData, setFormData] = useState<FormData>({
pekerjaan: '', pekerjaan: '',
lakiLaki: 0, lakiLaki: 0,
perempuan: 0, perempuan: 0,
}); });
const [originalData, setOriginalData] = useState<FormData>({
pekerjaan: '',
lakiLaki: 0,
perempuan: 0,
});
// ✅ Load data hanya sekali di awal (tidak reset form) // ✅ Load data hanya sekali di awal (tidak reset form)
useEffect(() => { useEffect(() => {
@@ -41,6 +47,7 @@ export default function EditDemografiPekerjaan() {
const loadData = async () => { const loadData = async () => {
try { try {
setIsSubmitting(true);
stateDemografi.update.id = id; stateDemografi.update.id = id;
await stateDemografi.findUnique.load(id); await stateDemografi.findUnique.load(id);
@@ -51,10 +58,17 @@ export default function EditDemografiPekerjaan() {
lakiLaki: Number(data.lakiLaki ?? 0), lakiLaki: Number(data.lakiLaki ?? 0),
perempuan: Number(data.perempuan ?? 0), perempuan: Number(data.perempuan ?? 0),
}); });
setOriginalData({
pekerjaan: data.pekerjaan ?? '',
lakiLaki: Number(data.lakiLaki ?? 0),
perempuan: Number(data.perempuan ?? 0),
});
} }
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error);
toast.error('Gagal memuat data'); toast.error('Gagal memuat data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -75,9 +89,19 @@ export default function EditDemografiPekerjaan() {
[] []
); );
const handleResetForm = () => {
setFormData({
pekerjaan: originalData.pekerjaan,
lakiLaki: Number(originalData.lakiLaki),
perempuan: Number(originalData.perempuan),
});
toast.info("Form dikembalikan ke data awal");
};
// ✅ Submit hanya update global state sekali // ✅ Submit hanya update global state sekali
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateDemografi.update.id = id; stateDemografi.update.id = id;
stateDemografi.update.form = { ...formData }; stateDemografi.update.form = { ...formData };
@@ -88,6 +112,8 @@ export default function EditDemografiPekerjaan() {
} catch (error) { } catch (error) {
console.error('Error updating data:', error); console.error('Error updating data:', error);
toast.error('Gagal memperbarui data'); toast.error('Gagal memperbarui data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -145,6 +171,17 @@ export default function EditDemografiPekerjaan() {
/> />
<Group justify="flex-end"> <Group justify="flex-end">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -155,7 +192,7 @@ export default function EditDemografiPekerjaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -17,11 +18,13 @@ import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
import { toast } from 'react-toastify';
function CreateDemografiPekerjaan() { function CreateDemografiPekerjaan() {
const stateDemografi = useProxy(demografiPekerjaan); const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateDemografi.create.form = { stateDemografi.create.form = {
@@ -32,16 +35,23 @@ function CreateDemografiPekerjaan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateDemografi.create.create(); try {
if (id) { const id = await stateDemografi.create.create();
const idStr = String(id); if (id) {
await stateDemografi.findUnique.load(idStr); const idStr = String(id);
if (stateDemografi.findUnique.data) { await stateDemografi.findUnique.load(idStr);
setChartData([stateDemografi.findUnique.data]); if (stateDemografi.findUnique.data) {
setChartData([stateDemografi.findUnique.data]);
}
} }
resetForm();
router.push('/admin/ekonomi/demografi-pekerjaan');
} catch (error) {
console.error('Error creating demografi pekerjaan:', error);
toast.error('Terjadi kesalahan saat menambah demografi pekerjaan');
} finally {
setIsSubmitting(false);
} }
resetForm();
router.push('/admin/ekonomi/demografi-pekerjaan');
}; };
return ( return (
@@ -74,7 +84,7 @@ function CreateDemografiPekerjaan() {
<TextInput <TextInput
label="Pekerjaan" label="Pekerjaan"
type="text" type="text"
defaultValue={stateDemografi.create.form.pekerjaan} value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan" placeholder="Masukkan pekerjaan"
onChange={(val) => { onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value; stateDemografi.create.form.pekerjaan = val.currentTarget.value;
@@ -84,7 +94,7 @@ function CreateDemografiPekerjaan() {
<TextInput <TextInput
label="Jumlah Pekerja Laki-Laki" label="Jumlah Pekerja Laki-Laki"
type="number" type="number"
defaultValue={stateDemografi.create.form.lakiLaki} value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki-laki" placeholder="Masukkan jumlah pekerja laki-laki"
onChange={(val) => { onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value); stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
@@ -94,7 +104,7 @@ function CreateDemografiPekerjaan() {
<TextInput <TextInput
label="Jumlah Pekerja Perempuan" label="Jumlah Pekerja Perempuan"
type="number" type="number"
defaultValue={stateDemografi.create.form.perempuan} value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan" placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => { onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value); stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
@@ -103,6 +113,17 @@ function CreateDemografiPekerjaan() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -113,7 +134,7 @@ function CreateDemografiPekerjaan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -22,7 +23,7 @@ function EditJumlahPendudukMiskin() {
const router = useRouter(); const router = useRouter();
const params = useParams() as { id: string }; const params = useParams() as { id: string };
const stateJPM = useProxy(jumlahPendudukMiskin); const stateJPM = useProxy(jumlahPendudukMiskin);
const [isSubmitting, setIsSubmitting] = useState(false);
const id = params.id; const id = params.id;
// 🔹 State lokal untuk form // 🔹 State lokal untuk form
@@ -31,6 +32,11 @@ function EditJumlahPendudukMiskin() {
totalPoorPopulation: 0, totalPoorPopulation: 0,
}); });
const [originalData, setOriginalData] = useState({
year: 0,
totalPoorPopulation: 0,
});
// 🔹 Load data awal dari backend // 🔹 Load data awal dari backend
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
@@ -44,6 +50,10 @@ function EditJumlahPendudukMiskin() {
year: data.year || 0, year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0, totalPoorPopulation: data.totalPoorPopulation || 0,
}); });
setOriginalData({
year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0,
});
} }
} catch (error) { } catch (error) {
console.error('Gagal memuat data:', error); console.error('Gagal memuat data:', error);
@@ -62,9 +72,18 @@ function EditJumlahPendudukMiskin() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
year: originalData.year,
totalPoorPopulation: originalData.totalPoorPopulation,
});
toast.info('Form dikembalikan ke data awal');
};
// 🔹 Submit form // 🔹 Submit form
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateJPM.update.id = id; stateJPM.update.id = id;
// update global state cuma saat submit // update global state cuma saat submit
stateJPM.update.form = { ...formData }; stateJPM.update.form = { ...formData };
@@ -75,6 +94,8 @@ function EditJumlahPendudukMiskin() {
} catch (error) { } catch (error) {
console.error('Gagal menyimpan data:', error); console.error('Gagal menyimpan data:', error);
toast.error('Terjadi kesalahan saat menyimpan data'); toast.error('Terjadi kesalahan saat menyimpan data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -124,6 +145,17 @@ function EditJumlahPendudukMiskin() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -134,7 +166,7 @@ function EditJumlahPendudukMiskin() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -2,17 +2,19 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
import { toast } from 'react-toastify';
export default function CreateJumlahPendudukMiskin() { export default function CreateJumlahPendudukMiskin() {
const stateJPM = useProxy(jumlahPendudukMiskin); const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateJPM.create.form = { stateJPM.create.form = {
@@ -22,16 +24,24 @@ export default function CreateJumlahPendudukMiskin() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateJPM.create.create(); try {
if (id) { setIsSubmitting(true);
const idStr = String(id); const id = await stateJPM.create.create();
await stateJPM.findUnique.load(idStr); if (id) {
if (stateJPM.findUnique.data) { const idStr = String(id);
setChartData([stateJPM.findUnique.data]); await stateJPM.findUnique.load(idStr);
if (stateJPM.findUnique.data) {
setChartData([stateJPM.findUnique.data]);
}
} }
resetForm();
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
} catch (error) {
console.error(error)
toast.error(error instanceof Error ? error.message : "Gagal menambahkan jumlah penduduk miskin")
} finally {
setIsSubmitting(false);
} }
resetForm();
router.push('/admin/ekonomi/jumlah-penduduk-miskin');
}; };
return ( return (
@@ -59,7 +69,7 @@ export default function CreateJumlahPendudukMiskin() {
<TextInput <TextInput
label="Tahun" label="Tahun"
type="number" type="number"
defaultValue={stateJPM.create.form.year || ''} value={stateJPM.create.form.year || ''}
placeholder="Masukkan tahun" placeholder="Masukkan tahun"
onChange={(e) => { onChange={(e) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
@@ -71,7 +81,7 @@ export default function CreateJumlahPendudukMiskin() {
<TextInput <TextInput
label="Jumlah Penduduk Miskin" label="Jumlah Penduduk Miskin"
type="number" type="number"
defaultValue={stateJPM.create.form.totalPoorPopulation} value={stateJPM.create.form.totalPoorPopulation}
placeholder="Masukkan jumlah penduduk miskin" placeholder="Masukkan jumlah penduduk miskin"
onChange={(e) => { onChange={(e) => {
stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value); stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value);
@@ -80,6 +90,17 @@ export default function CreateJumlahPendudukMiskin() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -90,7 +111,7 @@ export default function CreateJumlahPendudukMiskin() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -2,10 +2,11 @@
'use client'; 'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() { function EditGrafikBerdasarkanPendidikan() {
@@ -13,6 +14,7 @@ function EditGrafikBerdasarkanPendidikan() {
const params = useParams() as { id: string }; const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id; const id = params.id;
const [isSubmitting, setIsSubmitting] = useState(false);
// state lokal untuk form // state lokal untuk form
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -23,6 +25,14 @@ function EditGrafikBerdasarkanPendidikan() {
S1: '', S1: '',
}); });
const [originalData, setOriginalData] = useState({
SD: '',
SMP: '',
SMA: '',
D3: '',
S1: '',
});
useEffect(() => { useEffect(() => {
if (id) { if (id) {
stategrafik.findUnique.load(id).then(() => { stategrafik.findUnique.load(id).then(() => {
@@ -35,26 +45,51 @@ function EditGrafikBerdasarkanPendidikan() {
D3: data.D3 || '', D3: data.D3 || '',
S1: data.S1 || '', S1: data.S1 || '',
}); });
setOriginalData({
SD: data.SD || '',
SMP: data.SMP || '',
SMA: data.SMA || '',
D3: data.D3 || '',
S1: data.S1 || '',
});
} }
}); });
} }
}, [id]); }, [id]);
const handleChange = (field: keyof typeof formData) => const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
(e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.currentTarget;
setFormData((prev) => ({ setFormData((prev) => ({ ...prev, [name]: value }));
...prev, };
[field]: e.currentTarget.value,
}));
};
const handleResetForm = () => {
setFormData({
SD: originalData.SD,
SMP: originalData.SMP,
SMA: originalData.SMA,
D3: originalData.D3,
S1: originalData.S1,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
stategrafik.update.id = id; try {
stategrafik.update.form = { ...formData }; // update global state pas submit aja setIsSubmitting(true);
await stategrafik.update.submit(); stategrafik.update.id = id;
router.push( stategrafik.update.form = { ...formData }; // update global state pas submit aja
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' await stategrafik.update.submit();
); router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
} catch (error) {
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui data grafik');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -83,42 +118,58 @@ function EditGrafikBerdasarkanPendidikan() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
name="SD"
label="SD" label="SD"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
value={formData.SD} value={formData.SD}
onChange={handleChange('SD')} onChange={handleChange}
/> />
<TextInput <TextInput
name="SMP"
label="SMP" label="SMP"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
value={formData.SMP} value={formData.SMP}
onChange={handleChange('SMP')} onChange={handleChange}
/> />
<TextInput <TextInput
name="SMA"
label="SMA" label="SMA"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
value={formData.SMA} value={formData.SMA}
onChange={handleChange('SMA')} onChange={handleChange}
/> />
<TextInput <TextInput
name="D3"
label="D3" label="D3"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
value={formData.D3} value={formData.D3}
onChange={handleChange('D3')} onChange={handleChange}
/> />
<TextInput <TextInput
name="S1"
label="S1" label="S1"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
value={formData.S1} value={formData.S1}
onChange={handleChange('S1')} onChange={handleChange}
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -129,7 +180,7 @@ function EditGrafikBerdasarkanPendidikan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -3,16 +3,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Loader, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateGrafikBerdasarkanPendidikan() { function CreateGrafikBerdasarkanPendidikan() {
const router = useRouter(); const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stategrafik.create.form = { stategrafik.create.form = {
@@ -26,18 +28,26 @@ function CreateGrafikBerdasarkanPendidikan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stategrafik.create.create(); try {
if (id) { setIsSubmitting(true);
const idStr = String(id); const id = await stategrafik.create.create();
await stategrafik.findUnique.load(idStr); if (id) {
if (stategrafik.findUnique.data) { const idStr = String(id);
setDonutData([stategrafik.findUnique.data]); await stategrafik.findUnique.load(idStr);
if (stategrafik.findUnique.data) {
setDonutData([stategrafik.findUnique.data]);
}
} }
resetForm();
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
} catch (error) {
console.error(error);
toast.error('Terjadi kesalahan saat menambahkan data grafik');
} finally {
setIsSubmitting(false);
} }
resetForm();
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
}; };
return ( return (
@@ -64,7 +74,7 @@ function CreateGrafikBerdasarkanPendidikan() {
label="SD" label="SD"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.SD} value={stategrafik.create.form.SD}
onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)}
required required
/> />
@@ -72,7 +82,7 @@ function CreateGrafikBerdasarkanPendidikan() {
label="SMP" label="SMP"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.SMP} value={stategrafik.create.form.SMP}
onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)}
required required
/> />
@@ -80,7 +90,7 @@ function CreateGrafikBerdasarkanPendidikan() {
label="SMA" label="SMA"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.SMA} value={stategrafik.create.form.SMA}
onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)}
required required
/> />
@@ -88,7 +98,7 @@ function CreateGrafikBerdasarkanPendidikan() {
label="D3" label="D3"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.D3} value={stategrafik.create.form.D3}
onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)}
required required
/> />
@@ -96,12 +106,23 @@ function CreateGrafikBerdasarkanPendidikan() {
label="S1" label="S1"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.S1} value={stategrafik.create.form.S1}
onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.value)}
required required
/> />
<Group justify="right" mt="md"> <Group justify="right" mt="md">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -112,7 +133,7 @@ function CreateGrafikBerdasarkanPendidikan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -217,20 +217,22 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<Title order={3} pb={10}> <Title order={3} pb={10}>
Grafik Pengangguran Berdasarkan Pendidikan Grafik Pengangguran Berdasarkan Pendidikan
</Title> </Title>
{donutData.length > 0 ? ( <Center>
<DonutChart {donutData.length > 0 ? (
data={donutData} <DonutChart
withLabels data={donutData}
withTooltip withLabels
tooltipDataSource="segment" withTooltip
size={260} tooltipDataSource="segment"
thickness={40} size={260}
/> thickness={40}
) : ( />
<Text color="dimmed"> ) : (
Belum ada data untuk ditampilkan dalam grafik <Text color="dimmed">
</Text> Belum ada data untuk ditampilkan dalam grafik
)} </Text>
)}
</Center>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -25,6 +26,8 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
); );
const id = params.id; const id = params.id;
const [isSubmitting, setIsSubmitting] = useState(false);
// ✅ state lokal, controlled // ✅ state lokal, controlled
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
usia18_25: '', usia18_25: '',
@@ -33,6 +36,13 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
usia46_keatas: '', usia46_keatas: '',
}); });
const [originalData, setOriginalData] = useState({
usia18_25: '',
usia26_35: '',
usia36_45: '',
usia46_keatas: '',
});
// load data dari global state -> masukin ke local state // load data dari global state -> masukin ke local state
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@@ -45,6 +55,13 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
usia36_45: data.usia36_45 || '', usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '', usia46_keatas: data.usia46_keatas || '',
}); });
setOriginalData({
usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '',
});
} }
}); });
} }
@@ -57,8 +74,19 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
usia18_25: originalData.usia18_25,
usia26_35: originalData.usia26_35,
usia36_45: originalData.usia36_45,
usia46_keatas: originalData.usia46_keatas,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// ✅ baru update global state pas submit // ✅ baru update global state pas submit
stategrafik.update.id = id; stategrafik.update.id = id;
stategrafik.update.form = { ...formData }; stategrafik.update.form = { ...formData };
@@ -72,6 +100,8 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error('Terjadi kesalahan saat memperbarui data grafik'); toast.error('Terjadi kesalahan saat memperbarui data grafik');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -134,6 +164,17 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -144,7 +185,7 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -4,16 +4,18 @@
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter(); const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur); const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stategrafik.create.form = { stategrafik.create.form = {
@@ -26,16 +28,24 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stategrafik.create.create(); try {
if (id) { setIsSubmitting(true);
const idStr = String(id); const id = await stategrafik.create.create();
await stategrafik.findUnique.load(idStr); if (id) {
if (stategrafik.findUnique.data) { const idStr = String(id);
setDonutData([stategrafik.findUnique.data]); await stategrafik.findUnique.load(idStr);
if (stategrafik.findUnique.data) {
setDonutData([stategrafik.findUnique.data]);
}
} }
resetForm();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
} catch (error) {
console.error('Error creating:', error);
toast.error('Terjadi kesalahan saat membuat data');
} finally {
setIsSubmitting(false);
} }
resetForm();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
}; };
return ( return (
@@ -64,7 +74,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 18 - 25" label="Usia 18 - 25"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.usia18_25} value={stategrafik.create.form.usia18_25}
onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)}
required required
/> />
@@ -72,7 +82,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 26 - 35" label="Usia 26 - 35"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.usia26_35} value={stategrafik.create.form.usia26_35}
onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)}
required required
/> />
@@ -80,7 +90,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 36 - 45" label="Usia 36 - 45"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.usia36_45} value={stategrafik.create.form.usia36_45}
onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)}
required required
/> />
@@ -88,13 +98,24 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
label="Usia 46 +" label="Usia 46 +"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stategrafik.create.form.usia46_keatas} value={stategrafik.create.form.usia46_keatas}
onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)} onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)}
required required
/> />
{/* Submit Button */} {/* Submit Button */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -105,7 +126,7 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -31,6 +32,7 @@ function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
// --- state lokal form // --- state lokal form
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -42,6 +44,15 @@ function EditDetailDataPengangguran() {
percentageChange: 0, percentageChange: 0,
}); });
const [originalData, setOriginalData] = useState({
month: '',
year: new Date().getFullYear(),
educatedUnemployment: 0,
uneducatedUnemployment: 0,
totalUnemployment: 0,
percentageChange: 0,
});
// --- hitung total + persentase perubahan // --- hitung total + persentase perubahan
const calculateTotalAndChange = useCallback( const calculateTotalAndChange = useCallback(
async (data: typeof formData) => { async (data: typeof formData) => {
@@ -109,6 +120,15 @@ function EditDetailDataPengangguran() {
totalUnemployment: data.totalUnemployment, totalUnemployment: data.totalUnemployment,
percentageChange: data.percentageChange || 0, percentageChange: data.percentageChange || 0,
}); });
setOriginalData({
month: data.month,
year: yearValue,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
totalUnemployment: data.totalUnemployment,
percentageChange: data.percentageChange || 0,
});
} catch (err) { } catch (err) {
console.error('Error loading detail:', err); console.error('Error loading detail:', err);
toast.error('Gagal memuat data detail'); toast.error('Gagal memuat data detail');
@@ -118,9 +138,22 @@ function EditDetailDataPengangguran() {
loadDetail(); loadDetail();
}, [params?.id]); }, [params?.id]);
const handleResetForm = () => {
setFormData({
month: originalData.month,
year: originalData.year,
educatedUnemployment: originalData.educatedUnemployment,
uneducatedUnemployment: originalData.uneducatedUnemployment,
totalUnemployment: originalData.totalUnemployment,
percentageChange: originalData.percentageChange,
});
toast.info("Form dikembalikan ke data awal");
};
// --- submit form // --- submit form
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
const { total, percentageChange } = await calculateTotalAndChange(formData); const { total, percentageChange } = await calculateTotalAndChange(formData);
stateDetail.update.form = { stateDetail.update.form = {
@@ -137,6 +170,8 @@ function EditDetailDataPengangguran() {
} catch (err) { } catch (err) {
console.error('Error updating:', err); console.error('Error updating:', err);
toast.error('Terjadi kesalahan saat memperbarui data'); toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -205,6 +240,17 @@ function EditDetailDataPengangguran() {
</Text> </Text>
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -215,7 +261,7 @@ function EditDetailDataPengangguran() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
NumberInput, NumberInput,
Paper, Paper,
Select, Select,
@@ -17,12 +18,14 @@ import {
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() { function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran); const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const monthOptions = [ const monthOptions = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
@@ -72,15 +75,23 @@ function CreateJumlahPengangguran() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await calculateTotalAndChange(); try {
const id = await stateDetail.create.create(); setIsSubmitting(true);
if (id) { await calculateTotalAndChange();
await stateDetail.findUnique.load(String(id)); const id = await stateDetail.create.create();
if (stateDetail.findUnique.data) { if (id) {
setChartData([stateDetail.findUnique.data]); await stateDetail.findUnique.load(String(id));
if (stateDetail.findUnique.data) {
setChartData([stateDetail.findUnique.data]);
}
resetForm();
router.push('/admin/ekonomi/jumlah-pengangguran');
} }
resetForm(); } catch (error) {
router.push('/admin/ekonomi/jumlah-pengangguran'); console.error("Error creating jumlah pengangguran:", error);
toast.error("Gagal menambahkan data pengangguran");
} finally {
setIsSubmitting(false);
} }
}; };
@@ -176,7 +187,19 @@ function CreateJumlahPengangguran() {
</Box> </Box>
{/* Action Button */} {/* Action Button */}
<Group justify="right" mt="md"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -186,9 +209,8 @@ function CreateJumlahPengangguran() {
color: '#fff', color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
disabled={!stateDetail.create.form.month || !stateDetail.create.form.year}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -23,6 +24,7 @@ function EditLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState); const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
posisi: '', posisi: '',
@@ -35,6 +37,17 @@ function EditLowonganKerja() {
notelp: '', notelp: '',
}); });
const [originalData, setOriginalData] = useState({
posisi: '',
namaPerusahaan: '',
lokasi: '',
tipePekerjaan: '',
gaji: '',
deskripsi: '',
kualifikasi: '',
notelp: '',
})
// load data sekali aja ketika mount / id berubah // load data sekali aja ketika mount / id berubah
useEffect(() => { useEffect(() => {
const loadLowongan = async () => { const loadLowongan = async () => {
@@ -54,6 +67,16 @@ function EditLowonganKerja() {
kualifikasi: data.kualifikasi || '', kualifikasi: data.kualifikasi || '',
notelp: data.notelp || '', notelp: data.notelp || '',
}); });
setOriginalData({
posisi: data.posisi || '',
namaPerusahaan: data.namaPerusahaan || '',
lokasi: data.lokasi || '',
tipePekerjaan: data.tipePekerjaan || '',
gaji: data.gaji || '',
deskripsi: data.deskripsi || '',
kualifikasi: data.kualifikasi || '',
notelp: data.notelp || '',
});
} }
} catch (error) { } catch (error) {
console.error("Error loading lowongan kerja:", error); console.error("Error loading lowongan kerja:", error);
@@ -70,9 +93,23 @@ function EditLowonganKerja() {
[field]: value, [field]: value,
})); }));
}; };
const handleResetForm = () => {
setFormData({
posisi: originalData.posisi,
namaPerusahaan: originalData.namaPerusahaan,
lokasi: originalData.lokasi,
tipePekerjaan: originalData.tipePekerjaan,
gaji: originalData.gaji,
deskripsi: originalData.deskripsi,
kualifikasi: originalData.kualifikasi,
notelp: originalData.notelp,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
lowonganState.update.id = params?.id as string; lowonganState.update.id = params?.id as string;
lowonganState.update.form = { ...formData }; lowonganState.update.form = { ...formData };
@@ -82,6 +119,8 @@ function EditLowonganKerja() {
} catch (error) { } catch (error) {
console.error("Error updating lowongan kerja:", error); console.error("Error updating lowongan kerja:", error);
toast.error("Terjadi kesalahan saat memperbarui lowongan kerja"); toast.error("Terjadi kesalahan saat memperbarui lowongan kerja");
} finally {
setIsSubmitting(false);
} }
}; };
@@ -175,6 +214,17 @@ function EditLowonganKerja() {
</Box> </Box>
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -185,7 +235,7 @@ function EditLowonganKerja() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -4,6 +4,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -15,10 +16,13 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja'; import lowonganKerjaState from '../../../_state/ekonomi/lowongan-kerja';
import { useState } from 'react';
import { toast } from 'react-toastify';
function CreateLowonganKerja() { function CreateLowonganKerja() {
const lowonganState = useProxy(lowonganKerjaState); const lowonganState = useProxy(lowonganKerjaState);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
lowonganState.create.form = { lowonganState.create.form = {
@@ -34,9 +38,19 @@ function CreateLowonganKerja() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
await lowonganState.create.create(); try {
resetForm(); setIsSubmitting(true);
router.push('/admin/ekonomi/lowongan-kerja-lokal'); await lowonganState.create.create();
resetForm();
router.push('/admin/ekonomi/lowongan-kerja-lokal');
} catch (error) {
console.error('Error creating lowongan kerja:', error);
toast.error(
error instanceof Error ? error.message : 'Gagal membuat lowongan kerja'
);
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -66,7 +80,7 @@ function CreateLowonganKerja() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
defaultValue={lowonganState.create.form.posisi} value={lowonganState.create.form.posisi}
onChange={(val) => onChange={(val) =>
(lowonganState.create.form.posisi = val.target.value) (lowonganState.create.form.posisi = val.target.value)
} }
@@ -75,7 +89,7 @@ function CreateLowonganKerja() {
required required
/> />
<TextInput <TextInput
defaultValue={lowonganState.create.form.namaPerusahaan} value={lowonganState.create.form.namaPerusahaan}
onChange={(val) => onChange={(val) =>
(lowonganState.create.form.namaPerusahaan = val.target.value) (lowonganState.create.form.namaPerusahaan = val.target.value)
} }
@@ -84,7 +98,7 @@ function CreateLowonganKerja() {
required required
/> />
<TextInput <TextInput
defaultValue={lowonganState.create.form.notelp} value={lowonganState.create.form.notelp}
onChange={(val) => onChange={(val) =>
(lowonganState.create.form.notelp = val.target.value) (lowonganState.create.form.notelp = val.target.value)
} }
@@ -93,7 +107,7 @@ function CreateLowonganKerja() {
required required
/> />
<TextInput <TextInput
defaultValue={lowonganState.create.form.lokasi} value={lowonganState.create.form.lokasi}
onChange={(val) => onChange={(val) =>
(lowonganState.create.form.lokasi = val.target.value) (lowonganState.create.form.lokasi = val.target.value)
} }
@@ -102,7 +116,7 @@ function CreateLowonganKerja() {
required required
/> />
<TextInput <TextInput
defaultValue={lowonganState.create.form.tipePekerjaan} value={lowonganState.create.form.tipePekerjaan}
onChange={(val) => onChange={(val) =>
(lowonganState.create.form.tipePekerjaan = val.target.value) (lowonganState.create.form.tipePekerjaan = val.target.value)
} }
@@ -111,7 +125,7 @@ function CreateLowonganKerja() {
required required
/> />
<TextInput <TextInput
defaultValue={lowonganState.create.form.gaji} value={lowonganState.create.form.gaji}
onChange={(val) => onChange={(val) =>
(lowonganState.create.form.gaji = val.target.value) (lowonganState.create.form.gaji = val.target.value)
} }
@@ -146,6 +160,17 @@ function CreateLowonganKerja() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -156,7 +181,7 @@ function CreateLowonganKerja() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,6 +5,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -23,9 +24,9 @@ function EditKategoriProduk() {
const params = useParams(); const params = useParams();
const id = params?.id as string; const id = params?.id as string;
const statePasar = useProxy(pasarDesaState.kategoriProduk); const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ nama: '' }); const [formData, setFormData] = useState({ nama: '' });
const [loading, setLoading] = useState(true); const [originalData, setOriginalData] = useState({ nama: '' });
useEffect(() => { useEffect(() => {
const loadKategoriProduk = async () => { const loadKategoriProduk = async () => {
@@ -40,12 +41,11 @@ function EditKategoriProduk() {
// simpan data ke state lokal // simpan data ke state lokal
setFormData({ nama: data.nama || '' }); setFormData({ nama: data.nama || '' });
setOriginalData({ nama: data.nama || '' });
} }
} catch (error) { } catch (error) {
console.error('Error loading kategori produk:', error); console.error('Error loading kategori produk:', error);
toast.error('Gagal memuat data kategori produk'); toast.error('Gagal memuat data kategori produk');
} finally {
setLoading(false);
} }
}; };
@@ -59,8 +59,16 @@ function EditKategoriProduk() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
if (!formData.nama.trim()) { if (!formData.nama.trim()) {
toast.error('Nama kategori produk tidak boleh kosong'); toast.error('Nama kategori produk tidak boleh kosong');
return; return;
@@ -81,13 +89,11 @@ function EditKategoriProduk() {
} catch (error) { } catch (error) {
console.error('Error updating kategori produk:', error); console.error('Error updating kategori produk:', error);
toast.error('Terjadi kesalahan saat memperbarui kategori produk'); toast.error('Terjadi kesalahan saat memperbarui kategori produk');
} finally {
setIsSubmitting(false);
} }
}; };
if (loading) {
return <Text>Loading...</Text>;
}
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */} {/* Header dengan tombol back */}
@@ -125,6 +131,17 @@ function EditKategoriProduk() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -135,7 +152,7 @@ function EditKategoriProduk() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,6 +5,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
@@ -12,7 +13,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa'; import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
@@ -20,6 +21,7 @@ import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
function CreateKategoriProduk() { function CreateKategoriProduk() {
const router = useRouter(); const router = useRouter();
const statePasar = useProxy(pasarDesaState.kategoriProduk); const statePasar = useProxy(pasarDesaState.kategoriProduk);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => { useEffect(() => {
statePasar.findMany.load(); statePasar.findMany.load();
@@ -32,27 +34,34 @@ function CreateKategoriProduk() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!statePasar.create.form.nama) { try {
return toast.warn('Nama kategori produk wajib diisi'); if (!statePasar.create.form.nama) {
return toast.warn('Nama kategori produk wajib diisi');
}
setIsSubmitting(true);
await statePasar.create.create();
resetForm();
router.push('/admin/ekonomi/pasar-desa/kategori-produk');
} catch (error) {
console.error(error)
toast.error('Gagal menambahkan kategori produk');
} finally {
setIsSubmitting(false);
} }
await statePasar.create.create();
resetForm();
router.push('/admin/ekonomi/pasar-desa/kategori-produk');
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */} {/* Header dengan tombol kembali */}
<Group mb="md"> <Group mb="md">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
p="xs" p="xs"
radius="md" radius="md"
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Kategori Produk Tambah Kategori Produk
</Title> </Title>
@@ -71,12 +80,23 @@ function CreateKategoriProduk() {
<TextInput <TextInput
label="Nama Kategori Produk" label="Nama Kategori Produk"
placeholder="Masukkan nama kategori produk" placeholder="Masukkan nama kategori produk"
defaultValue={statePasar.create.form.nama || ''} value={statePasar.create.form.nama || ''}
onChange={(e) => (statePasar.create.form.nama = e.target.value)} onChange={(e) => (statePasar.create.form.nama = e.target.value)}
required required
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -87,7 +107,7 @@ function CreateKategoriProduk() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,10 +5,12 @@ import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pa
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
MultiSelect, MultiSelect,
Paper, Paper,
Stack, Stack,
@@ -40,6 +42,7 @@ function EditPasarDesa() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({ const [formData, setFormData] = useState<FormData>({
nama: '', nama: '',
harga: 0, harga: 0,
@@ -50,6 +53,17 @@ function EditPasarDesa() {
kontak: '', kontak: '',
}); });
const [originalData, setOriginalData] = useState({
nama: '',
harga: 0,
alamatUsaha: '',
imageId: '',
imageUrl: "",
rating: 0,
kategoriId: [],
kontak: '',
});
// load data awal // load data awal
useEffect(() => { useEffect(() => {
pasarState.kategoriProduk.findManyAll.load(); pasarState.kategoriProduk.findManyAll.load();
@@ -70,6 +84,16 @@ function EditPasarDesa() {
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [], kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: data.kontak || '', kontak: data.kontak || '',
}); });
setOriginalData({
nama: data.nama || '',
harga: data.harga || 0,
alamatUsaha: data.alamatUsaha || '',
imageId: data.imageId || '',
imageUrl: data.image?.link || "",
rating: data.rating || 0,
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: data.kontak || '',
});
if (data.image?.link) setPreviewImage(data.image.link); if (data.image?.link) setPreviewImage(data.image.link);
} }
} catch (error) { } catch (error) {
@@ -87,8 +111,25 @@ function EditPasarDesa() {
setFormData((prev) => ({ ...prev, [key]: value })); setFormData((prev) => ({ ...prev, [key]: value }));
}; };
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
harga: originalData.harga,
alamatUsaha: originalData.alamatUsaha,
imageId: originalData.imageId,
rating: originalData.rating,
kategoriId: (originalData as any)?.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: originalData.kontak,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
// upload image kalau ada file baru // upload image kalau ada file baru
let imageId = formData.imageId; let imageId = formData.imageId;
if (file) { if (file) {
@@ -110,6 +151,8 @@ function EditPasarDesa() {
} catch (error) { } catch (error) {
console.error('Error updating pasar desa:', error); console.error('Error updating pasar desa:', error);
toast.error('Terjadi kesalahan saat memperbarui pasar desa'); toast.error('Terjadi kesalahan saat memperbarui pasar desa');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -148,7 +191,7 @@ function EditPasarDesa() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -167,25 +210,45 @@ function EditPasarDesa() {
Seret gambar atau klik untuk memilih file Seret gambar atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <Box pos={"relative"} mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ style={{
maxHeight: 220, maxHeight: 200,
objectFit: 'contain', objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`, border: '1px solid #ddd',
}} }}
loading="lazy" loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -254,6 +317,17 @@ function EditPasarDesa() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -264,7 +338,7 @@ function EditPasarDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -3,10 +3,12 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
MultiSelect, MultiSelect,
Paper, Paper,
Stack, Stack,
@@ -27,6 +29,7 @@ export default function CreatePasarDesa() {
const statePasar = useProxy(pasarDesaState); const statePasar = useProxy(pasarDesaState);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => { useEffect(() => {
statePasar.kategoriProduk.findManyAll.load(); statePasar.kategoriProduk.findManyAll.load();
@@ -47,25 +50,33 @@ export default function CreatePasarDesa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { try {
return toast.warn('Silakan pilih file gambar terlebih dahulu'); setIsSubmitting(true);
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
statePasar.pasarDesa.create.form.imageId = uploaded.id;
await statePasar.pasarDesa.create.create();
resetForm();
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
} catch (error) {
console.error('Error creating kategori produk:', error);
toast.error('Gagal membuat kategori produk');
} finally {
setIsSubmitting(false);
} }
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
statePasar.pasarDesa.create.form.imageId = uploaded.id;
await statePasar.pasarDesa.create.create();
resetForm();
router.push('/admin/ekonomi/pasar-desa/produk-pasar-desa');
}; };
return ( return (
@@ -105,7 +116,7 @@ export default function CreatePasarDesa() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -126,7 +137,7 @@ export default function CreatePasarDesa() {
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}> <Box pos={"relative"} mt="sm" style={{ textAlign: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
@@ -134,6 +145,24 @@ export default function CreatePasarDesa() {
style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }} style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
loading="lazy" loading="lazy"
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -142,7 +171,7 @@ export default function CreatePasarDesa() {
<TextInput <TextInput
label="Nama Produk" label="Nama Produk"
placeholder="Masukkan nama produk" placeholder="Masukkan nama produk"
defaultValue={statePasar.pasarDesa.create.form.nama} value={statePasar.pasarDesa.create.form.nama}
onChange={(e) => (statePasar.pasarDesa.create.form.nama = e.target.value)} onChange={(e) => (statePasar.pasarDesa.create.form.nama = e.target.value)}
required required
/> />
@@ -152,7 +181,7 @@ export default function CreatePasarDesa() {
type="number" type="number"
label="Harga Produk" label="Harga Produk"
placeholder="Masukkan harga produk" placeholder="Masukkan harga produk"
defaultValue={statePasar.pasarDesa.create.form.harga} value={statePasar.pasarDesa.create.form.harga}
onChange={(e) => (statePasar.pasarDesa.create.form.harga = Number(e.target.value))} onChange={(e) => (statePasar.pasarDesa.create.form.harga = Number(e.target.value))}
required required
/> />
@@ -165,7 +194,7 @@ export default function CreatePasarDesa() {
step={0.1} step={0.1}
label="Rating Produk (05)" label="Rating Produk (05)"
placeholder="Masukkan rating produk" placeholder="Masukkan rating produk"
defaultValue={statePasar.pasarDesa.create.form.rating} value={statePasar.pasarDesa.create.form.rating}
onChange={(e) => { onChange={(e) => {
const value = Number(e.target.value); const value = Number(e.target.value);
if (value >= 0 && value <= 5) { if (value >= 0 && value <= 5) {
@@ -178,7 +207,7 @@ export default function CreatePasarDesa() {
<TextInput <TextInput
label="Alamat Usaha" label="Alamat Usaha"
placeholder="Masukkan alamat usaha" placeholder="Masukkan alamat usaha"
defaultValue={statePasar.pasarDesa.create.form.alamatUsaha} value={statePasar.pasarDesa.create.form.alamatUsaha}
onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)} onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
/> />
@@ -187,7 +216,7 @@ export default function CreatePasarDesa() {
label="Kontak" label="Kontak"
type="number" type="number"
placeholder="Masukkan kontak" placeholder="Masukkan kontak"
defaultValue={statePasar.pasarDesa.create.form.kontak} value={statePasar.pasarDesa.create.form.kontak}
onChange={(e) => (statePasar.pasarDesa.create.form.kontak = e.target.value)} onChange={(e) => (statePasar.pasarDesa.create.form.kontak = e.target.value)}
/> />
@@ -207,6 +236,17 @@ export default function CreatePasarDesa() {
{/* Tombol Submit */} {/* Tombol Submit */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -217,7 +257,7 @@ export default function CreatePasarDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -9,6 +9,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -49,6 +50,8 @@ function EditProgramKemiskinan() {
const stateProgram = useProxy(programKemiskinanState); const stateProgram = useProxy(programKemiskinanState);
const [formData, setFormData] = useState<FormData>(initialForm); const [formData, setFormData] = useState<FormData>(initialForm);
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState<FormData>(initialForm);
// Load data 1x dari global state → isi local state // Load data 1x dari global state → isi local state
useEffect(() => { useEffect(() => {
@@ -68,6 +71,15 @@ function EditProgramKemiskinan() {
jumlah: data.statistik?.jumlah?.toString() ?? '', jumlah: data.statistik?.jumlah?.toString() ?? '',
}, },
}); });
setOriginalData({
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
icon: data.icon ?? '',
statistik: {
tahun: data.statistik?.tahun?.toString() ?? '',
jumlah: data.statistik?.jumlah?.toString() ?? '',
},
});
} }
} catch (err) { } catch (err) {
console.error('Error load data:', err); console.error('Error load data:', err);
@@ -99,8 +111,22 @@ function EditProgramKemiskinan() {
[] []
); );
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
deskripsi: originalData.deskripsi,
icon: originalData.icon,
statistik: {
tahun: originalData.statistik.tahun,
jumlah: originalData.statistik.jumlah,
},
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateProgram.update.id = id; stateProgram.update.id = id;
stateProgram.update.form = formData; stateProgram.update.form = formData;
await stateProgram.update.update(); await stateProgram.update.update();
@@ -110,6 +136,8 @@ function EditProgramKemiskinan() {
} catch (error) { } catch (error) {
console.error('Error update program:', error); console.error('Error update program:', error);
toast.error('Terjadi kesalahan saat memperbarui program'); toast.error('Terjadi kesalahan saat memperbarui program');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -192,6 +220,17 @@ function EditProgramKemiskinan() {
</Box> </Box>
<Group justify="right" mt="md"> <Group justify="right" mt="md">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -202,7 +241,7 @@ function EditProgramKemiskinan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -27,6 +28,7 @@ function CreateProgramKemiskinan() {
const router = useRouter(); const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
programState.create.form = { programState.create.form = {
nama: '', nama: '',
@@ -40,24 +42,32 @@ function CreateProgramKemiskinan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!programState.create.form.nama || !programState.create.form.deskripsi) { try {
return toast.warn('Judul dan deskripsi wajib diisi'); setIsSubmitting(true);
} if (!programState.create.form.nama || !programState.create.form.deskripsi) {
return toast.warn('Judul dan deskripsi wajib diisi');
const id = await programState.create.create();
if (id) {
const idStr = String(id);
await programState.findUnique.load(idStr);
if (programState.findUnique.data) {
setLineChart([programState.findUnique.data]);
} }
toast.success('Program berhasil ditambahkan');
} else {
toast.error('Gagal menambahkan program, coba lagi');
}
resetForm(); const id = await programState.create.create();
router.push('/admin/ekonomi/program-kemiskinan'); if (id) {
const idStr = String(id);
await programState.findUnique.load(idStr);
if (programState.findUnique.data) {
setLineChart([programState.findUnique.data]);
}
toast.success('Program berhasil ditambahkan');
} else {
toast.error('Gagal menambahkan program, coba lagi');
}
resetForm();
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {
console.error('Gagal menyimpan data:', error);
toast.error('Terjadi kesalahan saat menyimpan data');
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -90,7 +100,7 @@ function CreateProgramKemiskinan() {
<TextInput <TextInput
label="Judul Program" label="Judul Program"
placeholder="Masukkan judul program" placeholder="Masukkan judul program"
defaultValue={programState.create.form.nama} value={programState.create.form.nama}
onChange={(val) => (programState.create.form.nama = val.target.value)} onChange={(val) => (programState.create.form.nama = val.target.value)}
required required
/> />
@@ -125,7 +135,7 @@ function CreateProgramKemiskinan() {
<Group grow> <Group grow>
<TextInput <TextInput
type="number" type="number"
defaultValue={programState.create.form.statistik.jumlah} value={programState.create.form.statistik.jumlah}
onChange={(val) => onChange={(val) =>
(programState.create.form.statistik.jumlah = val.target.value) (programState.create.form.statistik.jumlah = val.target.value)
} }
@@ -135,7 +145,7 @@ function CreateProgramKemiskinan() {
/> />
<TextInput <TextInput
type="number" type="number"
defaultValue={programState.create.form.statistik.tahun} value={programState.create.form.statistik.tahun}
onChange={(val) => onChange={(val) =>
(programState.create.form.statistik.tahun = val.target.value) (programState.create.form.statistik.tahun = val.target.value)
} }
@@ -147,6 +157,17 @@ function CreateProgramKemiskinan() {
{/* Tombol Submit */} {/* Tombol Submit */}
<Group justify="right" mt="sm"> <Group justify="right" mt="sm">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -157,7 +178,7 @@ function CreateProgramKemiskinan() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -8,6 +8,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -24,7 +25,7 @@ function EditSektorUnggulanDesa() {
const router = useRouter(); const router = useRouter();
const params = useParams() as { id: string }; const params = useParams() as { id: string };
const stateGrafik = useProxy(grafikSektorUnggulan); const stateGrafik = useProxy(grafikSektorUnggulan);
const [isSubmitting, setIsSubmitting] = useState(false);
const id = params.id; const id = params.id;
// state lokal buat form // state lokal buat form
@@ -34,6 +35,12 @@ function EditSektorUnggulanDesa() {
value: 0, value: 0,
}); });
const [originalData, setOriginalData] = useState({
name: '',
description: '',
value: 0,
});
// Load data saat komponen mount // Load data saat komponen mount
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@@ -47,6 +54,11 @@ function EditSektorUnggulanDesa() {
description: data.description || '', description: data.description || '',
value: data.value || 0, value: data.value || 0,
}); });
setOriginalData({
name: data.name || '',
description: data.description || '',
value: data.value || 0,
});
} }
}) })
.catch((err) => { .catch((err) => {
@@ -65,6 +77,7 @@ function EditSektorUnggulanDesa() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateGrafik.update.id = id; stateGrafik.update.id = id;
stateGrafik.update.form = { ...formData }; // update global pas submit stateGrafik.update.form = { ...formData }; // update global pas submit
await stateGrafik.update.submit(); await stateGrafik.update.submit();
@@ -73,9 +86,20 @@ function EditSektorUnggulanDesa() {
} catch (error) { } catch (error) {
console.error('Error update sektor unggulan:', error); console.error('Error update sektor unggulan:', error);
toast.error('Terjadi kesalahan saat memperbarui sektor unggulan'); toast.error('Terjadi kesalahan saat memperbarui sektor unggulan');
} finally {
setIsSubmitting(false);
} }
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
description: originalData.description,
value: originalData.value,
});
toast.info('Form dikembalikan ke data awal');
};
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
@@ -129,6 +153,17 @@ function EditSektorUnggulanDesa() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -139,7 +174,7 @@ function EditSektorUnggulanDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -18,11 +19,13 @@ import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
import { toast } from 'react-toastify';
function CreateSektorUnggulanDesa() { function CreateSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan); const stateGrafik = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateGrafik.create.form = { stateGrafik.create.form = {
@@ -33,16 +36,24 @@ function CreateSektorUnggulanDesa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateGrafik.create.create(); try {
if (id) { setIsSubmitting(true);
const idStr = String(id); const id = await stateGrafik.create.create();
await stateGrafik.findUnique.load(idStr); if (id) {
if (stateGrafik.findUnique.data) { const idStr = String(id);
setChartData([stateGrafik.findUnique.data]); await stateGrafik.findUnique.load(idStr);
if (stateGrafik.findUnique.data) {
setChartData([stateGrafik.findUnique.data]);
}
} }
resetForm();
router.push('/admin/ekonomi/sektor-unggulan-desa');
} catch (error) {
console.error('Error creating sektor unggulan:', error);
toast.error('Terjadi kesalahan saat menambahkan sektor unggulan');
} finally {
setIsSubmitting(false);
} }
resetForm();
router.push('/admin/ekonomi/sektor-unggulan-desa');
}; };
return ( return (
@@ -75,7 +86,7 @@ function CreateSektorUnggulanDesa() {
<TextInput <TextInput
label="Nama Sektor Unggulan" label="Nama Sektor Unggulan"
placeholder="Masukkan nama sektor unggulan" placeholder="Masukkan nama sektor unggulan"
defaultValue={stateGrafik.create.form.name} value={stateGrafik.create.form.name}
onChange={(e) => { onChange={(e) => {
stateGrafik.create.form.name = e.currentTarget.value; stateGrafik.create.form.name = e.currentTarget.value;
}} }}
@@ -98,7 +109,7 @@ function CreateSektorUnggulanDesa() {
label="Jumlah" label="Jumlah"
type="number" type="number"
placeholder="Masukkan jumlah" placeholder="Masukkan jumlah"
defaultValue={stateGrafik.create.form.value} value={stateGrafik.create.form.value}
onChange={(e) => { onChange={(e) => {
stateGrafik.create.form.value = Number(e.currentTarget.value); stateGrafik.create.form.value = Number(e.currentTarget.value);
}} }}
@@ -106,6 +117,17 @@ function CreateSektorUnggulanDesa() {
/> />
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -116,7 +138,7 @@ function CreateSektorUnggulanDesa() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,10 +5,12 @@ import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
Paper, Paper,
Select, Select,
Stack, Stack,
@@ -27,7 +29,7 @@ export default function EditPegawaiBumDes() {
const router = useRouter(); const router = useRouter();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai); const stateOrganisasi = useProxy(stateStrukturBumDes.pegawai);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
namaLengkap: '', namaLengkap: '',
gelarAkademik: '', gelarAkademik: '',
@@ -39,6 +41,18 @@ export default function EditPegawaiBumDes() {
posisiId: '', posisiId: '',
isActive: true, isActive: true,
}); });
const [originalData, setOriginalData] = useState({
namaLengkap: '',
gelarAkademik: '',
imageId: '',
tanggalMasuk: '',
email: '',
telepon: '',
alamat: '',
posisiId: '',
isActive: true,
imageUrl: ''
});
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -67,6 +81,18 @@ export default function EditPegawaiBumDes() {
posisiId: data.posisiId || '', posisiId: data.posisiId || '',
isActive: data.isActive ?? true, isActive: data.isActive ?? true,
}); });
setOriginalData({
namaLengkap: data.namaLengkap || '',
gelarAkademik: data.gelarAkademik || '',
imageId: data.imageId || '',
tanggalMasuk: data.tanggalMasuk || '',
email: data.email || '',
telepon: data.telepon || '',
alamat: data.alamat || '',
posisiId: data.posisiId || '',
isActive: data.isActive ?? true,
imageUrl: data.image?.link || '',
});
setPreviewImage(data.image?.link || null); setPreviewImage(data.image?.link || null);
} }
@@ -79,8 +105,26 @@ export default function EditPegawaiBumDes() {
loadPegawai(); loadPegawai();
}, [id]); }, [id]);
const handleResetForm = () => {
setFormData({
namaLengkap: originalData.namaLengkap,
gelarAkademik: originalData.gelarAkademik,
imageId: originalData.imageId,
tanggalMasuk: originalData.tanggalMasuk,
email: originalData.email,
telepon: originalData.telepon,
alamat: originalData.alamat,
posisiId: originalData.posisiId,
isActive: originalData.isActive,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
if (!formData.namaLengkap.trim()) { if (!formData.namaLengkap.trim()) {
return toast.error('Nama lengkap tidak boleh kosong'); return toast.error('Nama lengkap tidak boleh kosong');
} }
@@ -103,6 +147,8 @@ export default function EditPegawaiBumDes() {
} catch (error) { } catch (error) {
console.error('Error updating pegawai:', error); console.error('Error updating pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai'); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -164,13 +210,13 @@ export default function EditPegawaiBumDes() {
<Dropzone.Idle><IconPhoto size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle> <Dropzone.Idle><IconPhoto size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle>
<Stack gap="xs" align="center"> <Stack gap="xs" align="center">
<Text size="md" fw={500}>Seret gambar atau klik untuk memilih file</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> <Text size="sm" c="dimmed">Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp</Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <Box pos={"relative"} mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
@@ -178,6 +224,24 @@ export default function EditPegawaiBumDes() {
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
loading="lazy" loading="lazy"
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -244,10 +308,21 @@ export default function EditPegawaiBumDes() {
</Box> </Box>
{/* Submit Button */} {/* Submit Button */}
<Group justify="flex-end" mt="md"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md" radius="md"
size="md" size="md"
style={{ style={{
@@ -256,7 +331,7 @@ export default function EditPegawaiBumDes() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -4,7 +4,7 @@
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core'; import { ActionIcon, Box, Button, Group, Image, Loader, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -20,6 +20,8 @@ function CreatePegawaiBumDes() {
stateStrukturBumDes.posisiOrganisasi.findManyAll.load(); stateStrukturBumDes.posisiOrganisasi.findManyAll.load();
resetForm(); resetForm();
}, []); }, []);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateOrganisasi.create.form = { stateOrganisasi.create.form = {
@@ -33,6 +35,8 @@ function CreatePegawaiBumDes() {
posisiId: "", posisiId: "",
isActive: true, isActive: true,
}; };
setPreviewImage(null);
setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -41,15 +45,18 @@ function CreatePegawaiBumDes() {
} }
try { try {
setIsSubmitting(true);
// Upload gambar dulu // Upload gambar dulu
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file, file,
name: previewImage.file.name, name: file.name,
}); });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
// Set status aktif secara otomatis // Set status aktif secara otomatis
@@ -69,6 +76,8 @@ function CreatePegawaiBumDes() {
} catch (error) { } catch (error) {
console.error("Error creating pegawai:", error); console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai"); toast.error("Terjadi kesalahan saat menambahkan pegawai");
} finally {
setIsSubmitting(false);
} }
}; };
@@ -96,7 +105,7 @@ function CreatePegawaiBumDes() {
<TextInput <TextInput
label="Nama Lengkap" label="Nama Lengkap"
placeholder="Masukkan nama lengkap" placeholder="Masukkan nama lengkap"
defaultValue={stateOrganisasi.create.form.namaLengkap} value={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
required required
/> />
@@ -105,7 +114,7 @@ function CreatePegawaiBumDes() {
<TextInput <TextInput
label="Gelar Akademik" label="Gelar Akademik"
placeholder="Contoh: S.Kom" placeholder="Contoh: S.Kom"
defaultValue={stateOrganisasi.create.form.gelarAkademik} value={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/> />
</Box> </Box>
@@ -163,7 +172,7 @@ function CreatePegawaiBumDes() {
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="md"> <Box mt="md" pos={"relative"}>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Preview Gambar Preview Gambar
</Text> </Text>
@@ -180,6 +189,24 @@ function CreatePegawaiBumDes() {
}} }}
loading='lazy' loading='lazy'
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -188,7 +215,7 @@ function CreatePegawaiBumDes() {
label="Tanggal Masuk" label="Tanggal Masuk"
type="date" type="date"
placeholder="Contoh: 2022-01-01" placeholder="Contoh: 2022-01-01"
defaultValue={stateOrganisasi.create.form.tanggalMasuk} value={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/> />
</Box> </Box>
@@ -198,7 +225,7 @@ function CreatePegawaiBumDes() {
label="Email" label="Email"
type="email" type="email"
placeholder="Contoh: email@example.com" placeholder="Contoh: email@example.com"
defaultValue={stateOrganisasi.create.form.email} value={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/> />
</Box> </Box>
@@ -207,7 +234,7 @@ function CreatePegawaiBumDes() {
<TextInput <TextInput
label="Nomor Telepon" label="Nomor Telepon"
placeholder="Contoh: 08123456789" placeholder="Contoh: 08123456789"
defaultValue={stateOrganisasi.create.form.telepon} value={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/> />
</Box> </Box>
@@ -216,7 +243,7 @@ function CreatePegawaiBumDes() {
<TextInput <TextInput
label="Alamat" label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1" placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={stateOrganisasi.create.form.alamat} value={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)} onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/> />
</Box> </Box>
@@ -242,6 +269,17 @@ function CreatePegawaiBumDes() {
<Group justify="flex-end" mt="md"> <Group justify="flex-end" mt="md">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -252,7 +290,7 @@ function CreatePegawaiBumDes() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,7 +5,7 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -17,6 +17,7 @@ function EditPosisiOrganisasiBumDes() {
const params = useParams(); const params = useParams();
const id = params?.id as string; const id = params?.id as string;
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: '', nama: '',
@@ -24,6 +25,12 @@ function EditPosisiOrganisasiBumDes() {
hierarki: 0, hierarki: 0,
}); });
const [originalData, setOriginalData] = useState({
nama: '',
deskripsi: '',
hierarki: 0,
});
// Fungsi generik untuk update formData // Fungsi generik untuk update formData
const handleChange = (field: keyof typeof formData, value: any) => { const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
@@ -42,6 +49,11 @@ function EditPosisiOrganisasiBumDes() {
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0, hierarki: data.hierarki || 0,
}); });
setOriginalData({
nama: data.nama || '',
deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0,
});
} }
} catch (err) { } catch (err) {
console.error('Error loading posisi organisasi:', err); console.error('Error loading posisi organisasi:', err);
@@ -52,6 +64,15 @@ function EditPosisiOrganisasiBumDes() {
loadPosisiOrganisasi(); loadPosisiOrganisasi();
}, [id]); }, [id]);
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
deskripsi: originalData.deskripsi,
hierarki: originalData.hierarki,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!formData.nama.trim()) { if (!formData.nama.trim()) {
toast.error('Nama posisi organisasi tidak boleh kosong'); toast.error('Nama posisi organisasi tidak boleh kosong');
@@ -59,6 +80,7 @@ function EditPosisiOrganisasiBumDes() {
} }
try { try {
setIsSubmitting(true);
// Update global state hanya saat submit // Update global state hanya saat submit
stateOrganisasi.edit.form = { stateOrganisasi.edit.form = {
nama: formData.nama.trim(), nama: formData.nama.trim(),
@@ -78,6 +100,8 @@ function EditPosisiOrganisasiBumDes() {
} catch (err) { } catch (err) {
console.error('Error updating posisi organisasi:', err); console.error('Error updating posisi organisasi:', err);
// toast error biasanya sudah ada di update // toast error biasanya sudah ada di update
} finally {
setIsSubmitting(false);
} }
}; };
@@ -132,10 +156,21 @@ function EditPosisiOrganisasiBumDes() {
required required
/> />
<Group justify="flex-end" mt="md"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md" radius="md"
size="md" size="md"
style={{ style={{
@@ -144,7 +179,7 @@ function EditPosisiOrganisasiBumDes() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -3,37 +3,32 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'; import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreatePosisiOrganisasiBumDes() { function CreatePosisiOrganisasiBumDes() {
const router = useRouter(); const router = useRouter();
const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi); const stateOrganisasi = useProxy(stateStrukturBumDes.posisiOrganisasi);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => { useEffect(() => {
stateOrganisasi.findMany.load(); stateOrganisasi.findMany.load();
// Initialize form with default values }, []);
const resetForm = () => {
stateOrganisasi.create.form = { stateOrganisasi.create.form = {
nama: "", nama: "",
deskripsi: "", deskripsi: "",
hierarki: 0, hierarki: 0,
}; };
}
return () => {
// Clean up form on unmount
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
};
}, []);
const handleSubmit = async () => { const handleSubmit = async () => {
setIsSubmitting(true);
try { try {
if (!stateOrganisasi.create.form.nama.trim()) { if (!stateOrganisasi.create.form.nama.trim()) {
return toast.error('Nama posisi tidak boleh kosong'); return toast.error('Nama posisi tidak boleh kosong');
@@ -45,6 +40,8 @@ function CreatePosisiOrganisasiBumDes() {
} catch (error) { } catch (error) {
toast.error('Gagal menambahkan posisi organisasi'); toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error); console.error('Error:', error);
} finally {
setIsSubmitting(false);
} }
}; };
@@ -71,7 +68,7 @@ function CreatePosisiOrganisasiBumDes() {
<TextInput <TextInput
label="Nama Posisi" label="Nama Posisi"
placeholder="Contoh: Kepala Desa" placeholder="Contoh: Kepala Desa"
defaultValue={stateOrganisasi.create.form.nama} value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.target.value)} onChange={(e) => (stateOrganisasi.create.form.nama = e.target.value)}
required required
/> />
@@ -93,7 +90,7 @@ function CreatePosisiOrganisasiBumDes() {
type="number" type="number"
min={0} min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)" placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
defaultValue={stateOrganisasi.create.form.hierarki || ''} value={stateOrganisasi.create.form.hierarki || ''}
onChange={(e) => { onChange={(e) => {
const value = parseInt(e.target.value, 10); const value = parseInt(e.target.value, 10);
stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value; stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
@@ -101,10 +98,21 @@ function CreatePosisiOrganisasiBumDes() {
required required
/> />
<Group justify="flex-end" mt="md"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
loading={stateOrganisasi.create.loading}
radius="md" radius="md"
size="md" size="md"
style={{ style={{
@@ -113,7 +121,7 @@ function CreatePosisiOrganisasiBumDes() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,10 +5,12 @@ import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digita
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Image, Image,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -29,12 +31,18 @@ function EditDigitalSmartVillage() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
deskripsi: '', deskripsi: '',
imageId: '', imageId: '',
}); });
const [originalData, setOriginalData] = useState({
name: '',
deskripsi: '',
imageId: '',
imageUrl: '',
});
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
@@ -49,6 +57,12 @@ function EditDigitalSmartVillage() {
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
imageId: data.imageId || '', imageId: data.imageId || '',
}); });
setOriginalData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
imageUrl: data.image?.link || '',
});
if (data?.image?.link) setPreviewImage(data.image.link); if (data?.image?.link) setPreviewImage(data.image.link);
} }
@@ -63,6 +77,7 @@ function EditDigitalSmartVillage() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsSubmitting(true);
stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData }; stateDesaDigital.edit.form = { ...stateDesaDigital.edit.form, ...formData };
if (file) { if (file) {
@@ -79,9 +94,22 @@ function EditDigitalSmartVillage() {
} catch (error) { } catch (error) {
console.error('Error updating desa digital:', error); console.error('Error updating desa digital:', error);
toast.error('Terjadi kesalahan saat memperbarui data'); toast.error('Terjadi kesalahan saat memperbarui data');
} finally {
setIsSubmitting(false);
} }
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
imageId: originalData.imageId,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info('Form dikembalikan ke data awal');
};
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
@@ -119,7 +147,7 @@ function EditDigitalSmartVillage() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
> >
@@ -138,25 +166,45 @@ function EditDigitalSmartVillage() {
Seret gambar atau klik untuk memilih file Seret gambar atau klik untuk memilih file
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text> </Text>
</Stack> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{previewImage && ( {previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}> <Box pos={"relative"} mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image <Image
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ style={{
maxHeight: 220, maxHeight: 200,
objectFit: 'contain', objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`, border: '1px solid #ddd',
}} }}
loading="lazy" loading="lazy"
/> />
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
@@ -185,6 +233,17 @@ function EditDigitalSmartVillage() {
{/* Tombol Simpan */} {/* Tombol Simpan */}
<Group justify="right"> <Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -195,7 +254,7 @@ function EditDigitalSmartVillage() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -2,9 +2,11 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
@@ -26,6 +28,7 @@ export default function CreateDesaDigital() {
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter(); const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
stateDesaDigital.create.form = { stateDesaDigital.create.form = {
@@ -43,6 +46,7 @@ export default function CreateDesaDigital() {
} }
try { try {
setIsSubmitting(true);
const uploadRes = await ApiFetch.api.fileStorage.create.post({ const uploadRes = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
@@ -63,6 +67,8 @@ export default function CreateDesaDigital() {
} catch (error) { } catch (error) {
console.error('Error in handleSubmit:', error); console.error('Error in handleSubmit:', error);
toast.error('Terjadi kesalahan saat menyimpan data'); toast.error('Terjadi kesalahan saat menyimpan data');
} finally {
setIsSubmitting(false);
} }
}; };
@@ -101,7 +107,7 @@ export default function CreateDesaDigital() {
<TextInput <TextInput
label="Nama Desa Digital" label="Nama Desa Digital"
placeholder="Masukkan nama desa digital" placeholder="Masukkan nama desa digital"
defaultValue={stateDesaDigital.create.form.name} value={stateDesaDigital.create.form.name}
onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)} onChange={(e) => (stateDesaDigital.create.form.name = e.target.value)}
radius="md" radius="md"
withAsterisk withAsterisk
@@ -135,7 +141,7 @@ export default function CreateDesaDigital() {
}} }}
onReject={() => toast.error('File tidak valid, gunakan format gambar')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md" radius="md"
p="xl" p="xl"
style={{ style={{
@@ -163,6 +169,7 @@ export default function CreateDesaDigital() {
{/* Preview */} {/* Preview */}
{previewImage && ( {previewImage && (
<Box <Box
pos={"relative"}
mt="sm" mt="sm"
style={{ style={{
textAlign: 'center', textAlign: 'center',
@@ -180,12 +187,42 @@ export default function CreateDesaDigital() {
borderRadius: 12, borderRadius: 12,
}} }}
/> />
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box> </Box>
)} )}
</Box> </Box>
{/* Tombol Submit */} {/* Tombol Submit */}
<Group justify="flex-end" mt="sm"> <Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md" radius="md"
@@ -194,20 +231,9 @@ export default function CreateDesaDigital() {
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`, background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff', color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)', boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
}}
onMouseEnter={(e) => {
(e.currentTarget.style.transform = 'translateY(-2px)');
(e.currentTarget.style.boxShadow =
'0 6px 20px rgba(79, 172, 254, 0.5)');
}}
onMouseLeave={(e) => {
(e.currentTarget.style.transform = 'translateY(0)');
(e.currentTarget.style.boxShadow =
'0 4px 15px rgba(79, 172, 254, 0.4)');
}} }}
> >
Simpan {isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

Some files were not shown because too many files have changed in this diff Show More