nico/12-nov-25 #10

Merged
nicoarya20 merged 2 commits from nico/12-nov-25 into staging 2025-11-12 17:43:31 +08:00
455 changed files with 13778 additions and 6950 deletions

View File

@@ -3,9 +3,9 @@
"version": "0.1.5", "version": "0.1.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "bun --bun next dev", "dev": "next dev",
"build": "bun --bun next build", "build": "next build",
"start": "bun --bun next start" "start": "next start"
}, },
"prisma": { "prisma": {
"seed": "bun run prisma/seed.ts" "seed": "bun run prisma/seed.ts"

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

@@ -48,6 +48,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) {
}; };
}, [editor, onChange]); }, [editor, onChange]);
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

@@ -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

@@ -93,6 +93,34 @@ const sdgsDesa = proxy({
} }
}, },
}, },
findManyAll: {
data: null as any[] | null,
loading: false,
load: async () => { // Change to arrow function
sdgsDesa.findManyAll.loading = true; // Use the full path to access the property
try {
const query: any = {};
const res = await ApiFetch.api.landingpage.sdgsdesa[
"findManyAll"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
sdgsDesa.findManyAll.data = res.data.data || [];
} else {
console.error("Failed to load media sosial:", res.data?.message);
sdgsDesa.findManyAll.data = [];
}
} catch (error) {
console.error("Error loading media sosial:", error);
sdgsDesa.findManyAll.data = [];
} finally {
sdgsDesa.findManyAll.loading = false;
}
},
},
findUnique: { findUnique: {
data: null as Prisma.SdgsDesaGetPayload<{ data: null as Prisma.SdgsDesaGetPayload<{
include: { include: {

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 () => {
setIsSubmitting(true);
try {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push('/admin/desa/berita/kategori-berita'); 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,9 +131,24 @@ 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 */}
@@ -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,6 +49,8 @@ export default function CreateBerita() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!file) { if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu'); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
@@ -66,6 +71,12 @@ export default function CreateBerita() {
resetForm(); resetForm();
router.push('/admin/desa/berita/list-berita'); 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);
}
}; };
return ( return (
@@ -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,7 +76,18 @@ function EditVideo() {
[] []
); );
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
linkVideo: originalData.linkVideo,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
const converted = convertYoutubeUrlToEmbed(formData.linkVideo); const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) { if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar."); toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
@@ -81,6 +107,12 @@ function EditVideo() {
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');
} }
} catch (error) {
console.error('Error updating video:', error);
toast.error('Terjadi kesalahan saat memperbarui video');
} finally {
setIsSubmitting(false);
}
}; };
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
@@ -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,6 +38,8 @@ function CreateVideo() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!embedLink) { if (!embedLink) {
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.'); toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
return; return;
@@ -44,6 +49,12 @@ function CreateVideo() {
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 (
@@ -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);
} }
}; };
@@ -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);
@@ -64,7 +77,17 @@ function EditPelayananPendudukNonPermanent() {
})); }));
}; };
const handleResetForm = () => {
setFormData({
name: originalData.name,
deskripsi: originalData.deskripsi,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!statePendudukNonPermanent.findById.data) return; if (!statePendudukNonPermanent.findById.data) return;
// Update global state hanya di submit // Update global state hanya di submit
@@ -76,6 +99,12 @@ function EditPelayananPendudukNonPermanent() {
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 (
@@ -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,6 +35,14 @@ 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) {
@@ -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");
} }
@@ -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);
} }
}; };
@@ -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,115 +1,270 @@
'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} />
@@ -119,6 +274,7 @@ function EditSuratKeterangan() {
</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 && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar 1"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/> />
</Box>
)}
</Box>
{/* Upload Gambar 2 */} {/* Gambar 2 */}
<Box> <FileUploader
<Text fw="bold" fz="sm" mb={6}> title="Gambar Alur Pelayanan Surat"
Gambar Alur Pelayanan Surat file={file2}
</Text> setFile={setFile2}
<Dropzone preview={previewImage2}
onDrop={(files) => { setPreview={setPreviewImage2}
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>

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);
} }
}; };
@@ -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,26 +206,48 @@ 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' }}>
<Box>
<Image <Image
src={previewImage} src={previewImage.startsWith('http') ? previewImage : `${window.location.origin}${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"
/> />
</Box> </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);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</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,6 +42,8 @@ function CreatePenghargaan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!file) { if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu'); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
@@ -59,6 +64,12 @@ function CreatePenghargaan() {
await statePenghargaan.create.create(); await statePenghargaan.create.create();
resetForm(); resetForm();
router.push('/admin/desa/penghargaan'); router.push('/admin/desa/penghargaan');
} catch (error) {
console.error('Error creating penghargaan:', error);
toast.error('Terjadi kesalahan saat menambahkan penghargaan');
} finally {
setIsSubmitting(false);
}
}; };
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 () => {
try {
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push('/admin/desa/pengumuman/kategori-pengumuman'); 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 () => {
try {
setIsSubmitting(true);
await pengumumanState.pengumuman.create.create(); await pengumumanState.pengumuman.create.create();
resetForm(); resetForm();
router.push('/admin/desa/pengumuman/list-pengumuman'); 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 = () => {
@@ -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,9 +28,16 @@ function CreateKategoriPotensi() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
await createState.create.create(); await createState.create.create();
resetForm(); resetForm();
router.push('/admin/desa/potensi/kategori-potensi'); router.push('/admin/desa/potensi/kategori-potensi');
} catch (error) {
console.error('Error creating kategori potensi:', error);
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -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) => {
@@ -62,19 +74,16 @@ function EditPotensi() {
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, });
// }));
if (data?.image?.link) {
setPreviewImage(data.image.link); setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading potensi:", error); console.error("Error loading potensi:", error);
@@ -85,11 +94,24 @@ function EditPotensi() {
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,6 +137,8 @@ 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);
} }
}; };
@@ -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,12 +30,15 @@ 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 () => {
try {
setIsSubmitting(true);
if (!file) return toast.warn('Pilih file gambar terlebih dahulu'); 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({
@@ -52,6 +57,12 @@ function CreatePotensi() {
resetForm(); resetForm();
router.push('/admin/desa/potensi/list-potensi'); router.push('/admin/desa/potensi/list-potensi');
} catch (error) {
console.error('Error creating potensi:', error);
toast.error('Terjadi kesalahan saat menambahkan potensi');
} finally {
setIsSubmitting(false);
}
}; };
const resetForm = () => { const resetForm = () => {
@@ -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,146 +1,238 @@
/* 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 [formData, setFormData] = useState<FormData>({ judul: '', deskripsi: '' });
const [originalData, setOriginalData] = useState<FormData>({ judul: '', deskripsi: '' });
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// Load data const [loadError, setLoadError] = useState<string | null>(null);
// 🧭 Load data awal
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 lambangState.findUnique.load(id); const data = await stateProfileDesa.lambangDesa.findUnique.load(id);
lambangState.update.initialize(data);
if (data) {
const initial: FormData = {
judul: data.judul || '',
deskripsi: data.deskripsi || '',
};
setFormData(initial);
setOriginalData(initial);
// Penting untuk isi id di state sebelum submit
stateProfileDesa.lambangDesa.update.initialize(data);
} else {
setLoadError('Data tidak ditemukan');
}
} catch (error) { } catch (error) {
console.error("Error loading lambang:", error); console.error('Error loading lambang:', error);
toast.error("Gagal memuat data lambang desa"); setLoadError('Gagal memuat data lambang desa');
toast.error('Gagal memuat data lambang desa');
} finally {
setIsLoading(false);
} }
}; };
loadData(); loadData();
return () => { return () => {
lambangState.update.reset(); stateProfileDesa.lambangDesa.update.reset();
lambangState.findUnique.reset(); stateProfileDesa.lambangDesa.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
// 🔁 Reset form
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit handler
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !lambangState.update.form.judul.trim()) { if (!formData.judul.trim()) {
toast.error("Judul wajib diisi"); toast.error('Judul wajib diisi');
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const success = await lambangState.update.submit(); const state = stateProfileDesa.lambangDesa;
state.update.form.judul = formData.judul;
state.update.form.deskripsi = formData.deskripsi;
const success = await state.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 lambang desa:", error); console.error('Error update lambang desa:', error);
toast.error("Terjadi kesalahan saat update lambang desa"); toast.error('Terjadi kesalahan saat update lambang desa');
} finally { } finally {
setIsSubmitting(false); 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(); const handleBack = () => router.back();
// Loading state // 🔄 Loading
if (lambangState.findUnique.loading || lambangState.update.loading) { 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 lambang desa...
</Text>
</Stack>
</Center> </Center>
</Box> </Box>
); );
} }
// Error state // Error
if (lambangState.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={handleBack} 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 icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
<Text fw="bold">Error</Text> {loadError}
<Text>{lambangState.findUnique.error}</Text>
</Alert> </Alert>
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
Kembali ke Halaman Utama
</Button>
</Stack> </Stack>
</Box> </Box>
); );
} }
// 🧱 UI utama
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 Lambang Desa</Title> <Title order={4} ml="sm" c="dark">
Edit Lambang 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 Lambang Desa</Title> w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
{/* Judul */} {/* Judul */}
<TextInput <TextInput
label={<Text fw="bold">Judul</Text>} label={<Text fw="bold" size="sm">Judul</Text>}
placeholder="Judul lambang" placeholder="Masukkan judul lambang desa"
defaultValue={lambangState.update.form.judul} value={formData.judul}
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value} onChange={handleJudulChange}
error={!lambangState.update.form.judul && "Judul wajib diisi"} error={!formData.judul.trim() && 'Judul wajib diisi'}
required
size="md"
radius="md"
/> />
{/* Deskripsi */} {/* Deskripsi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" size="sm" mb={8}>
<EditEditor Deskripsi
value={lambangState.update.form.deskripsi} </Text>
onChange={(val) => lambangState.update.form.deskripsi = val} <EditEditor value={formData.deskripsi} onChange={handleDeskripsiChange} />
/>
</Box> </Box>
{/* Buttons */} {/* Tombol Aksi */}
<Group> <Group justify="right">
<Button <Button
bg={colors['blue-button']} variant="outline"
onClick={handleSubmit} color="gray"
loading={isSubmitting || lambangState.update.loading} radius="md"
disabled={!lambangState.update.form.judul} size="md"
onClick={handleResetForm}
disabled={isSubmitting}
> >
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || lambangState.update.loading}>
Batal Batal
</Button> </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> </Group>
</Stack> </Stack>
</Paper> </Paper>

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';
@@ -20,7 +20,7 @@ function Page() {
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
@@ -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"
<TextInput radius="md"
label={<Text fw="bold">Judul</Text>} shadow="sm"
placeholder="Judul sejarah" style={{ border: '1px solid #e0e0e0' }}
defaultValue={sejarahState.update.form.judul} >
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value} <Stack gap="lg">
error={!sejarahState.update.form.judul && "Judul wajib diisi"} {/* Form Title */}
/>
{/* Deskripsi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text> <Title order={3} mb={4}>
<EditEditor Informasi Sejarah Desa
value={sejarahState.update.form.deskripsi} </Title>
onChange={(val) => sejarahState.update.form.deskripsi = val} </Box>
{/* Judul Field */}
<TextInput
label={<Text fw="bold" size="sm">Judul Sejarah</Text>}
placeholder="Masukkan judul sejarah desa"
value={formData.judul}
onChange={handleJudulChange}
error={!formData.judul.trim() && 'Judul wajib diisi'}
required
size="md"
radius="md"
/> />
{/* Deskripsi Field */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Deskripsi Sejarah
</Text>
<EditEditor
value={formData.deskripsi}
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>

View File

@@ -1,149 +1,241 @@
/* 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 [originalData, setOriginalData] = useState<FormData>({ visi: '', misi: '' });
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); 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;
} }
setIsLoading(true);
setLoadError(null);
try { try {
const data = await visiMisiState.findUnique.load(id); const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id);
visiMisiState.update.initialize(data); if (data) {
const initialData: FormData = {
visi: data.visi || '',
misi: data.misi || '',
};
setFormData(initialData);
setOriginalData(initialData);
// set id ke state agar submit pakai endpoint benar
stateProfileDesa.visiMisiDesa.update.initialize(data);
} else {
setLoadError('Data tidak ditemukan');
}
} catch (error) { } catch (error) {
console.error("Error loading visi misi:", error); console.error('Error load visi misi:', error);
toast.error("Gagal memuat data visi misi desa"); setLoadError('Gagal memuat data visi misi desa');
toast.error('Gagal memuat data visi misi desa');
} finally {
setIsLoading(false);
} }
}; };
loadData(); loadData();
return () => { return () => {
visiMisiState.update.reset(); stateProfileDesa.visiMisiDesa.update.reset();
visiMisiState.findUnique.reset(); stateProfileDesa.visiMisiDesa.findUnique.reset();
}; };
}, [params?.id, router]); }, [params?.id, router]);
// 🔄 Reset Form
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !visiMisiState.update.form.visi.trim()) { if (!formData.visi.trim()) {
toast.error("Visi wajib diisi"); toast.error('Visi wajib diisi');
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const success = await visiMisiState.update.submit(); const originalState = stateProfileDesa.visiMisiDesa;
// update data form ke state sebelum submit
originalState.update.form.visi = formData.visi;
originalState.update.form.misi = formData.misi;
const success = await originalState.update.submit();
if (success) { 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 visi misi desa:", error); console.error('Error update visi misi desa:', error);
toast.error("Terjadi kesalahan saat update visi misi desa"); toast.error('Terjadi kesalahan saat update visi misi desa');
} finally { } finally {
setIsSubmitting(false); 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(); const handleBack = () => router.back();
// Loading state // Loading
if (visiMisiState.findUnique.loading || visiMisiState.update.loading) { 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
if (visiMisiState.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={handleBack} 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>{visiMisiState.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>
); );
} }
// ✅ UI
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="xs"> <Stack gap="md">
<Group mb="md"> {/* Header */}
<Button variant="subtle" onClick={handleBack} p="xs" radius="md"> <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>
</Button> <Title order={4} ml="sm" c="dark">
<Title order={4} ml="sm" c="dark">Edit Visi Misi Desa</Title> Edit Visi & Misi 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 Visi Misi Desa</Title> 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 */} {/* Visi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Visi</Text> <Text fw="bold" size="sm" mb={8}>
<EditEditor Visi
value={visiMisiState.update.form.visi} </Text>
onChange={(val) => visiMisiState.update.form.visi = val} <EditEditor value={formData.visi} onChange={handleVisiChange} />
/>
</Box> </Box>
{/* Misi */} {/* Misi */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Misi</Text> <Text fw="bold" size="sm" mb={8}>
<EditEditor Misi
value={visiMisiState.update.form.misi} </Text>
onChange={(val) => visiMisiState.update.form.misi = val} <EditEditor value={formData.misi} onChange={handleMisiChange} />
/>
</Box> </Box>
{/* Buttons */} {/* Actions */}
<Group> <Group justify="right">
<Button <Button
bg={colors['blue-button']} variant="outline"
onClick={handleSubmit} color="gray"
loading={isSubmitting || visiMisiState.update.loading} radius="md"
disabled={!visiMisiState.update.form.visi} size="md"
onClick={handleResetForm}
> >
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || visiMisiState.update.loading}>
Batal Batal
</Button> </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> </Group>
</Stack> </Stack>
</Paper> </Paper>

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,6 +121,8 @@ 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);
} }
}; };
@@ -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,6 +29,8 @@ function CreatePerbekelDariMasaKeMasa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!file) { if (!file) {
return toast.warn('Pilih file gambar terlebih dahulu'); return toast.warn('Pilih file gambar terlebih dahulu');
} }
@@ -44,6 +47,12 @@ function CreatePerbekelDariMasaKeMasa() {
await state.create.create(); await state.create.create();
resetForm(); resetForm();
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa'); 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);
}
}; };
return ( return (
@@ -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

@@ -2,23 +2,22 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
ScrollArea,
Stack, Stack,
Tabs, Tabs,
TabsList, TabsList,
TabsPanel, TabsPanel,
TabsTab, TabsTab,
Title, Title
Tooltip,
ScrollArea,
} from '@mantine/core'; } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { import {
IconFileAnalytics,
IconCoins, IconCoins,
IconFileAnalytics,
IconShoppingCart, IconShoppingCart,
IconWallet, IconWallet,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
@@ -29,29 +28,25 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
label: "APB Desa", label: "APB Desa",
value: "apbdesa", value: "apbdesa",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa",
icon: <IconFileAnalytics size={18} stroke={1.8} />, icon: <IconFileAnalytics size={18} stroke={1.8} />
tooltip: "Lihat ringkasan Anggaran Pendapatan dan Belanja Desa",
}, },
{ {
label: "Pendapatan", label: "Pendapatan",
value: "pendapatan", value: "pendapatan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan",
icon: <IconCoins size={18} stroke={1.8} />, icon: <IconCoins size={18} stroke={1.8} />,
tooltip: "Kelola data pendapatan desa",
}, },
{ {
label: "Belanja", label: "Belanja",
value: "belanja", value: "belanja",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja",
icon: <IconShoppingCart size={18} stroke={1.8} />, icon: <IconShoppingCart size={18} stroke={1.8} />,
tooltip: "Atur data belanja desa",
}, },
{ {
label: "Pembiayaan", label: "Pembiayaan",
value: "pembiayaan", value: "pembiayaan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan", href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan",
icon: <IconWallet size={18} stroke={1.8} />, icon: <IconWallet size={18} stroke={1.8} />,
tooltip: "Kelola data pembiayaan desa",
}, },
]; ];
@@ -104,14 +99,8 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: "pop", duration: 200 }}
>
<TabsTab <TabsTab
key={i}
value={tab.value} value={tab.value}
leftSection={tab.icon} leftSection={tab.icon}
style={{ style={{
@@ -123,7 +112,6 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
> >
{tab.label} {tab.label}
</TabsTab> </TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
</ScrollArea> </ScrollArea>

View File

@@ -1,11 +1,14 @@
/* 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,
Loader,
MultiSelect, MultiSelect,
Paper, Paper,
Skeleton, Skeleton,
@@ -13,8 +16,6 @@ import {
Text, Text,
TextInput, TextInput,
Title, Title,
Tooltip,
Group,
} 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';
@@ -23,72 +24,124 @@ 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 */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -97,7 +150,6 @@ function EditAPBDesa() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit APB Desa Edit APB Desa
</Title> </Title>
@@ -117,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"
@@ -151,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({
function SelectPendapatan({ label,
state,
selectedIds, selectedIds,
onSelectionChange, onSelectionChange,
}: { }: {
label: string;
state: any;
selectedIds: string[]; selectedIds: string[];
onSelectionChange: (ids: string[]) => void; onSelectionChange: (ids: string[]) => void;
}) { }) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); const proxyState = useProxy(state);
useShallowEffect(() => { useShallowEffect(() => {
pendapatanState.findMany.load(); proxyState.findMany.load();
}, []); }, []);
if (!pendapatanState.findMany.data) { const data = proxyState.findMany.data;
return <Skeleton height={38} />; const isLoading = !data;
const options =
data
?.filter((item: any) => item?.id)
.map((item: any) => ({
value: String(item.id),
label: String(item?.name || '(Tanpa Nama)'),
})) || [];
if (isLoading) {
return (
<Box>
<Text fz="sm" fw="bold" mb={4}>{label}</Text>
<Skeleton height={38} radius="sm" />
</Box>
);
}
if (options.length === 0) {
return (
<Alert color="gray" variant="light">
<Text size="sm">
Tidak ada data {label.toLowerCase()} tersedia.
</Text>
</Alert>
);
} }
return ( return (
<MultiSelect <MultiSelect
label={<Text fz="sm" fw="bold">Pendapatan</Text>} label={<Text fz="sm" fw="bold">{label}</Text>}
data={pendapatanState.findMany.data.map((p: any) => ({ data={options}
value: p.id,
label: p.name,
}))}
value={selectedIds} value={selectedIds}
onChange={onSelectionChange} onChange={(ids) => onSelectionChange(safeStringArray(ids))}
searchable searchable
clearable clearable
placeholder="Pilih pendapatan..." placeholder={`Pilih ${label.toLowerCase()}...`}
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
/> />
); );
}
function SelectBelanja({
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 (
<MultiSelect
label={<Text fz="sm" fw="bold">Belanja</Text>}
data={belanjaState.findMany.data.map((b: any) => ({
value: b.id,
label: b.name,
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih belanja..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
function SelectPembiayaan({
selectedIds,
onSelectionChange,
}: {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {
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

@@ -9,8 +9,7 @@ import {
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Text, Text
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
@@ -81,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">
@@ -159,7 +158,6 @@ function DetailAPBDesa() {
</Box> </Box>
<Group gap="sm" mt={10}> <Group gap="sm" mt={10}>
<Tooltip label="Hapus APB Desa" withArrow position="top">
<Button <Button
color="red" color="red"
onClick={() => { onClick={() => {
@@ -172,9 +170,7 @@ function DetailAPBDesa() {
> >
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit APB Desa" withArrow position="top">
<Button <Button
color="green" color="green"
onClick={() => onClick={() =>
@@ -188,7 +184,6 @@ function DetailAPBDesa() {
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Tooltip>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -6,23 +6,26 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
MultiSelect, MultiSelect,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
Tooltip,
} 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 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 = {
@@ -34,20 +37,26 @@ function CreateAPBDesa() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
await apbDesaState.create.submit(); await apbDesaState.create.submit();
resetForm(); resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa'); 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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah APB Desa Tambah APB Desa
</Title> </Title>
@@ -65,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);
}} }}
@@ -97,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"
@@ -107,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

@@ -5,8 +5,8 @@ import {
Button, Button,
Center, Center,
Group, Group,
Paper,
Pagination, Pagination,
Paper,
Skeleton, Skeleton,
Stack, Stack,
Table, Table,
@@ -15,8 +15,7 @@ import {
TableTh, TableTh,
TableThead, TableThead,
TableTr, TableTr,
Text, Text
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -82,7 +81,6 @@ function ListAPBDesa({ search }: { search: string }) {
<Text fw={600} fz="lg"> <Text fw={600} fz="lg">
List APB Desa List APB Desa
</Text> </Text>
<Tooltip label="Tambah APB Desa" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -95,7 +93,6 @@ function ListAPBDesa({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table highlightOnHover> <Table highlightOnHover>
@@ -138,7 +135,6 @@ function ListAPBDesa({ search }: { search: string }) {
)} )}
</TableTd> </TableTd>
<TableTd> <TableTd>
<Tooltip label="Lihat Detail" withArrow>
<Button <Button
variant="light" variant="light"
color="green" color="green"
@@ -151,7 +147,6 @@ function ListAPBDesa({ search }: { search: string }) {
<IconDeviceImacCog size={20} /> <IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text> <Text ml={5}>Detail</Text>
</Button> </Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))

View File

@@ -7,11 +7,11 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Tooltip
} 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';
@@ -23,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 =
@@ -59,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);
@@ -71,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,
@@ -83,14 +94,23 @@ 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 */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -99,7 +119,6 @@ function EditBelanja() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Jenis Belanja Edit Jenis Belanja
</Title> </Title>
@@ -138,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"
@@ -148,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,21 +6,23 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
Tooltip,
} 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 { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
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 =
@@ -48,16 +50,23 @@ function CreateBelanja() {
return toast.warn('Lengkapi semua field terlebih dahulu'); return toast.warn('Lengkapi semua field terlebih dahulu');
} }
try {
setIsSubmitting(true);
await belanjaState.create.submit(); await belanjaState.create.submit();
resetForm(); resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja'); 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 (
<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">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -66,7 +75,6 @@ function CreateBelanja() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Jenis Belanja Tambah Jenis Belanja
</Title> </Title>
@@ -85,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
/> />
@@ -94,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);
@@ -103,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"
@@ -113,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

@@ -17,8 +17,7 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -96,7 +95,6 @@ function ListBelanja({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Belanja</Title> <Title order={4}>Daftar Belanja</Title>
<Tooltip label="Tambah Belanja" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -107,7 +105,6 @@ function ListBelanja({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
@@ -138,7 +135,6 @@ function ListBelanja({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Group gap="xs"> <Group gap="xs">
<Tooltip label="Edit" withArrow>
<Button <Button
size="xs" size="xs"
variant="light" variant="light"
@@ -151,8 +147,6 @@ function ListBelanja({ search }: { search: string }) {
> >
<IconEdit size={16} /> <IconEdit size={16} />
</Button> </Button>
</Tooltip>
<Tooltip label="Hapus" withArrow>
<Button <Button
size="xs" size="xs"
variant="light" variant="light"
@@ -165,7 +159,6 @@ function ListBelanja({ search }: { search: string }) {
> >
<IconTrash size={16} /> <IconTrash size={16} />
</Button> </Button>
</Tooltip>
</Group> </Group>
</TableTd> </TableTd>
</TableTr> </TableTr>

View File

@@ -6,11 +6,11 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Tooltip
} 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,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'
@@ -56,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);
@@ -66,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,
@@ -80,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);
} }
}; };
@@ -87,7 +108,6 @@ function EditPembiayaan() {
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -96,7 +116,6 @@ function EditPembiayaan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Jenis Pembiayaan Edit Jenis Pembiayaan
</Title> </Title>
@@ -135,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"
@@ -145,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

@@ -1,26 +1,27 @@
'use client'; 'use client';
import React from 'react';
import { useProxy } from 'valtio/utils';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import { useRouter } from 'next/navigation';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Title,
TextInput,
Text, Text,
Tooltip, TextInput,
Title
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
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 =
@@ -44,6 +45,8 @@ function CreatePembiayaan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) { if (!pembiayaanState.create.form.name || !pembiayaanState.create.form.value) {
return toast.warn('Nama dan nilai wajib diisi'); return toast.warn('Nama dan nilai wajib diisi');
} }
@@ -51,13 +54,18 @@ function CreatePembiayaan() {
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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -66,7 +74,6 @@ function CreatePembiayaan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Jenis Pembiayaan Tambah Jenis Pembiayaan
</Title> </Title>
@@ -85,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;
}} }}
@@ -96,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);
@@ -105,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"
@@ -115,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

@@ -1,9 +1,11 @@
'use client' 'use client'
import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Center, Center,
Group, Group,
Pagination,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
@@ -14,19 +16,16 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip,
Pagination,
} from '@mantine/core'; } from '@mantine/core';
import React, { useState } from 'react';
import HeaderSearch from '../../../_com/header';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import { useProxy } from 'valtio/utils';
import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function Pembiayaan() { function Pembiayaan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -95,7 +94,6 @@ function ListPembiayaan({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pembiayaan</Title> <Title order={4}>Daftar Pembiayaan</Title>
<Tooltip label="Tambah Pembiayaan" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -106,7 +104,6 @@ function ListPembiayaan({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>

View File

@@ -6,11 +6,11 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Tooltip
} 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 +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: '',
@@ -56,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);
@@ -73,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,
@@ -87,14 +107,16 @@ 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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header with Back Button */} {/* Header with Back Button */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -103,7 +125,6 @@ function EditPendapatan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Jenis Pendapatan Edit Jenis Pendapatan
</Title> </Title>
@@ -140,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"
@@ -150,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,19 +5,22 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Tooltip
} 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, ''));
@@ -40,16 +43,23 @@ function CreatePendapatan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
await pendapatanState.create.submit(); await pendapatanState.create.submit();
resetForm(); resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan'); 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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back + judul */} {/* Header dengan tombol back + judul */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -58,7 +68,6 @@ function CreatePendapatan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Jenis Pendapatan Tambah Jenis Pendapatan
</Title> </Title>
@@ -75,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;
}} }}
@@ -86,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);
@@ -98,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"
@@ -108,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

@@ -17,8 +17,7 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -96,7 +95,6 @@ function ListPendapatan({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Pendapatan</Title> <Title order={4}>Daftar Pendapatan</Title>
<Tooltip label="Tambah Pendapatan Baru" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -107,7 +105,6 @@ function ListPendapatan({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>

View File

@@ -6,15 +6,15 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Tooltip,
} 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';
import { useEffect, useState, useCallback } from 'react'; import { useCallback, 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 demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
@@ -29,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(() => {
@@ -42,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);
@@ -52,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);
} }
}; };
@@ -76,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 };
@@ -89,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);
} }
}; };
@@ -96,7 +121,6 @@ export default function EditDemografiPekerjaan() {
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -105,7 +129,6 @@ export default function EditDemografiPekerjaan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Demografi Pekerjaan Edit Demografi Pekerjaan
</Title> </Title>
@@ -148,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"
@@ -158,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,22 +7,24 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Tooltip,
} 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 { 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 = {
@@ -33,6 +35,7 @@ function CreateDemografiPekerjaan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
const id = await stateDemografi.create.create(); const id = await stateDemografi.create.create();
if (id) { if (id) {
const idStr = String(id); const idStr = String(id);
@@ -43,13 +46,18 @@ function CreateDemografiPekerjaan() {
} }
resetForm(); resetForm();
router.push('/admin/ekonomi/demografi-pekerjaan'); 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);
}
}; };
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">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -58,7 +66,6 @@ function CreateDemografiPekerjaan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Demografi Pekerjaan Tambah Demografi Pekerjaan
</Title> </Title>
@@ -77,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;
@@ -87,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);
@@ -97,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);
@@ -106,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"
@@ -116,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

@@ -6,7 +6,9 @@ import {
Box, Box,
Button, Button,
Center, Center,
Flex,
Group, Group,
Pagination,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
@@ -17,10 +19,7 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip,
Pagination,
Flex,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
@@ -111,7 +110,6 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>List Demografi Pekerjaan</Title> <Title order={4}>List Demografi Pekerjaan</Title>
<Tooltip label="Tambah Data Pekerjaan" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -120,7 +118,6 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>

View File

@@ -6,24 +6,24 @@ import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Group,
Tooltip,
} 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';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditJumlahPendudukMiskin() { 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
@@ -32,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;
@@ -45,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);
@@ -63,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 };
@@ -76,13 +94,14 @@ 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);
} }
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -91,7 +110,6 @@ function EditJumlahPendudukMiskin() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Jumlah Penduduk Miskin Edit Jumlah Penduduk Miskin
</Title> </Title>
@@ -127,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"
@@ -137,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

@@ -1,18 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; import colors from '@/con/colors';
import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { 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 colors from '@/con/colors';
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,6 +24,8 @@ export default function CreateJumlahPendudukMiskin() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
const id = await stateJPM.create.create(); const id = await stateJPM.create.create();
if (id) { if (id) {
const idStr = String(id); const idStr = String(id);
@@ -32,17 +36,21 @@ export default function CreateJumlahPendudukMiskin() {
} }
resetForm(); resetForm();
router.push('/admin/ekonomi/jumlah-penduduk-miskin'); 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);
}
}; };
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">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Jumlah Penduduk Miskin Tambah Jumlah Penduduk Miskin
</Title> </Title>
@@ -61,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;
@@ -73,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);
@@ -82,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"
@@ -92,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

@@ -16,11 +16,10 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -101,7 +100,6 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Jumlah Penduduk Miskin</Title> <Title order={4}>Daftar Jumlah Penduduk Miskin</Title>
<Tooltip label="Tambah Data" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -112,7 +110,6 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>

View File

@@ -2,18 +2,17 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
ScrollArea,
Stack, Stack,
Tabs, Tabs,
TabsList, TabsList,
TabsPanel, TabsPanel,
TabsTab, TabsTab,
Title, Title
Tooltip,
ScrollArea,
} from '@mantine/core'; } from '@mantine/core';
import { IconSchool, IconUsers } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconUsers, IconSchool } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
@@ -24,15 +23,13 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
label: "Pengangguran Berdasarkan Usia", label: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia", value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia",
icon: <IconUsers size={18} stroke={1.8} />, icon: <IconUsers size={18} stroke={1.8} />
tooltip: "Data pengangguran menurut kelompok usia",
}, },
{ {
label: "Pengangguran Berdasarkan Pendidikan", label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan", value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan", href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan",
icon: <IconSchool size={18} stroke={1.8} />, icon: <IconSchool size={18} stroke={1.8} />
tooltip: "Data pengangguran menurut tingkat pendidikan",
}, },
]; ];
@@ -78,14 +75,8 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
}} }}
> >
{tabs.map((tab, i) => ( {tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: "pop", duration: 200 }}
>
<TabsTab <TabsTab
key={i}
value={tab.value} value={tab.value}
leftSection={tab.icon} leftSection={tab.icon}
style={{ style={{
@@ -97,7 +88,6 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
> >
{tab.label} {tab.label}
</TabsTab> </TabsTab>
</Tooltip>
))} ))}
</TabsList> </TabsList>
</ScrollArea> </ScrollArea>

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, Tooltip } 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,32 +45,56 @@ 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 () => {
try {
setIsSubmitting(true);
stategrafik.update.id = id; stategrafik.update.id = id;
stategrafik.update.form = { ...formData }; // update global state pas submit aja stategrafik.update.form = { ...formData }; // update global state pas submit aja
await stategrafik.update.submit(); await stategrafik.update.submit();
router.push( router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' '/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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -69,7 +103,6 @@ function EditGrafikBerdasarkanPendidikan() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Grafik Pengangguran Berdasarkan Pendidikan Edit Grafik Pengangguran Berdasarkan Pendidikan
</Title> </Title>
@@ -85,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"
@@ -131,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

@@ -1,19 +1,20 @@
'use client'; 'use client';
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { useRouter } from 'next/navigation';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; import { Box, Button, Loader, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateGrafikBerdasarkanPendidikan() { 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 = {
@@ -27,6 +28,8 @@ function CreateGrafikBerdasarkanPendidikan() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
const id = await stategrafik.create.create(); const id = await stategrafik.create.create();
if (id) { if (id) {
const idStr = String(id); const idStr = String(id);
@@ -39,16 +42,20 @@ function CreateGrafikBerdasarkanPendidikan() {
router.push( router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan' '/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);
}
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Data Pengangguran Berdasarkan Pendidikan Tambah Data Pengangguran Berdasarkan Pendidikan
</Title> </Title>
@@ -67,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
/> />
@@ -75,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
/> />
@@ -83,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
/> />
@@ -91,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
/> />
@@ -99,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"
@@ -115,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

@@ -1,6 +1,7 @@
/* 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 { DonutChart } from '@mantine/charts';
import { import {
Box, Box,
Button, Button,
@@ -17,15 +18,13 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { DonutChart } from '@mantine/charts';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
@@ -116,7 +115,6 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Flex justify="space-between" align="center" mb="md"> <Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title> <Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title>
<Tooltip label="Tambah Data" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -129,7 +127,6 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Flex> </Flex>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
@@ -165,7 +162,6 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTd>{item.D3}</TableTd> <TableTd>{item.D3}</TableTd>
<TableTd>{item.S1}</TableTd> <TableTd>{item.S1}</TableTd>
<TableTd> <TableTd>
<Tooltip label="Edit Data" withArrow>
<Button <Button
color="green" color="green"
variant="light" variant="light"
@@ -177,10 +173,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
> >
<IconEdit size={18} /> <IconEdit size={18} />
</Button> </Button>
</Tooltip>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Tooltip label="Hapus Data" withArrow>
<Button <Button
color="red" color="red"
variant="light" variant="light"
@@ -192,7 +186,6 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
> >
<IconTrash size={18} /> <IconTrash size={18} />
</Button> </Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
@@ -224,6 +217,7 @@ 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>
<Center>
{donutData.length > 0 ? ( {donutData.length > 0 ? (
<DonutChart <DonutChart
data={donutData} data={donutData}
@@ -238,6 +232,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
)} )}
</Center>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -5,18 +5,18 @@ import colors from '@/con/colors';
import { import {
Box, Box,
Button, Button,
Group,
Loader,
Paper, Paper,
Stack, Stack,
TextInput, TextInput,
Title, Title
Group,
Tooltip,
} 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';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter(); const router = useRouter();
@@ -26,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: '',
@@ -34,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) {
@@ -46,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 || '',
});
} }
}); });
} }
@@ -58,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 };
@@ -73,13 +100,14 @@ 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);
} }
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -88,7 +116,6 @@ function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Grafik Pengangguran Berdasarkan Usia Kerja Edit Grafik Pengangguran Berdasarkan Usia Kerja
</Title> </Title>
@@ -137,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"
@@ -147,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

@@ -2,18 +2,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client'; 'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core'; import { Box, Button, Group, Loader, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { 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,6 +28,8 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
const id = await stategrafik.create.create(); const id = await stategrafik.create.create();
if (id) { if (id) {
const idStr = String(id); const idStr = String(id);
@@ -36,17 +40,21 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
} }
resetForm(); resetForm();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'); 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);
}
}; };
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">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Data Pengangguran Berdasarkan Usia Tambah Data Pengangguran Berdasarkan Usia
</Title> </Title>
@@ -66,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
/> />
@@ -74,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
/> />
@@ -82,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
/> />
@@ -90,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"
@@ -107,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

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -103,7 +103,6 @@ function DetailJumlahPengangguran() {
{/* Tombol Edit & Hapus */} {/* Tombol Edit & Hapus */}
<Flex gap="sm"> <Flex gap="sm">
<Tooltip label="Hapus Data Pengangguran" withArrow position="top">
<Button <Button
onClick={() => { onClick={() => {
setSelectedId(data.id); setSelectedId(data.id);
@@ -116,9 +115,7 @@ function DetailJumlahPengangguran() {
> >
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Data Pengangguran" withArrow position="top">
<Button <Button
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)} onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
color="green" color="green"
@@ -128,7 +125,6 @@ function DetailJumlahPengangguran() {
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Tooltip>
</Flex> </Flex>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -7,23 +7,25 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
NumberInput,
Paper, Paper,
Select,
Stack, Stack,
Text, Text,
NumberInput, Title
Title,
Select,
Tooltip,
} 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 { 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',
@@ -73,6 +75,8 @@ function CreateJumlahPengangguran() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
await calculateTotalAndChange(); await calculateTotalAndChange();
const id = await stateDetail.create.create(); const id = await stateDetail.create.create();
if (id) { if (id) {
@@ -83,13 +87,18 @@ function CreateJumlahPengangguran() {
resetForm(); resetForm();
router.push('/admin/ekonomi/jumlah-pengangguran'); router.push('/admin/ekonomi/jumlah-pengangguran');
} }
} catch (error) {
console.error("Error creating jumlah pengangguran:", error);
toast.error("Gagal menambahkan data pengangguran");
} finally {
setIsSubmitting(false);
}
}; };
return ( 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">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -98,7 +107,6 @@ function CreateJumlahPengangguran() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Data Pengangguran Tambah Data Pengangguran
</Title> </Title>
@@ -179,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"
@@ -189,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

@@ -1,17 +1,17 @@
/* 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 { BarChart } from '@mantine/charts';
import { import {
Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title, Tooltip Text, Title
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
@@ -85,7 +85,6 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Detail Data Pengangguran</Title> <Title order={4}>Daftar Detail Data Pengangguran</Title>
<Tooltip label="Tambah Data Baru" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -94,7 +93,6 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>

View File

@@ -7,12 +7,12 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
Tooltip,
} 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';
@@ -24,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: '',
@@ -36,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 () => {
@@ -55,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);
@@ -71,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 };
@@ -83,18 +119,17 @@ 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);
} }
}; };
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol back */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Edit Lowongan Kerja Lokal Edit Lowongan Kerja Lokal
</Title> </Title>
@@ -179,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"
@@ -189,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

@@ -1,6 +1,6 @@
'use client' 'use client'
import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -108,7 +108,6 @@ function DetailLowonganKerjaLokal() {
</Box> </Box>
<Group gap="sm" mt="sm"> <Group gap="sm" mt="sm">
<Tooltip label="Hapus Lowongan" withArrow position="top">
<Button <Button
color="red" color="red"
onClick={() => { onClick={() => {
@@ -121,9 +120,7 @@ function DetailLowonganKerjaLokal() {
> >
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Lowongan" withArrow position="top">
<Button <Button
color="green" color="green"
onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)} onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${data.id}/edit`)}
@@ -133,7 +130,6 @@ function DetailLowonganKerjaLokal() {
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Tooltip>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -4,22 +4,25 @@ import {
Box, Box,
Button, Button,
Group, Group,
Loader,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title
Tooltip,
} 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 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 = {
@@ -35,16 +38,24 @@ function CreateLowonganKerja() {
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
try {
setIsSubmitting(true);
await lowonganState.create.create(); await lowonganState.create.create();
resetForm(); resetForm();
router.push('/admin/ekonomi/lowongan-kerja-lokal'); 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 (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan tombol kembali */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
variant="subtle" variant="subtle"
onClick={() => router.back()} onClick={() => router.back()}
@@ -53,7 +64,6 @@ function CreateLowonganKerja() {
> >
<IconArrowBack color={colors['blue-button']} size={24} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark"> <Title order={4} ml="sm" c="dark">
Tambah Lowongan Kerja Lokal Tambah Lowongan Kerja Lokal
</Title> </Title>
@@ -70,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)
} }
@@ -79,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)
} }
@@ -88,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)
} }
@@ -97,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)
} }
@@ -106,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)
} }
@@ -115,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)
} }
@@ -150,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"
@@ -160,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

@@ -16,8 +16,7 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -69,7 +68,6 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Lowongan Kerja Lokal</Title> <Title order={4}>Daftar Lowongan Kerja Lokal</Title>
<Tooltip label="Tambah Lowongan Kerja" withArrow>
<Button <Button
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
@@ -80,7 +78,6 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
> >
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip>
</Group> </Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>

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