Compare commits
16 Commits
nico/4-nov
...
fb010bd05a
| Author | SHA1 | Date | |
|---|---|---|---|
| fb010bd05a | |||
| 6c32f3ebdb | |||
| cfe60ed8fe | |||
| 0feeb4de93 | |||
| dc56a329dc | |||
| 9622eb5a9a | |||
| 417a8937f5 | |||
| a9195d30bd | |||
| db8909b9ed | |||
| 569b0d408b | |||
| f66a46f645 | |||
| 40985f961a | |||
| 22424ef53e | |||
| 14b49334ac | |||
| 4a6829c502 | |||
| 60b035749d |
@@ -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"
|
||||||
|
|||||||
@@ -183,17 +183,45 @@ model SdgsDesa {
|
|||||||
|
|
||||||
//========================================= APBDes ========================================= //
|
//========================================= APBDes ========================================= //
|
||||||
model APBDes {
|
model APBDes {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
tahun Int?
|
||||||
jumlah String
|
name String? // misalnya: "APBDes Tahun 2025"
|
||||||
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
deskripsi String?
|
||||||
imageId String?
|
jumlah String? // total keseluruhan (opsional, bisa juga dihitung dari items)
|
||||||
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
items APBDesItem[]
|
||||||
fileId String?
|
image FileStorage? @relation("APBDesImage", fields: [imageId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
imageId String?
|
||||||
updatedAt DateTime @updatedAt
|
file FileStorage? @relation("APBDesFile", fields: [fileId], references: [id])
|
||||||
deletedAt DateTime @default(now())
|
fileId String?
|
||||||
isActive Boolean @default(true)
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime? // opsional, tidak perlu default now()
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
model APBDesItem {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
kode String // contoh: "4", "4.1", "4.1.2"
|
||||||
|
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
||||||
|
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
||||||
|
realisasi Float
|
||||||
|
selisih Float // realisasi - anggaran
|
||||||
|
persentase Float
|
||||||
|
tipe String? // (realisasi / anggaran) * 100
|
||||||
|
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
||||||
|
parentId String? // untuk relasi hierarki
|
||||||
|
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
||||||
|
children APBDesItem[] @relation("APBDesItemParent")
|
||||||
|
apbdesId String
|
||||||
|
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
@@index([kode])
|
||||||
|
@@index([level])
|
||||||
|
@@index([apbdesId])
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= PRESTASI DESA ========================================= //
|
//========================================= PRESTASI DESA ========================================= //
|
||||||
@@ -1942,23 +1970,28 @@ model KeunggulanProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model BeasiswaPendaftar {
|
model BeasiswaPendaftar {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
namaLengkap String
|
namaLengkap String
|
||||||
nik String @unique
|
nis String?
|
||||||
|
kelas String?
|
||||||
|
jenisKelamin JenisKelamin
|
||||||
|
alamatDomisili String?
|
||||||
tempatLahir String
|
tempatLahir String
|
||||||
tanggalLahir DateTime
|
tanggalLahir DateTime
|
||||||
jenisKelamin JenisKelamin
|
namaOrtu String?
|
||||||
kewarganegaraan String
|
nik String @unique
|
||||||
agama Agama
|
pekerjaanOrtu String?
|
||||||
alamatKTP String
|
penghasilan String?
|
||||||
alamatDomisili String?
|
|
||||||
noHp String
|
noHp String
|
||||||
email String @unique
|
kewarganegaraan String?
|
||||||
statusPernikahan StatusPernikahan
|
agama Agama?
|
||||||
|
alamatKTP String?
|
||||||
|
email String? @unique
|
||||||
|
statusPernikahan StatusPernikahan?
|
||||||
ukuranBaju UkuranBaju?
|
ukuranBaju UkuranBaju?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
enum JenisKelamin {
|
enum JenisKelamin {
|
||||||
|
|||||||
@@ -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)">
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function EditEditor({ value, onChange }: EditEditorProps) {
|
|||||||
editor.off('update', updateHandler);
|
editor.off('update', updateHandler);
|
||||||
};
|
};
|
||||||
}, [editor, onChange]);
|
}, [editor, onChange]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RichTextEditor editor={editor}>
|
<RichTextEditor editor={editor}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -5,58 +5,166 @@ import { toast } from "react-toastify";
|
|||||||
import { proxy } from "valtio";
|
import { proxy } from "valtio";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const templateapbDesaForm = z.object({
|
// --- Zod Schema ---
|
||||||
name: z.string().min(1, "Judul minimal 1 karakter"),
|
const ApbdesItemSchema = z.object({
|
||||||
jumlah: z.string().min(1, "Deskripsi minimal 1 karakter"),
|
kode: z.string().min(1),
|
||||||
imageId: z.string().min(1, "File minimal 1"),
|
uraian: z.string().min(1),
|
||||||
fileId: z.string().min(1, "File minimal 1"),
|
anggaran: z.number().min(0),
|
||||||
|
realisasi: z.number().min(0),
|
||||||
|
selisih: z.number(),
|
||||||
|
persentase: z.number().min(0).max(1000), // allow >100% if overbudget
|
||||||
|
level: z.number().int().min(1).max(3),
|
||||||
|
tipe: z.string().min(1), // "pendapatan" | "belanja"
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultapbdesForm = {
|
const ApbdesFormSchema = z.object({
|
||||||
name: "",
|
tahun: z.number().int().min(2000, "Tahun tidak valid"),
|
||||||
jumlah: "",
|
imageId: z.string().min(1, "Gambar wajib diunggah"),
|
||||||
|
fileId: z.string().min(1, "File wajib diunggah"),
|
||||||
|
items: z.array(ApbdesItemSchema).min(1, "Minimal ada 1 item"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Default Form ---
|
||||||
|
const defaultApbdesForm = {
|
||||||
|
tahun: new Date().getFullYear(),
|
||||||
imageId: "",
|
imageId: "",
|
||||||
fileId: "",
|
fileId: "",
|
||||||
|
items: [] as z.infer<typeof ApbdesItemSchema>[],
|
||||||
};
|
};
|
||||||
|
|
||||||
const apbdes = proxy({
|
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
|
||||||
create: {
|
// --- Helper: hitung selisih & persentase otomatis (opsional di frontend) ---
|
||||||
form: { ...defaultapbdesForm },
|
function normalizeItem(item: Partial<z.infer<typeof ApbdesItemSchema>>): z.infer<typeof ApbdesItemSchema> {
|
||||||
loading: false,
|
const anggaran = item.anggaran ?? 0;
|
||||||
async create() {
|
const realisasi = item.realisasi ?? 0;
|
||||||
const cek = templateapbDesaForm.safeParse(apbdes.create.form);
|
|
||||||
if (!cek.success) {
|
// ✅ Formula yang benar
|
||||||
const err = `[${cek.error.issues
|
const selisih = anggaran - realisasi; // positif = sisa anggaran, negatif = over budget
|
||||||
.map((v) => `${v.path.join(".")}`)
|
const persentase = anggaran > 0 ? (realisasi / anggaran) * 100 : 0; // persentase realisasi terhadap anggaran
|
||||||
.join("\n")}] required`;
|
|
||||||
return toast.error(err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
apbdes.create.loading = true;
|
|
||||||
const res = await ApiFetch.api.landingpage.apbdes["create"].post({
|
|
||||||
...apbdes.create.form,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
return {
|
||||||
|
kode: item.kode || "",
|
||||||
|
uraian: item.uraian || "",
|
||||||
|
anggaran,
|
||||||
|
realisasi,
|
||||||
|
selisih,
|
||||||
|
persentase,
|
||||||
|
level: item.level || 1,
|
||||||
|
tipe: item.tipe || "pendapatan",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- State Utama ---
|
||||||
|
const apbdes = proxy({
|
||||||
|
// create: {
|
||||||
|
// form: { ...defaultApbdesForm },
|
||||||
|
// loading: false,
|
||||||
|
|
||||||
|
// addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
// const normalized = normalizeItem(item);
|
||||||
|
// this.form.items.push(normalized);
|
||||||
|
// },
|
||||||
|
|
||||||
|
// removeItem(index: number) {
|
||||||
|
// this.form.items.splice(index, 1);
|
||||||
|
// },
|
||||||
|
|
||||||
|
// updateItem(index: number, updates: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
// const current = this.form.items[index];
|
||||||
|
// if (current) {
|
||||||
|
// const updated = normalizeItem({ ...current, ...updates });
|
||||||
|
// this.form.items[index] = updated;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
|
||||||
|
// reset() {
|
||||||
|
// this.form = { ...defaultApbdesForm };
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async create() {
|
||||||
|
// const parsed = ApbdesFormSchema.safeParse(this.form);
|
||||||
|
// if (!parsed.success) {
|
||||||
|
// const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
|
||||||
|
// toast.error(`Validasi gagal:\n${errors.join("\n")}`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// this.loading = true;
|
||||||
|
// const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
|
||||||
|
|
||||||
|
// if (res.data?.success) {
|
||||||
|
// toast.success("APBDes berhasil dibuat");
|
||||||
|
// apbdes.findMany.load();
|
||||||
|
// this.reset();
|
||||||
|
// } else {
|
||||||
|
// toast.error(res.data?.message || "Gagal membuat APBDes");
|
||||||
|
// }
|
||||||
|
// } catch (error: any) {
|
||||||
|
// console.error("Create APBDes error:", error);
|
||||||
|
// toast.error(error?.message || "Terjadi kesalahan saat membuat APBDes");
|
||||||
|
// } finally {
|
||||||
|
// this.loading = false;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
create: {
|
||||||
|
form: { ...defaultApbdesForm },
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
const normalized = normalizeItem(item);
|
||||||
|
this.form.items.push(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index: number) {
|
||||||
|
this.form.items.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateItem(index: number, updates: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
const current = this.form.items[index];
|
||||||
|
if (current) {
|
||||||
|
const updated = normalizeItem({ ...current, ...updates });
|
||||||
|
this.form.items[index] = updated;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.form = { ...defaultApbdesForm };
|
||||||
|
},
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
const parsed = ApbdesFormSchema.safeParse(this.form);
|
||||||
|
if (!parsed.success) {
|
||||||
|
const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
|
||||||
|
toast.error(`Validasi gagal:\n${errors.join("\n")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await ApiFetch.api.landingpage.apbdes["create"].post(parsed.data);
|
||||||
|
|
||||||
|
if (res.data?.success) {
|
||||||
|
toast.success("APBDes berhasil dibuat");
|
||||||
apbdes.findMany.load();
|
apbdes.findMany.load();
|
||||||
return toast.success("Data berhasil ditambahkan");
|
this.reset();
|
||||||
|
} else {
|
||||||
|
toast.error(res.data?.message || "Gagal membuat APBDes");
|
||||||
}
|
}
|
||||||
return toast.error("Gagal menambahkan data");
|
} catch (error: any) {
|
||||||
} catch (error) {
|
console.error("Create APBDes error:", error);
|
||||||
console.log(error);
|
toast.error(error?.message || "Terjadi kesalahan saat membuat APBDes");
|
||||||
toast.error("Gagal menambahkan data");
|
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.create.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.APBDesGetPayload<{
|
| Prisma.APBDesGetPayload<{
|
||||||
include: {
|
include: { image: true; file: true; items: true };
|
||||||
image: true;
|
|
||||||
file: true;
|
|
||||||
};
|
|
||||||
}>[]
|
}>[]
|
||||||
| null,
|
| null,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -64,194 +172,202 @@ const apbdes = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
|
||||||
apbdes.findMany.loading = true; // Use the full path to access the property
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
|
apbdes.findMany.loading = true;
|
||||||
apbdes.findMany.page = page;
|
apbdes.findMany.page = page;
|
||||||
apbdes.findMany.search = search;
|
apbdes.findMany.search = search;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: any = { page, limit };
|
const query: Record<string, string> = { page: String(page), limit: String(limit) };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.apbdes[
|
const res = await ApiFetch.api.landingpage.apbdes["findMany"].get({ query });
|
||||||
"findMany"
|
|
||||||
].get({
|
if (res.data?.success) {
|
||||||
query
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
|
||||||
apbdes.findMany.data = res.data.data || [];
|
apbdes.findMany.data = res.data.data || [];
|
||||||
apbdes.findMany.total = res.data.total || 0;
|
apbdes.findMany.total = res.data.meta?.total || 0;
|
||||||
apbdes.findMany.totalPages = res.data.totalPages || 1;
|
apbdes.findMany.totalPages = res.data.meta?.totalPages || 1;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to load pegawai:", res.data?.message);
|
|
||||||
apbdes.findMany.data = [];
|
apbdes.findMany.data = [];
|
||||||
apbdes.findMany.total = 0;
|
apbdes.findMany.total = 0;
|
||||||
apbdes.findMany.totalPages = 1;
|
apbdes.findMany.totalPages = 1;
|
||||||
|
toast.error(res.data?.message || "Gagal memuat data");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading pegawai:", error);
|
console.error("FindMany error:", error);
|
||||||
apbdes.findMany.data = [];
|
apbdes.findMany.data = [];
|
||||||
apbdes.findMany.total = 0;
|
apbdes.findMany.total = 0;
|
||||||
apbdes.findMany.totalPages = 1;
|
apbdes.findMany.totalPages = 1;
|
||||||
|
toast.error("Gagal memuat daftar APBDes");
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.findMany.loading = false;
|
apbdes.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as Prisma.APBDesGetPayload<{
|
data: null as
|
||||||
include: {
|
| Prisma.APBDesGetPayload<{
|
||||||
image: true;
|
include: { image: true; file: true; items: true };
|
||||||
file: true;
|
}>
|
||||||
};
|
| null,
|
||||||
}> | null,
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
|
if (!id || id.trim() === '') {
|
||||||
|
this.data = null;
|
||||||
|
this.error = "ID tidak valid";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/landingpage/apbdes/${id}`);
|
// Pastikan URL-nya benar
|
||||||
if (res.ok) {
|
const url = `/api/landingpage/apbdes/${id}`;
|
||||||
const data = await res.json();
|
console.log("🌐 Fetching:", url);
|
||||||
apbdes.findUnique.data = data.data ?? null;
|
|
||||||
|
// Gunakan fetch biasa atau ApiFetch dengan cara yang benar
|
||||||
|
const response = await fetch(url);
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.log("📦 Response:", res);
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
this.data = res.data;
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch data", res.status, res.statusText);
|
this.data = null;
|
||||||
apbdes.findUnique.data = null;
|
this.error = res.message || "Gagal memuat detail APBDes";
|
||||||
|
toast.error(this.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error);
|
console.error("❌ FindUnique error:", error);
|
||||||
apbdes.findUnique.data = null;
|
this.data = null;
|
||||||
|
this.error = "Gagal memuat detail APBDes";
|
||||||
|
toast.error(this.error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: {
|
delete: {
|
||||||
loading: false,
|
loading: false,
|
||||||
async byId(id: string) {
|
async byId(id: string) {
|
||||||
if (!id) return toast.warn("ID tidak valid");
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apbdes.delete.loading = true;
|
this.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.apbdes as any)["del"][id].delete();
|
||||||
|
|
||||||
const response = await fetch(`/api/landingpage/apbdes/del/${id}`, {
|
if (res.data?.success) {
|
||||||
method: "DELETE",
|
toast.success("APBDes berhasil dihapus");
|
||||||
headers: {
|
apbdes.findMany.load();
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok && result?.success) {
|
|
||||||
toast.success(result.message || "apbdes berhasil dihapus");
|
|
||||||
await apbdes.findMany.load(); // refresh list
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(result?.message || "Gagal menghapus apbdes");
|
toast.error(res.data?.message || "Gagal menghapus APBDes");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Gagal delete:", error);
|
console.error("Delete error:", error);
|
||||||
toast.error("Terjadi kesalahan saat menghapus apbdes");
|
toast.error(error?.message || "Terjadi kesalahan saat menghapus");
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.delete.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
edit: {
|
edit: {
|
||||||
id: "",
|
id: "",
|
||||||
form: { ...defaultapbdesForm },
|
form: { ...defaultApbdesForm },
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
async load(id: string) {
|
async load(id: string) {
|
||||||
if (!id) {
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
toast.warn("ID tidak valid");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apbdes.edit.loading = true;
|
this.loading = true;
|
||||||
|
const res = await (ApiFetch.api.landingpage.apbdes as any)[id].get();
|
||||||
|
|
||||||
const response = await fetch(`/api/landingpage/apbdes/${id}`, {
|
if (res.data?.success) {
|
||||||
method: "GET",
|
const data = res.data.data;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
if (result?.success) {
|
|
||||||
const data = result.data;
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name,
|
tahun: data.tahun || new Date().getFullYear(),
|
||||||
jumlah: data.jumlah,
|
imageId: data.imageId || "",
|
||||||
imageId: data.imageId,
|
fileId: data.fileId || "",
|
||||||
fileId: data.fileId,
|
items: (data.items || []).map((item: any) => ({
|
||||||
|
kode: item.kode,
|
||||||
|
uraian: item.uraian,
|
||||||
|
anggaran: item.anggaran,
|
||||||
|
realisasi: item.realisasi,
|
||||||
|
selisih: item.selisih,
|
||||||
|
persentase: item.persentase,
|
||||||
|
level: item.level,
|
||||||
|
tipe: item.tipe,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result?.message || "Gagal memuat data");
|
throw new Error(res.data?.message || "Gagal memuat data");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error loading apbdes:", error);
|
console.error("Edit load error:", error);
|
||||||
toast.error(
|
toast.error(error.message || "Gagal memuat data untuk diedit");
|
||||||
error instanceof Error ? error.message : "Gagal memuat data"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.edit.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
const cek = templateapbDesaForm.safeParse(apbdes.edit.form);
|
const parsed = ApbdesFormSchema.safeParse(this.form);
|
||||||
if (!cek.success) {
|
if (!parsed.success) {
|
||||||
const err = `[${cek.error.issues
|
const errors = parsed.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`);
|
||||||
.map((v) => `${v.path.join(".")}`)
|
toast.error(`Validasi gagal:\n${errors.join("\n")}`);
|
||||||
.join("\n")}] required`;
|
return false;
|
||||||
return toast.error(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apbdes.edit.loading = true;
|
this.loading = true;
|
||||||
const response = await fetch(`/api/landingpage/apbdes/${this.id}`, {
|
// Include the ID in the request body
|
||||||
method: "PUT",
|
const requestData = {
|
||||||
headers: {
|
...parsed.data,
|
||||||
"Content-Type": "application/json",
|
id: this.id, // Add the ID to the request body
|
||||||
},
|
};
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.form.name,
|
const res = await (ApiFetch.api.landingpage.apbdes as any)[this.id].put(requestData);
|
||||||
jumlah: this.form.jumlah,
|
|
||||||
imageId: this.form.imageId,
|
if (res.data?.success) {
|
||||||
fileId: this.form.fileId,
|
toast.success("APBDes berhasil diperbarui");
|
||||||
}),
|
apbdes.findMany.load();
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
errorData.message || `HTTP error! status: ${response.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
toast.success("Berhasil update apbdes");
|
|
||||||
await apbdes.findMany.load(); // refresh list
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.message || "Gagal mengupdate apbdes");
|
throw new Error(res.data?.message || "Gagal memperbarui APBDes");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error updating apbdes:", error);
|
console.error("Update error:", error);
|
||||||
toast.error(
|
toast.error(error.message || "Gagal memperbarui APBDes");
|
||||||
error instanceof Error ? error.message : "Gagal mengupdate apbdes"
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
apbdes.edit.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addItem(item: Partial<z.infer<typeof ApbdesItemSchema>>) {
|
||||||
|
const normalized = normalizeItem(item);
|
||||||
|
this.form.items.push(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index: number) {
|
||||||
|
this.form.items.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
apbdes.edit.id = "";
|
this.id = "";
|
||||||
apbdes.edit.form = { ...defaultapbdesForm };
|
this.form = { ...defaultApbdesForm };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apbdes;
|
export default apbdes;
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -9,34 +9,32 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const templateBeasiswaPendaftar = z.object({
|
const templateBeasiswaPendaftar = z.object({
|
||||||
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
namaLengkap: z.string().min(1, "Nama harus diisi"),
|
||||||
nik: z.string().min(1, "NIK harus diisi"),
|
nis: z.string().min(1, "NIS harus diisi"),
|
||||||
|
kelas: z.string().min(1, "Kelas harus diisi"),
|
||||||
|
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
|
||||||
|
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
|
||||||
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
|
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
|
||||||
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
|
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
|
||||||
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
|
namaOrtu: z.string().min(1, "Nama ortu harus diisi"),
|
||||||
kewarganegaraan: z.string().min(1, "Kewarganegaraan harus diisi"),
|
nik: z.string().min(1, "NIK harus diisi"),
|
||||||
agama: z.string().min(1, "Agama harus diisi"),
|
pekerjaanOrtu: z.string().min(1, "Pekerjaan ortu harus diisi"),
|
||||||
alamatKTP: z.string().min(1, "Alamat KTP harus diisi"),
|
penghasilan: z.string().min(1, "Penghasilan ortu harus diisi"),
|
||||||
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
|
|
||||||
noHp: z.string().min(1, "No HP harus diisi"),
|
noHp: z.string().min(1, "No HP harus diisi"),
|
||||||
email: z.string().min(1, "Email harus diisi"),
|
|
||||||
statusPernikahan: z.string().min(1, "Status pernikahan harus diisi"),
|
|
||||||
ukuranBaju: z.string().min(1, "Ukuran baju harus diisi"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultBeasiswaPendaftar = {
|
const defaultBeasiswaPendaftar = {
|
||||||
namaLengkap: "",
|
namaLengkap: "",
|
||||||
nik: "",
|
nis: "",
|
||||||
|
kelas: "",
|
||||||
|
jenisKelamin: "",
|
||||||
|
alamatDomisili: "",
|
||||||
tempatLahir: "",
|
tempatLahir: "",
|
||||||
tanggalLahir: "",
|
tanggalLahir: "",
|
||||||
jenisKelamin: "",
|
namaOrtu: "",
|
||||||
kewarganegaraan: "",
|
nik: "",
|
||||||
agama: "",
|
pekerjaanOrtu: "",
|
||||||
alamatKTP: "",
|
penghasilan: "",
|
||||||
alamatDomisili: "",
|
|
||||||
noHp: "",
|
noHp: "",
|
||||||
email: "",
|
|
||||||
statusPernikahan: "",
|
|
||||||
ukuranBaju: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const beasiswaPendaftar = proxy({
|
const beasiswaPendaftar = proxy({
|
||||||
@@ -200,18 +198,17 @@ const beasiswaPendaftar = proxy({
|
|||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
namaLengkap: data.namaLengkap,
|
namaLengkap: data.namaLengkap,
|
||||||
nik: data.nik,
|
nis: data.nis,
|
||||||
|
kelas: data.kelas,
|
||||||
|
jenisKelamin: data.jenisKelamin,
|
||||||
|
alamatDomisili: data.alamatDomisili,
|
||||||
tempatLahir: data.tempatLahir,
|
tempatLahir: data.tempatLahir,
|
||||||
tanggalLahir: data.tanggalLahir,
|
tanggalLahir: data.tanggalLahir,
|
||||||
jenisKelamin: data.jenisKelamin,
|
namaOrtu: data.namaOrtu,
|
||||||
kewarganegaraan: data.kewarganegaraan,
|
nik: data.nik,
|
||||||
agama: data.agama,
|
pekerjaanOrtu: data.pekerjaanOrtu,
|
||||||
alamatKTP: data.alamatKTP,
|
penghasilan: data.penghasilan,
|
||||||
alamatDomisili: data.alamatDomisili,
|
|
||||||
noHp: data.noHp,
|
noHp: data.noHp,
|
||||||
email: data.email,
|
|
||||||
statusPernikahan: data.statusPernikahan,
|
|
||||||
ukuranBaju: data.ukuranBaju,
|
|
||||||
};
|
};
|
||||||
return data; // Return the loaded data
|
return data; // Return the loaded data
|
||||||
} else {
|
} else {
|
||||||
@@ -249,17 +246,17 @@ const beasiswaPendaftar = proxy({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
namaLengkap: this.form.namaLengkap,
|
namaLengkap: this.form.namaLengkap,
|
||||||
nik: this.form.nik,
|
nis: this.form.nis,
|
||||||
tanggalLahir: this.form.tanggalLahir,
|
kelas: this.form.kelas,
|
||||||
jenisKelamin: this.form.jenisKelamin,
|
jenisKelamin: this.form.jenisKelamin,
|
||||||
kewarganegaraan: this.form.kewarganegaraan,
|
|
||||||
agama: this.form.agama,
|
|
||||||
alamatKTP: this.form.alamatKTP,
|
|
||||||
alamatDomisili: this.form.alamatDomisili,
|
alamatDomisili: this.form.alamatDomisili,
|
||||||
|
tempatLahir: this.form.tempatLahir,
|
||||||
|
tanggalLahir: this.form.tanggalLahir,
|
||||||
|
namaOrtu: this.form.namaOrtu,
|
||||||
|
nik: this.form.nik,
|
||||||
|
pekerjaanOrtu: this.form.pekerjaanOrtu,
|
||||||
|
penghasilan: this.form.penghasilan,
|
||||||
noHp: this.form.noHp,
|
noHp: this.form.noHp,
|
||||||
email: this.form.email,
|
|
||||||
statusPernikahan: this.form.statusPernikahan,
|
|
||||||
ukuranBaju: this.form.ukuranBaju,
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconBuildingStore, IconFileText, IconSparkles, IconUsers, IconUsersPlus } 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 { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,36 +14,31 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
label: "Pelayanan Surat Keterangan",
|
label: "Pelayanan Surat Keterangan",
|
||||||
value: "pelayanansuratketerangan",
|
value: "pelayanansuratketerangan",
|
||||||
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
href: "/admin/desa/layanan/pelayanan_surat_keterangan",
|
||||||
icon: <IconFileText size={18} stroke={1.8} />,
|
icon: <IconFileText size={18} stroke={1.8} />
|
||||||
tooltip: "Layanan terkait surat keterangan resmi desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Perizinan Berusaha",
|
label: "Pelayanan Perizinan Berusaha",
|
||||||
value: "pelayananperizinanusaha",
|
value: "pelayananperizinanusaha",
|
||||||
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
href: "/admin/desa/layanan/pelayanan_perizinan_berusaha",
|
||||||
icon: <IconBuildingStore size={18} stroke={1.8} />,
|
icon: <IconBuildingStore size={18} stroke={1.8} />
|
||||||
tooltip: "Layanan untuk izin usaha masyarakat"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Telunjuk Sakti Desa",
|
label: "Pelayanan Telunjuk Sakti Desa",
|
||||||
value: "pelayanantelunjuksaktidesa",
|
value: "pelayanantelunjuksaktidesa",
|
||||||
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
href: "/admin/desa/layanan/pelayanan_telunjuk_sakti_desa",
|
||||||
icon: <IconSparkles size={18} stroke={1.8} />,
|
icon: <IconSparkles size={18} stroke={1.8} />
|
||||||
tooltip: "Layanan inovasi khusus desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Pelayanan Penduduk Non-Permanent",
|
label: "Pelayanan Penduduk Non-Permanent",
|
||||||
value: "pelayanannonpermanent",
|
value: "pelayanannonpermanent",
|
||||||
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
|
||||||
icon: <IconUsers size={18} stroke={1.8} />,
|
icon: <IconUsers size={18} stroke={1.8} />
|
||||||
tooltip: "Pendataan penduduk non-permanent"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Ajukan Permohonan",
|
label: "Ajukan Permohonan",
|
||||||
value: "ajukanpermohonan",
|
value: "ajukanpermohonan",
|
||||||
href: "/admin/desa/layanan/ajukan_permohonan",
|
href: "/admin/desa/layanan/ajukan_permohonan",
|
||||||
icon: <IconUsersPlus size={18} stroke={1.8} />,
|
icon: <IconUsersPlus size={18} stroke={1.8} />
|
||||||
tooltip: "Ajukan permohonan"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -91,14 +86,8 @@ function LayoutTabsLayanan({ 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={{
|
||||||
@@ -109,7 +98,6 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconCategory, IconNews } 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 { IconNews, IconCategory } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -15,15 +15,13 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
|||||||
label: "List Berita",
|
label: "List Berita",
|
||||||
value: "list_berita",
|
value: "list_berita",
|
||||||
href: "/admin/desa/berita/list-berita",
|
href: "/admin/desa/berita/list-berita",
|
||||||
icon: <IconNews size={18} stroke={1.8} />,
|
icon: <IconNews size={18} stroke={1.8} />
|
||||||
tooltip: "Lihat dan kelola semua berita desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Berita",
|
label: "Kategori Berita",
|
||||||
value: "kategori_berita",
|
value: "kategori_berita",
|
||||||
href: "/admin/desa/berita/kategori-berita",
|
href: "/admin/desa/berita/kategori-berita",
|
||||||
icon: <IconCategory size={18} stroke={1.8} />,
|
icon: <IconCategory size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola kategori berita desa"
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -71,46 +69,39 @@ function LayoutTabsBerita({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<Tooltip
|
<TabsTab
|
||||||
key={i}
|
key={i}
|
||||||
label={tab.tooltip}
|
value={tab.value}
|
||||||
position="bottom"
|
leftSection={tab.icon}
|
||||||
withArrow
|
style={{
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TabsTab
|
{tab.label}
|
||||||
value={tab.value}
|
</TabsTab>
|
||||||
leftSection={tab.icon}
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</TabsTab>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsPanel
|
||||||
key={i}
|
key={i}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
style={{
|
style={{
|
||||||
padding: "1.5rem",
|
padding: "1.5rem",
|
||||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Konten dummy, bisa diganti sesuai routing */}
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
</TabsPanel>
|
</TabsPanel>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Stack>
|
</Stack >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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';
|
||||||
@@ -23,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: '',
|
||||||
@@ -39,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);
|
||||||
@@ -56,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,
|
||||||
@@ -70,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -77,7 +95,6 @@ function EditKategoriBerita() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back Button + Title */}
|
{/* Back Button + Title */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -86,7 +103,6 @@ function EditKategoriBerita() {
|
|||||||
>
|
>
|
||||||
<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 Kategori Berita
|
Edit Kategori Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -112,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"
|
||||||
@@ -122,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>
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
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 = {
|
||||||
@@ -26,16 +29,23 @@ function CreateKategoriBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
setIsSubmitting(true);
|
||||||
resetForm();
|
try {
|
||||||
router.push('/admin/desa/berita/kategori-berita');
|
await createState.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/berita/kategori-berita');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating kategori berita:', error);
|
||||||
|
toast.error('Gagal menambahkan kategori berita');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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()}
|
||||||
@@ -44,7 +54,6 @@ function CreateKategoriBerita() {
|
|||||||
>
|
>
|
||||||
<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 Kategori Berita
|
Tambah Kategori Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -63,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"
|
||||||
@@ -79,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>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
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';
|
||||||
@@ -86,7 +85,6 @@ function ListKategoriBerita({ 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 Kategori Berita</Title>
|
<Title order={4}>Daftar Kategori Berita</Title>
|
||||||
<Tooltip label="Tambah Kategori Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -97,7 +95,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
@@ -123,7 +120,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Tooltip label="Edit Kategori Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
@@ -135,10 +131,8 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Tooltip label="Hapus Kategori Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -150,7 +144,6 @@ function ListKategoriBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Loader
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Dropzone } from "@mantine/dropzone";
|
import { Dropzone } from "@mantine/dropzone";
|
||||||
import {
|
import {
|
||||||
@@ -45,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();
|
||||||
@@ -64,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);
|
||||||
}
|
}
|
||||||
@@ -83,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,
|
||||||
@@ -109,23 +131,36 @@ function EditBerita() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating berita:", error);
|
console.error("Error updating berita:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui berita");
|
toast.error("Terjadi kesalahan saat memperbarui berita");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
judul: originalData.judul,
|
||||||
|
deskripsi: originalData.deskripsi,
|
||||||
|
kategoriBeritaId: originalData.kategoriBeritaId,
|
||||||
|
content: originalData.content,
|
||||||
|
imageId: originalData.imageId,
|
||||||
|
});
|
||||||
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
|
setFile(null);
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Berita
|
Edit Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -219,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"
|
||||||
@@ -238,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>
|
||||||
@@ -257,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>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useProxy } from 'valtio/utils';
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } 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';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
function DetailBerita() {
|
function DetailBerita() {
|
||||||
const beritaState = useProxy(stateDashboardBerita);
|
const beritaState = useProxy(stateDashboardBerita);
|
||||||
@@ -111,7 +111,6 @@ function DetailBerita() {
|
|||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Berita" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -124,9 +123,7 @@ function DetailBerita() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Berita" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/desa/berita/list-berita/${data.id}/edit`)}
|
||||||
@@ -136,7 +133,6 @@ function DetailBerita() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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';
|
||||||
@@ -29,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();
|
||||||
@@ -47,42 +49,48 @@ export default function CreateBerita() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
try {
|
||||||
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
setIsSubmitting(true);
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
|
}
|
||||||
|
|
||||||
|
beritaState.berita.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
|
await beritaState.berita.create.create();
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/berita/list-berita');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating berita:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat membuat berita');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
|
||||||
}
|
|
||||||
|
|
||||||
beritaState.berita.create.form.imageId = uploaded.id;
|
|
||||||
|
|
||||||
await beritaState.berita.create.create();
|
|
||||||
|
|
||||||
resetForm();
|
|
||||||
router.push('/admin/desa/berita/list-berita');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header dengan tombol kembali */}
|
{/* Header dengan tombol kembali */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Berita
|
Tambah Berita
|
||||||
</Title>
|
</Title>
|
||||||
@@ -100,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
|
||||||
/>
|
/>
|
||||||
@@ -112,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(
|
||||||
@@ -157,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"
|
||||||
>
|
>
|
||||||
@@ -178,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"
|
||||||
@@ -190,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>
|
||||||
@@ -207,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"
|
||||||
@@ -217,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>
|
||||||
|
|||||||
@@ -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 { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
@@ -68,7 +67,6 @@ function ListBerita({ 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 Berita</Title>
|
<Title order={4}>Daftar Berita</Title>
|
||||||
<Tooltip label="Tambah Berita" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconCircleDashedPlus size={18} />}
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -77,7 +75,6 @@ function ListBerita({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
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 { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
@@ -103,7 +102,6 @@ export default function ListImage() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify="space-between" align="center" pt="xs">
|
<Group justify="space-between" align="center" pt="xs">
|
||||||
<Tooltip label="Hapus foto" withArrow>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -116,7 +114,6 @@ export default function ListImage() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconPhoto, IconVideo } 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 { IconPhoto, IconVideo } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,15 +14,13 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
label: "Foto",
|
label: "Foto",
|
||||||
value: "foto",
|
value: "foto",
|
||||||
href: "/admin/desa/gallery/foto",
|
href: "/admin/desa/gallery/foto",
|
||||||
icon: <IconPhoto size={18} stroke={1.8} />,
|
icon: <IconPhoto size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola foto-foto galeri desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Video",
|
label: "Video",
|
||||||
value: "video",
|
value: "video",
|
||||||
href: "/admin/desa/gallery/video",
|
href: "/admin/desa/gallery/video",
|
||||||
icon: <IconVideo size={18} stroke={1.8} />,
|
icon: <IconVideo size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola video galeri desa"
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -70,25 +68,18 @@ function LayoutTabsGallery({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<Tooltip
|
<TabsTab
|
||||||
key={i}
|
key={i}
|
||||||
label={tab.tooltip}
|
value={tab.value}
|
||||||
position="bottom"
|
leftSection={tab.icon}
|
||||||
withArrow
|
style={{
|
||||||
transitionProps={{ transition: 'pop', duration: 200 }}
|
fontWeight: 600,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TabsTab
|
{tab.label}
|
||||||
value={tab.value}
|
</TabsTab>
|
||||||
leftSection={tab.icon}
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</TabsTab>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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,
|
||||||
@@ -11,11 +12,11 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
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 { 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 { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
|
||||||
@@ -25,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: '',
|
||||||
@@ -45,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);
|
||||||
@@ -62,25 +76,42 @@ function EditVideo() {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleResetForm = () => {
|
||||||
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
|
setFormData({
|
||||||
if (!converted) {
|
name: originalData.name,
|
||||||
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
deskripsi: originalData.deskripsi,
|
||||||
return;
|
linkVideo: originalData.linkVideo,
|
||||||
}
|
});
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
videoState.update.form = {
|
setIsSubmitting(true);
|
||||||
name: formData.name,
|
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
|
||||||
deskripsi: formData.deskripsi,
|
if (!converted) {
|
||||||
linkVideo: formData.linkVideo,
|
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
|
||||||
};
|
return;
|
||||||
await videoState.update.update();
|
}
|
||||||
toast.success('Video berhasil diperbarui!');
|
|
||||||
router.push('/admin/desa/gallery/video');
|
try {
|
||||||
|
videoState.update.form = {
|
||||||
|
name: formData.name,
|
||||||
|
deskripsi: formData.deskripsi,
|
||||||
|
linkVideo: formData.linkVideo,
|
||||||
|
};
|
||||||
|
await videoState.update.update();
|
||||||
|
toast.success('Video berhasil diperbarui!');
|
||||||
|
router.push('/admin/desa/gallery/video');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating video:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat memperbarui video');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating video:', error);
|
console.error('Error updating video:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui video');
|
toast.error('Terjadi kesalahan saat memperbarui video');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,16 +120,14 @@ function EditVideo() {
|
|||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Video
|
Edit Video
|
||||||
</Title>
|
</Title>
|
||||||
@@ -130,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%"
|
||||||
@@ -138,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>
|
||||||
@@ -154,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"
|
||||||
@@ -164,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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
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 { 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';
|
||||||
@@ -111,7 +111,6 @@ function DetailVideo() {
|
|||||||
|
|
||||||
{/* Tombol Aksi */}
|
{/* Tombol Aksi */}
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Video" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -124,9 +123,7 @@ function DetailVideo() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Video" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -138,7 +135,6 @@ function DetailVideo() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -11,9 +12,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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';
|
||||||
@@ -25,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 = {
|
||||||
@@ -36,31 +38,37 @@ function CreateVideo() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!embedLink) {
|
try {
|
||||||
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
setIsSubmitting(true);
|
||||||
return;
|
if (!embedLink) {
|
||||||
}
|
toast.error('Link YouTube tidak valid. Pastikan formatnya benar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
videoState.create.form.linkVideo = embedLink;
|
videoState.create.form.linkVideo = embedLink;
|
||||||
await videoState.create.create();
|
await videoState.create.create();
|
||||||
resetForm();
|
resetForm();
|
||||||
router.push('/admin/desa/gallery/video');
|
router.push('/admin/desa/gallery/video');
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating video:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat menambahkan video");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header Back Button + Title */}
|
{/* Header Back Button + Title */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Video
|
Tambah Video
|
||||||
</Title>
|
</Title>
|
||||||
@@ -80,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;
|
||||||
}}
|
}}
|
||||||
@@ -91,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,
|
||||||
@@ -109,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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -128,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"
|
||||||
@@ -138,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>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -74,7 +73,6 @@ function ListVideo({ 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 Video</Title>
|
<Title order={4}>Daftar Video</Title>
|
||||||
<Tooltip label="Tambah Video Baru" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -83,7 +81,6 @@ function ListVideo({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
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';
|
||||||
@@ -24,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: '',
|
||||||
@@ -51,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);
|
||||||
@@ -69,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,
|
||||||
@@ -80,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,11 +118,9 @@ function EditAjukanPermohonan() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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 Ajukan Permohonan
|
Edit Ajukan Permohonan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -156,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"
|
||||||
@@ -166,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>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -121,7 +120,6 @@ function DetailAjukanPermohonan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -135,9 +133,7 @@ function DetailAjukanPermohonan() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -151,7 +147,6 @@ function DetailAjukanPermohonan() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -8,12 +8,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';
|
||||||
@@ -33,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 () => {
|
||||||
@@ -46,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);
|
||||||
@@ -58,41 +70,55 @@ function EditPelayananPendudukNonPermanent() {
|
|||||||
|
|
||||||
const handleChange =
|
const handleChange =
|
||||||
(field: keyof typeof formData) =>
|
(field: keyof typeof formData) =>
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name,
|
||||||
|
deskripsi: originalData.deskripsi,
|
||||||
|
});
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!statePendudukNonPermanent.findById.data) return;
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
if (!statePendudukNonPermanent.findById.data) return;
|
||||||
|
|
||||||
// Update global state hanya di submit
|
// Update global state hanya di submit
|
||||||
const updated = {
|
const updated = {
|
||||||
...statePendudukNonPermanent.findById.data,
|
...statePendudukNonPermanent.findById.data,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
deskripsi: formData.deskripsi,
|
deskripsi: formData.deskripsi,
|
||||||
};
|
};
|
||||||
|
|
||||||
await statePendudukNonPermanent.update.update(updated);
|
await statePendudukNonPermanent.update.update(updated);
|
||||||
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
|
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating data:', error);
|
||||||
|
toast.error('Gagal memuat data pelayanan penduduk non permanent');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Pelayanan Penduduk Non Permanent
|
Edit Pelayanan Penduduk Non Permanent
|
||||||
</Title>
|
</Title>
|
||||||
@@ -130,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>
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
@@ -51,7 +50,6 @@ function PelayananPendudukNonPermanent() {
|
|||||||
</Title>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Data Pelayanan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -65,7 +63,6 @@ function PelayananPendudukNonPermanent() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
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';
|
||||||
@@ -35,13 +35,21 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
link: '',
|
link: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
deskripsi: '',
|
||||||
|
link: '',
|
||||||
|
});
|
||||||
|
|
||||||
// Load data detail
|
// Load data detail
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error("ID tidak valid");
|
toast.error("ID tidak valid");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -53,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");
|
||||||
}
|
}
|
||||||
@@ -63,10 +77,10 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
|
||||||
const handleChange =
|
const handleChange =
|
||||||
(field: keyof typeof formData) =>
|
(field: keyof typeof formData) =>
|
||||||
@@ -77,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,16 +127,14 @@ function EditPelayananPerizinanBerusaha() {
|
|||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{/* 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()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
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 Pelayanan Perizinan Berusaha
|
Edit Pelayanan Perizinan Berusaha
|
||||||
</Title>
|
</Title>
|
||||||
@@ -150,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>
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ import {
|
|||||||
StepperCompleted,
|
StepperCompleted,
|
||||||
StepperStep,
|
StepperStep,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import stateLayananDesa from '../../../_state/desa/layananDesa';
|
||||||
|
|
||||||
function PerizinanBerusaha() {
|
function PerizinanBerusaha() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -85,7 +84,6 @@ function PerizinanBerusaha() {
|
|||||||
</Title>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Data Perizinan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -99,7 +97,6 @@ function PerizinanBerusaha() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +1,280 @@
|
|||||||
'use client'
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
'use client';
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
|
Loader,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
|
||||||
} 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 { 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 * 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">
|
||||||
<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 Surat Keterangan
|
Edit Surat Keterangan
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '50%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
@@ -131,154 +284,66 @@ function EditSuratKeterangan() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Input nama */}
|
{/* Nama Surat */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Surat Keterangan"
|
label="Nama Surat Keterangan"
|
||||||
placeholder="Masukkan nama surat keterangan"
|
placeholder="Masukkan nama surat keterangan"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
onChange={handleNameChange}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Input deskripsi */}
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="sm" fw="bold" mb={6}>
|
<Text fz="sm" fw="bold" mb={6}>
|
||||||
Konten
|
Konten
|
||||||
</Text>
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={formData.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(htmlContent) =>
|
onChange={handleDeskripsiChange}
|
||||||
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Upload Gambar 1 */}
|
{/* Gambar 1 */}
|
||||||
<Box>
|
<FileUploader
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
title="Gambar Konten Pelayanan"
|
||||||
Gambar Konten Pelayanan
|
file={file}
|
||||||
</Text>
|
setFile={setFile}
|
||||||
<Dropzone
|
preview={previewImage}
|
||||||
onDrop={(files) => {
|
setPreview={setPreviewImage}
|
||||||
const selectedFile = files[0];
|
/>
|
||||||
if (selectedFile) {
|
|
||||||
setFile(selectedFile);
|
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
|
||||||
maxSize={5 * 1024 ** 2}
|
|
||||||
accept={{ 'image/*': [] }}
|
|
||||||
radius="md"
|
|
||||||
p="xl"
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
<Stack gap="xs" align="center">
|
|
||||||
<Text size="md" fw={500}>
|
|
||||||
Seret gambar atau klik untuk memilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format gambar wajib
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage && (
|
{/* Gambar 2 */}
|
||||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
<FileUploader
|
||||||
<Image
|
title="Gambar Alur Pelayanan Surat"
|
||||||
src={previewImage}
|
file={file2}
|
||||||
alt="Preview Gambar 1"
|
setFile={setFile2}
|
||||||
radius="md"
|
preview={previewImage2}
|
||||||
style={{
|
setPreview={setPreviewImage2}
|
||||||
maxHeight: 220,
|
/>
|
||||||
objectFit: 'contain',
|
|
||||||
border: `1px solid ${colors['blue-button']}`,
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Upload Gambar 2 */}
|
|
||||||
<Box>
|
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
|
||||||
Gambar Alur Pelayanan Surat
|
|
||||||
</Text>
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => {
|
|
||||||
const selectedFile = files[0];
|
|
||||||
if (selectedFile) {
|
|
||||||
setFile2(selectedFile);
|
|
||||||
setPreviewImage2(URL.createObjectURL(selectedFile));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
|
||||||
maxSize={5 * 1024 ** 2}
|
|
||||||
accept={{ 'image/*': [] }}
|
|
||||||
radius="md"
|
|
||||||
p="xl"
|
|
||||||
>
|
|
||||||
<Group justify="center" gap="xl" mih={180}>
|
|
||||||
<Dropzone.Accept>
|
|
||||||
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
|
|
||||||
</Dropzone.Accept>
|
|
||||||
<Dropzone.Reject>
|
|
||||||
<IconX size={48} color="red" stroke={1.5} />
|
|
||||||
</Dropzone.Reject>
|
|
||||||
<Dropzone.Idle>
|
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
|
||||||
</Dropzone.Idle>
|
|
||||||
<Stack gap="xs" align="center">
|
|
||||||
<Text size="md" fw={500}>
|
|
||||||
Seret gambar atau klik untuk memilih file
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Maksimal 5MB, format gambar wajib
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
{previewImage2 && (
|
|
||||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Image
|
|
||||||
src={previewImage2}
|
|
||||||
alt="Preview Gambar 2"
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
maxHeight: 220,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: `1px solid ${colors['blue-button']}`,
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
disabled={isSubmitting}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
boxShadow: '0 4px 15px rgba(79,172,254,0.4)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Simpan
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -287,4 +352,4 @@ function EditSuratKeterangan() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditSuratKeterangan;
|
export default EditSuratKeterangan;
|
||||||
@@ -10,8 +10,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';
|
||||||
@@ -142,7 +141,6 @@ function DetailSuratKeterangan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -156,9 +154,7 @@ function DetailSuratKeterangan() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Surat" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -172,7 +168,6 @@ function DetailSuratKeterangan() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ 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
|
||||||
Tooltip,
|
|
||||||
} 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,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 = {
|
||||||
@@ -46,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,
|
||||||
@@ -78,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,11 +90,9 @@ function CreateSuratKeterangan() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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 Surat Keterangan
|
Tambah Surat Keterangan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -106,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"
|
||||||
@@ -143,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"
|
||||||
>
|
>
|
||||||
@@ -164,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"
|
||||||
@@ -172,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>
|
||||||
@@ -193,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"
|
||||||
>
|
>
|
||||||
@@ -214,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"
|
||||||
@@ -222,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">
|
||||||
@@ -232,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"
|
||||||
@@ -242,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>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -82,7 +81,6 @@ function ListSuratKeterangan({ 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 Surat Keterangan</Title>
|
<Title order={4}>List Surat Keterangan</Title>
|
||||||
<Tooltip label="Tambah Surat Keterangan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -93,7 +91,6 @@ function ListSuratKeterangan({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
|
|||||||
@@ -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,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: '',
|
||||||
@@ -29,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 () => {
|
||||||
@@ -43,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);
|
||||||
@@ -61,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,
|
||||||
@@ -74,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,11 +105,9 @@ function EditPelayananTelunjukSakti() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back Button + Title */}
|
{/* Back Button + Title */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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 Pelayanan Telunjuk Sakti Desa
|
Edit Pelayanan Telunjuk Sakti Desa
|
||||||
</Title>
|
</Title>
|
||||||
@@ -128,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"
|
||||||
@@ -138,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>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -130,7 +129,6 @@ function DetailPelayananTelunjukSakti() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Layanan" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -144,9 +142,7 @@ function DetailPelayananTelunjukSakti() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Layanan" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -160,7 +156,6 @@ function DetailPelayananTelunjukSakti() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -6,20 +6,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 { 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 = {
|
||||||
@@ -31,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');
|
||||||
@@ -38,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,11 +50,9 @@ function CreatePelayananTelunjukDesa() {
|
|||||||
<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 Pelayanan Telunjuk Sakti Desa
|
Tambah Pelayanan Telunjuk Sakti Desa
|
||||||
</Title>
|
</Title>
|
||||||
@@ -67,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;
|
||||||
}}
|
}}
|
||||||
@@ -78,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;
|
||||||
}}
|
}}
|
||||||
@@ -89,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;
|
||||||
}}
|
}}
|
||||||
@@ -100,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"
|
||||||
@@ -110,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>
|
||||||
|
|||||||
@@ -1,159 +1,3 @@
|
|||||||
// /* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
// 'use client'
|
|
||||||
// import colors from '@/con/colors';
|
|
||||||
// import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
|
||||||
// import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
|
||||||
// import { useRouter } from 'next/navigation';
|
|
||||||
// import { useEffect, useMemo, useState } from 'react';
|
|
||||||
// import { useProxy } from 'valtio/utils';
|
|
||||||
// import HeaderSearch from '../../../_com/header';
|
|
||||||
// import JudulList from '../../../_com/judulList';
|
|
||||||
// import stateLayananDesa from '../../../_state/desa/layananDesa';
|
|
||||||
|
|
||||||
// function PelayananTelunjukSakti() {
|
|
||||||
// const [search, setSearch] = useState("");
|
|
||||||
// return (
|
|
||||||
// <Box>
|
|
||||||
// <HeaderSearch
|
|
||||||
// title='Posisi Organisasi'
|
|
||||||
// placeholder='pencarian'
|
|
||||||
// searchIcon={<IconSearch size={20} />}
|
|
||||||
// value={search}
|
|
||||||
// onChange={(e) => setSearch(e.currentTarget.value)}
|
|
||||||
// />
|
|
||||||
// <ListPelayananTelunjukSakti search={search} />
|
|
||||||
// </Box>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
|
||||||
// const telunjukSaktiState = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa)
|
|
||||||
// const router = useRouter()
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// data,
|
|
||||||
// page,
|
|
||||||
// totalPages,
|
|
||||||
// loading,
|
|
||||||
// load,
|
|
||||||
// } = telunjukSaktiState.findMany;
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// load(page, 10)
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// const filteredData = useMemo(() => {
|
|
||||||
// if (!data) return [];
|
|
||||||
// return data.filter(item => {
|
|
||||||
// const keyword = search.toLowerCase();
|
|
||||||
// return (
|
|
||||||
// item.name?.toLowerCase().includes(keyword) ||
|
|
||||||
// item.link?.toLowerCase().includes(keyword) ||
|
|
||||||
// item.deskripsi?.toLowerCase().includes(keyword)
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// .sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
|
||||||
// }, [data, search]);
|
|
||||||
|
|
||||||
// if (loading || !data) {
|
|
||||||
// return (
|
|
||||||
// <Stack py={10}>
|
|
||||||
// <Skeleton height={300} />
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (data.length === 0) {
|
|
||||||
// return (
|
|
||||||
// <Box py={10}>
|
|
||||||
// <Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
// <JudulList
|
|
||||||
// title='List Pelayanan Telunjuk Sakti Desa'
|
|
||||||
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
|
||||||
// />
|
|
||||||
// <Table striped withTableBorder withRowBorders>
|
|
||||||
// <TableThead>
|
|
||||||
// <TableTr>
|
|
||||||
// <TableTh>Nama</TableTh>
|
|
||||||
// <TableTh>Link</TableTh>
|
|
||||||
// <TableTh>Detail</TableTh>
|
|
||||||
// </TableTr>
|
|
||||||
// </TableThead>
|
|
||||||
// <TableTbody>
|
|
||||||
// <TableTr>
|
|
||||||
// <TableTd colSpan={3}>
|
|
||||||
// <Text fz={"sm"} color="gray.5">
|
|
||||||
// Tidak ada data
|
|
||||||
// </Text>
|
|
||||||
// </TableTd>
|
|
||||||
// </TableTr>
|
|
||||||
// </TableTbody>
|
|
||||||
// </Table>
|
|
||||||
// </Paper>
|
|
||||||
// </Box>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Box py={10}>
|
|
||||||
// <Paper bg={colors['white-1']} p={'md'}>
|
|
||||||
// <JudulList
|
|
||||||
// title='List Pelayanan Telunjuk Sakti Desa'
|
|
||||||
// href='/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/create'
|
|
||||||
// />
|
|
||||||
// <Table striped withTableBorder withRowBorders>
|
|
||||||
// <TableThead>
|
|
||||||
// <TableTr>
|
|
||||||
// <TableTh>Nama</TableTh>
|
|
||||||
// <TableTh>Link</TableTh>
|
|
||||||
// <TableTh>Detail</TableTh>
|
|
||||||
// </TableTr>
|
|
||||||
// </TableThead>
|
|
||||||
// <TableTbody>
|
|
||||||
// {filteredData.map((item) => (
|
|
||||||
// <TableTr key={item.id}>
|
|
||||||
// <TableTd>
|
|
||||||
// <Box w={100}>
|
|
||||||
// <Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.name }} />
|
|
||||||
// </Box>
|
|
||||||
// </TableTd>
|
|
||||||
// <TableTd>
|
|
||||||
// <Box w={100}>
|
|
||||||
// <a href={item.link} target="_blank" rel="noopener noreferrer">
|
|
||||||
// <Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} truncate="end" fz={"sm"} />
|
|
||||||
// </a>
|
|
||||||
// </Box>
|
|
||||||
// </TableTd>
|
|
||||||
// <TableTd>
|
|
||||||
// <Text>
|
|
||||||
// <Button onClick={() => router.push(`/admin/desa/layanan/pelayanan_telunjuk_sakti_desa/${item.id}`)}>
|
|
||||||
// <IconDeviceImac size={20} />
|
|
||||||
// </Button>
|
|
||||||
// </Text>
|
|
||||||
// </TableTd>
|
|
||||||
// </TableTr>
|
|
||||||
// ))}
|
|
||||||
// </TableTbody>
|
|
||||||
// </Table>
|
|
||||||
// </Paper>
|
|
||||||
// <Center>
|
|
||||||
// <Pagination
|
|
||||||
// value={page}
|
|
||||||
// onChange={(newPage) => {
|
|
||||||
// load(newPage, 10);
|
|
||||||
// window.scrollTo(0, 0);
|
|
||||||
// }}
|
|
||||||
// total={totalPages}
|
|
||||||
// mt="md"
|
|
||||||
// mb="md"
|
|
||||||
// />
|
|
||||||
// </Center>
|
|
||||||
// </Box>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default PelayananTelunjukSakti;
|
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
@@ -174,8 +18,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -225,7 +68,6 @@ function ListPelayananTelunjukSakti({ 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 Pelayanan Telunjuk Sakti</Title>
|
<Title order={4}>Daftar Pelayanan Telunjuk Sakti</Title>
|
||||||
<Tooltip label="Tambah Layanan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -236,7 +78,6 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ 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,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} 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';
|
||||||
@@ -30,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({
|
||||||
@@ -47,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);
|
||||||
@@ -68,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,11 +136,9 @@ function EditPenghargaan() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Tombol Back + Title */}
|
{/* Tombol Back + Title */}
|
||||||
<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 Penghargaan
|
Edit Penghargaan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -155,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"
|
||||||
>
|
>
|
||||||
@@ -174,25 +206,47 @@ function EditPenghargaan() {
|
|||||||
Seret gambar atau klik untuk memilih file
|
Seret gambar atau klik untuk memilih file
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB, format gambar wajib
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
<Box pos="relative" mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<Image
|
<Box>
|
||||||
src={previewImage}
|
<Image
|
||||||
alt="Preview Gambar"
|
src={previewImage.startsWith('http') ? previewImage : `${window.location.origin}${previewImage}`}
|
||||||
radius="md"
|
alt="Preview Gambar"
|
||||||
style={{
|
radius="md"
|
||||||
maxHeight: 220,
|
style={{
|
||||||
objectFit: 'contain',
|
maxHeight: 200,
|
||||||
border: `1px solid ${colors['blue-button']}`,
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
radius="xl"
|
||||||
|
size="sm"
|
||||||
|
pos="absolute"
|
||||||
|
top={5}
|
||||||
|
right={5}
|
||||||
|
onClick={() => {
|
||||||
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
}}
|
}}
|
||||||
loading="lazy"
|
style={{
|
||||||
/>
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconX size={14} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -212,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"
|
||||||
@@ -222,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>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState } from 'react';
|
import colors from '@/con/colors';
|
||||||
import penghargaanState from '../../../_state/desa/penghargaan';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -12,12 +8,15 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import colors from '@/con/colors';
|
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 { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
import penghargaanState from '../../../_state/desa/penghargaan';
|
||||||
|
|
||||||
function DetailPenghargaan() {
|
function DetailPenghargaan() {
|
||||||
const statePenghargaan = useProxy(penghargaanState);
|
const statePenghargaan = useProxy(penghargaanState);
|
||||||
@@ -127,34 +126,30 @@ function DetailPenghargaan() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm" mt={10}>
|
<Group gap="sm" mt={10}>
|
||||||
<Tooltip label="Hapus Penghargaan" withArrow position="top">
|
<Button
|
||||||
<Button
|
color="red"
|
||||||
color="red"
|
onClick={() => {
|
||||||
onClick={() => {
|
setSelectedId(data.id);
|
||||||
setSelectedId(data.id);
|
setModalHapus(true);
|
||||||
setModalHapus(true);
|
}}
|
||||||
}}
|
variant="light"
|
||||||
variant="light"
|
radius="md"
|
||||||
radius="md"
|
size="md"
|
||||||
size="md"
|
>
|
||||||
>
|
<IconTrash size={20} />
|
||||||
<IconTrash size={20} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Penghargaan" withArrow position="top">
|
<Button
|
||||||
<Button
|
color="green"
|
||||||
color="green"
|
onClick={() =>
|
||||||
onClick={() =>
|
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
|
||||||
router.push(`/admin/desa/penghargaan/${data.id}/edit`)
|
}
|
||||||
}
|
variant="light"
|
||||||
variant="light"
|
radius="md"
|
||||||
radius="md"
|
size="md"
|
||||||
size="md"
|
>
|
||||||
>
|
<IconEdit size={20} />
|
||||||
<IconEdit size={20} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
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
|
||||||
Tooltip,
|
|
||||||
} 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';
|
||||||
@@ -27,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 = {
|
||||||
@@ -40,37 +42,43 @@ function CreatePenghargaan() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
try {
|
||||||
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
setIsSubmitting(true);
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
|
if (!uploaded?.id) {
|
||||||
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
|
}
|
||||||
|
|
||||||
|
statePenghargaan.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
|
await statePenghargaan.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/penghargaan');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating penghargaan:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menambahkan penghargaan');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
|
||||||
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
|
||||||
}
|
|
||||||
|
|
||||||
statePenghargaan.create.form.imageId = uploaded.id;
|
|
||||||
|
|
||||||
await statePenghargaan.create.create();
|
|
||||||
resetForm();
|
|
||||||
router.push('/admin/desa/penghargaan');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 Penghargaan
|
Tambah Penghargaan
|
||||||
</Title>
|
</Title>
|
||||||
@@ -87,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"
|
||||||
@@ -95,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"
|
||||||
@@ -125,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"
|
||||||
>
|
>
|
||||||
@@ -146,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"
|
||||||
@@ -154,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"
|
||||||
@@ -170,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>
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -69,7 +68,6 @@ function ListPenghargaan({ 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 Penghargaan</Title>
|
<Title order={4}>List Penghargaan</Title>
|
||||||
<Tooltip label="Tambah Penghargaan" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -78,7 +76,6 @@ function ListPenghargaan({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconCategory, IconListDetails } 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 { IconListDetails, IconCategory } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,15 +14,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
|
|||||||
label: "List Pengumuman",
|
label: "List Pengumuman",
|
||||||
value: "listpengumuman",
|
value: "listpengumuman",
|
||||||
href: "/admin/desa/pengumuman/list-pengumuman",
|
href: "/admin/desa/pengumuman/list-pengumuman",
|
||||||
icon: <IconListDetails size={18} stroke={1.8} />,
|
icon: <IconListDetails size={18} stroke={1.8} />
|
||||||
tooltip: "Lihat semua daftar pengumuman"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Pengumuman",
|
label: "Kategori Pengumuman",
|
||||||
value: "kategoripengumuman",
|
value: "kategoripengumuman",
|
||||||
href: "/admin/desa/pengumuman/kategori-pengumuman",
|
href: "/admin/desa/pengumuman/kategori-pengumuman",
|
||||||
icon: <IconCategory size={18} stroke={1.8} />,
|
icon: <IconCategory size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola kategori pengumuman"
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -70,19 +68,18 @@ function LayoutTabsLayanan({ 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={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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';
|
||||||
@@ -23,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(() => {
|
||||||
@@ -36,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);
|
||||||
@@ -55,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,
|
||||||
@@ -67,14 +70,23 @@ 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 */}
|
||||||
<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()}
|
||||||
@@ -83,7 +95,6 @@ function EditKategoriPengumuman() {
|
|||||||
>
|
>
|
||||||
<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 Kategori Pengumuman
|
Edit Kategori Pengumuman
|
||||||
</Title>
|
</Title>
|
||||||
@@ -108,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"
|
||||||
@@ -118,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>
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
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 = {
|
||||||
@@ -26,16 +29,22 @@ function CreateKategoriPengumuman() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
try {
|
||||||
resetForm();
|
await createState.create.create();
|
||||||
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
resetForm();
|
||||||
|
router.push('/admin/desa/pengumuman/kategori-pengumuman');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Gagal menambahkan kategori pengumuman');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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()}
|
||||||
@@ -44,7 +53,6 @@ function CreateKategoriPengumuman() {
|
|||||||
>
|
>
|
||||||
<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 Kategori Pengumuman
|
Tambah Kategori Pengumuman
|
||||||
</Title>
|
</Title>
|
||||||
@@ -63,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"
|
||||||
@@ -79,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>
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
Box, Button, Center, Paper, Skeleton, Stack,
|
Box, Button, Center,
|
||||||
|
Pagination,
|
||||||
|
Paper, Skeleton, Stack,
|
||||||
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
|
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
|
||||||
Text, Title, Tooltip, Pagination
|
Text, Title
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
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';
|
||||||
@@ -66,16 +68,14 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
|
||||||
<Title order={4}>List Kategori Pengumuman</Title>
|
<Title order={4}>List Kategori Pengumuman</Title>
|
||||||
<Tooltip label="Tambah Kategori Pengumuman" withArrow>
|
<Button
|
||||||
<Button
|
leftSection={<IconPlus size={18} />}
|
||||||
leftSection={<IconPlus size={18} />}
|
color="blue"
|
||||||
color="blue"
|
variant="light"
|
||||||
variant="light"
|
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
|
||||||
onClick={() => router.push('/admin/desa/pengumuman/kategori-pengumuman/create')}
|
>
|
||||||
>
|
Tambah Baru
|
||||||
Tambah Baru
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
@@ -99,29 +99,25 @@ function ListKategoriPengumuman({ search }: { search: string }) {
|
|||||||
<Text truncate lineClamp={1}>{item.name}</Text>
|
<Text truncate lineClamp={1}>{item.name}</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Tooltip label="Edit Kategori Pengumuman" withArrow>
|
<Button
|
||||||
<Button
|
variant='light'
|
||||||
variant='light'
|
color='green'
|
||||||
color='green'
|
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
|
||||||
onClick={() => router.push(`/admin/desa/pengumuman/kategori-pengumuman/${item.id}`)}
|
>
|
||||||
>
|
<IconEdit size={20} />
|
||||||
<IconEdit size={20} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Tooltip label="Hapus Kategori Pengumuman" withArrow>
|
<Button
|
||||||
<Button
|
variant='light'
|
||||||
variant='light'
|
color='red'
|
||||||
color='red'
|
disabled={listDataState.delete.loading}
|
||||||
disabled={listDataState.delete.loading}
|
onClick={() => {
|
||||||
onClick={() => {
|
setSelectedId(item.id)
|
||||||
setSelectedId(item.id)
|
setModalHapus(true)
|
||||||
setModalHapus(true)
|
}}>
|
||||||
}}>
|
<IconTrash size={20} />
|
||||||
<IconTrash size={20} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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";
|
||||||
@@ -34,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();
|
||||||
@@ -51,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);
|
||||||
@@ -67,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,
|
||||||
@@ -79,22 +95,32 @@ 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">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Pengumuman
|
Edit Pengumuman
|
||||||
</Title>
|
</Title>
|
||||||
@@ -155,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>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
|
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -8,16 +10,13 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
Tooltip,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
|
||||||
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
|
|
||||||
|
|
||||||
export default function DetailPengumuman() {
|
export default function DetailPengumuman() {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman);
|
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||||
@@ -117,7 +116,6 @@ export default function DetailPengumuman() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Pengumuman" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -130,9 +128,7 @@ export default function DetailPengumuman() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Pengumuman" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -146,7 +142,6 @@ export default function DetailPengumuman() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -13,25 +13,36 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack } from '@tabler/icons-react';
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
function CreatePengumuman() {
|
function CreatePengumuman() {
|
||||||
const pengumumanState = useProxy(stateDesaPengumuman);
|
const pengumumanState = useProxy(stateDesaPengumuman);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
pengumumanState.category.findMany.load();
|
pengumumanState.category.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await pengumumanState.pengumuman.create.create();
|
try {
|
||||||
resetForm();
|
setIsSubmitting(true);
|
||||||
router.push('/admin/desa/pengumuman/list-pengumuman');
|
await pengumumanState.pengumuman.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/pengumuman/list-pengumuman');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating pengumuman:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat membuat pengumuman');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -47,11 +58,9 @@ function CreatePengumuman() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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 Pengumuman
|
Tambah Pengumuman
|
||||||
</Title>
|
</Title>
|
||||||
@@ -68,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"
|
||||||
@@ -79,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"
|
||||||
@@ -115,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"
|
||||||
@@ -125,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>
|
||||||
|
|||||||
@@ -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 { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
|
||||||
@@ -69,16 +68,14 @@ function ListPengumuman({ 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 Pengumuman</Title>
|
<Title order={4}>Daftar Pengumuman</Title>
|
||||||
<Tooltip label="Tambah Pengumuman" withArrow>
|
<Button
|
||||||
<Button
|
leftSection={<IconCircleDashedPlus size={18} />}
|
||||||
leftSection={<IconCircleDashedPlus size={18} />}
|
color="blue"
|
||||||
color="blue"
|
variant="light"
|
||||||
variant="light"
|
onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
|
||||||
onClick={() => router.push('/admin/desa/pengumuman/list-pengumuman/create')}
|
>
|
||||||
>
|
Tambah Baru
|
||||||
Tambah Baru
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
import { IconCategory, IconListCheck } from '@tabler/icons-react';
|
import { IconCategory, IconListCheck } 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';
|
||||||
@@ -14,15 +14,13 @@ function LayoutTabsPotensi({ children }: { children: React.ReactNode }) {
|
|||||||
label: "List Potensi",
|
label: "List Potensi",
|
||||||
value: "list_potensi",
|
value: "list_potensi",
|
||||||
href: "/admin/desa/potensi/list-potensi",
|
href: "/admin/desa/potensi/list-potensi",
|
||||||
icon: <IconListCheck size={18} stroke={1.8} />,
|
icon: <IconListCheck size={18} stroke={1.8} />
|
||||||
tooltip: "Lihat semua potensi desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kategori Potensi",
|
label: "Kategori Potensi",
|
||||||
value: "kategori_potensi",
|
value: "kategori_potensi",
|
||||||
href: "/admin/desa/potensi/kategori-potensi",
|
href: "/admin/desa/potensi/kategori-potensi",
|
||||||
icon: <IconCategory size={18} stroke={1.8} />,
|
icon: <IconCategory size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola kategori potensi"
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -70,19 +68,18 @@ function LayoutTabsPotensi({ 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={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</TabsTab>
|
</TabsTab>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
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';
|
||||||
@@ -27,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 () => {
|
||||||
@@ -39,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);
|
||||||
@@ -56,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,
|
||||||
@@ -70,13 +87,14 @@ 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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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()}
|
||||||
@@ -85,7 +103,6 @@ function EditKategoriPotensi() {
|
|||||||
>
|
>
|
||||||
<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 Kategori Potensi
|
Edit Kategori Potensi
|
||||||
</Title>
|
</Title>
|
||||||
@@ -109,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"
|
||||||
@@ -119,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>
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
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 = {
|
||||||
@@ -26,25 +28,30 @@ function CreateKategoriPotensi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await createState.create.create();
|
try {
|
||||||
resetForm();
|
setIsSubmitting(true);
|
||||||
router.push('/admin/desa/potensi/kategori-potensi');
|
await createState.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/potensi/kategori-potensi');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating kategori potensi:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header dengan back button */}
|
{/* Header dengan back button */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Kategori Potensi
|
Tambah Kategori Potensi
|
||||||
</Title>
|
</Title>
|
||||||
@@ -63,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"
|
||||||
@@ -79,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>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
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';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import potensiDesaState from '../../../_state/desa/potensi';
|
|
||||||
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
|
||||||
|
import potensiDesaState from '../../../_state/desa/potensi';
|
||||||
|
|
||||||
function KategoriPotensi() {
|
function KategoriPotensi() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -62,7 +62,6 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={4}>List Kategori Potensi</Title>
|
<Title order={4}>List Kategori Potensi</Title>
|
||||||
<Tooltip label="Tambah Kategori Potensi" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -71,7 +70,6 @@ function ListKategoriPotensi({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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";
|
||||||
@@ -39,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) => {
|
||||||
@@ -47,11 +58,11 @@ function EditPotensi() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiDesaState.kategoriPotensi.findMany.load();
|
potensiDesaState.kategoriPotensi.findMany.load();
|
||||||
|
|
||||||
const loadPotensi = async () => {
|
const loadPotensi = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await potensiState.edit.load(id);
|
const data = await potensiState.edit.load(id);
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -62,35 +73,45 @@ function EditPotensi() {
|
|||||||
content: data.content || "",
|
content: data.content || "",
|
||||||
imageId: data.imageId || "",
|
imageId: data.imageId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// // merge, bukan replace
|
setOriginalData({
|
||||||
// setFormData((prev) => ({
|
name: data.name || "",
|
||||||
// ...prev,
|
deskripsi: data.deskripsi || "",
|
||||||
// name: data.name ?? prev.name,
|
kategoriId: data.kategoriId || "",
|
||||||
// deskripsi: data.deskripsi ?? prev.deskripsi,
|
content: data.content || "",
|
||||||
// kategoriId: data.kategoriId ?? prev.kategoriId,
|
imageId: data.imageId || "",
|
||||||
// content: data.content ?? prev.content,
|
imageUrl: data.image?.link || "",
|
||||||
// imageId: data.imageId ?? prev.imageId,
|
});
|
||||||
// }));
|
setPreviewImage(data.image.link);
|
||||||
|
|
||||||
if (data?.image?.link) {
|
|
||||||
setPreviewImage(data.image.link);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading potensi:", error);
|
console.error("Error loading potensi:", error);
|
||||||
toast.error("Gagal memuat data potensi");
|
toast.error("Gagal memuat data potensi");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPotensi();
|
loadPotensi();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
name: originalData.name || "",
|
||||||
|
deskripsi: originalData.deskripsi || "",
|
||||||
|
kategoriId: originalData.kategoriId || "",
|
||||||
|
content: originalData.content || "",
|
||||||
|
imageId: originalData.imageId || ""
|
||||||
|
});
|
||||||
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
|
setFile(null);
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
let imageId = formData.imageId;
|
let imageId = formData.imageId;
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
@@ -116,22 +137,22 @@ function EditPotensi() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating potensi:", error);
|
console.error("Error updating potensi:", error);
|
||||||
toast.error("Terjadi kesalahan saat memperbarui potensi");
|
toast.error("Terjadi kesalahan saat memperbarui potensi");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
<Box px={{ base: "sm", md: "lg" }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button
|
||||||
<Button
|
variant="subtle"
|
||||||
variant="subtle"
|
onClick={() => router.back()}
|
||||||
onClick={() => router.back()}
|
p="xs"
|
||||||
p="xs"
|
radius="md"
|
||||||
radius="md"
|
>
|
||||||
>
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Potensi Desa
|
Edit Potensi Desa
|
||||||
</Title>
|
</Title>
|
||||||
@@ -167,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"
|
||||||
@@ -181,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}>
|
||||||
@@ -222,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>
|
||||||
@@ -258,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>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
|
||||||
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
import potensiDesaState from '@/app/admin/(dashboard)/_state/desa/potensi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
export default function DetailPotensi() {
|
export default function DetailPotensi() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -108,7 +108,6 @@ export default function DetailPotensi() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Potensi" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -121,9 +120,7 @@ export default function DetailPotensi() {
|
|||||||
>
|
>
|
||||||
<IconTrash size={20} />
|
<IconTrash size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Potensi" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -135,7 +132,6 @@ export default function DetailPotensi() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
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';
|
||||||
@@ -29,30 +30,39 @@ function CreatePotensi() {
|
|||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
potensiDesaState.kategoriPotensi.findMany.load();
|
potensiDesaState.kategoriPotensi.findMany.load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
if (!file) return toast.warn('Pilih file gambar terlebih dahulu');
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
return toast.error('Gagal upload gambar');
|
return toast.error('Gagal upload gambar');
|
||||||
|
}
|
||||||
|
|
||||||
|
potensiState.create.form.imageId = uploaded.id;
|
||||||
|
|
||||||
|
await potensiState.create.create();
|
||||||
|
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/potensi/list-potensi');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating potensi:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat menambahkan potensi');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
potensiState.create.form.imageId = uploaded.id;
|
|
||||||
|
|
||||||
await potensiState.create.create();
|
|
||||||
|
|
||||||
resetForm();
|
|
||||||
router.push('/admin/desa/potensi/list-potensi');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -72,11 +82,9 @@ function CreatePotensi() {
|
|||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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 Potensi Desa
|
Tambah Potensi Desa
|
||||||
</Title>
|
</Title>
|
||||||
@@ -93,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"
|
||||||
@@ -115,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 || ""}
|
||||||
@@ -125,7 +159,7 @@ function CreatePotensi() {
|
|||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.nama,
|
label: item.nama,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* Upload Gambar */}
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
@@ -142,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"
|
||||||
>
|
>
|
||||||
@@ -160,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>
|
||||||
@@ -190,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"
|
||||||
@@ -200,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>
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -76,7 +75,6 @@ function ListPotensi({ 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 Potensi Desa</Title>
|
<Title order={4}>Daftar Potensi Desa</Title>
|
||||||
<Tooltip label="Tambah Potensi" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -85,7 +83,6 @@ function ListPotensi({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
<Table highlightOnHover style={{ minWidth: '700px' }}>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
|
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||||
|
import { IconCalendar, IconUser, 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 { IconUser, IconUsers, IconCalendar } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -14,22 +14,19 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
label: "Profile Desa",
|
label: "Profile Desa",
|
||||||
value: "profiledesa",
|
value: "profiledesa",
|
||||||
href: "/admin/desa/profile/profile-desa",
|
href: "/admin/desa/profile/profile-desa",
|
||||||
icon: <IconUser size={18} stroke={1.8} />,
|
icon: <IconUser size={18} stroke={1.8} />
|
||||||
tooltip: "Lihat dan kelola profil desa"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel",
|
label: "Profile Perbekel",
|
||||||
value: "profileperbekel",
|
value: "profileperbekel",
|
||||||
href: "/admin/desa/profile/profile-perbekel",
|
href: "/admin/desa/profile/profile-perbekel",
|
||||||
icon: <IconUsers size={18} stroke={1.8} />,
|
icon: <IconUsers size={18} stroke={1.8} />
|
||||||
tooltip: "Kelola data Perbekel"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Profile Perbekel Dari Masa Ke Masa",
|
label: "Profile Perbekel Dari Masa Ke Masa",
|
||||||
value: "profile-perbekel-dari-masa-ke-masa",
|
value: "profile-perbekel-dari-masa-ke-masa",
|
||||||
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
href: "/admin/desa/profile/profile-perbekel-dari-masa-ke-masa",
|
||||||
icon: <IconCalendar size={18} stroke={1.8} />,
|
icon: <IconCalendar size={18} stroke={1.8} />
|
||||||
tooltip: "Riwayat Perbekel dari masa ke masa"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -76,42 +73,41 @@ function LayoutTabsDetail({ children }: { children: React.ReactNode }) {
|
|||||||
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
|
||||||
<Tooltip key={i} label={tab.tooltip} position="bottom" withArrow transitionProps={{ transition: 'pop', duration: 200 }}>
|
|
||||||
<TabsTab
|
|
||||||
value={tab.value}
|
|
||||||
leftSection={tab.icon}
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
transition: "all 0.2s ease",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</TabsTab>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
<TabsPanel
|
<TabsTab
|
||||||
key={i}
|
key={i}
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
|
leftSection={tab.icon}
|
||||||
style={{
|
style={{
|
||||||
padding: "1.5rem",
|
fontWeight: 600,
|
||||||
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
fontSize: "0.9rem",
|
||||||
borderRadius: "1rem",
|
transition: "all 0.2s ease",
|
||||||
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Konten dummy, bisa diganti sesuai routing */}
|
{tab.label}
|
||||||
<>{children}</>
|
</TabsTab>
|
||||||
</TabsPanel>
|
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</TabsList>
|
||||||
</Stack>
|
</ScrollArea>
|
||||||
);
|
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<TabsPanel
|
||||||
|
key={i}
|
||||||
|
value={tab.value}
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
|
||||||
|
borderRadius: "1rem",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Konten dummy, bisa diganti sesuai routing */}
|
||||||
|
<>{children}</>
|
||||||
|
</TabsPanel>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayoutTabsDetail;
|
export default LayoutTabsDetail;
|
||||||
|
|||||||
@@ -1,154 +1,244 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
'use client';
|
||||||
'use client'
|
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
|
|
||||||
|
// 🧩 Type untuk form
|
||||||
|
interface FormData {
|
||||||
|
judul: string;
|
||||||
|
deskripsi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧩 Main Component
|
||||||
function Page() {
|
function Page() {
|
||||||
const lambangState = useProxy(stateProfileDesa.lambangDesa)
|
const router = useRouter();
|
||||||
const router = useRouter()
|
const params = useParams();
|
||||||
const params = useParams()
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
// Load data
|
|
||||||
useEffect(() => {
|
|
||||||
const loadData = async () => {
|
|
||||||
const id = params?.id as string;
|
|
||||||
if (!id) {
|
|
||||||
toast.error("ID tidak valid");
|
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const [formData, setFormData] = useState<FormData>({ judul: '', deskripsi: '' });
|
||||||
const data = await lambangState.findUnique.load(id);
|
const [originalData, setOriginalData] = useState<FormData>({ judul: '', deskripsi: '' });
|
||||||
lambangState.update.initialize(data);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
} catch (error) {
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
console.error("Error loading lambang:", error);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
toast.error("Gagal memuat data lambang desa");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadData();
|
// 🧭 Load data awal
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
const id = params?.id as string;
|
||||||
|
if (!id) {
|
||||||
|
toast.error('ID tidak valid');
|
||||||
|
router.push('/admin/desa/profile/profile-desa');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
setIsLoading(true);
|
||||||
lambangState.update.reset();
|
setLoadError(null);
|
||||||
lambangState.findUnique.reset();
|
|
||||||
};
|
|
||||||
}, [params?.id, router]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
try {
|
||||||
if (isSubmitting || !lambangState.update.form.judul.trim()) {
|
const data = await stateProfileDesa.lambangDesa.findUnique.load(id);
|
||||||
toast.error("Judul wajib diisi");
|
|
||||||
return;
|
if (data) {
|
||||||
}
|
const initial: FormData = {
|
||||||
|
judul: data.judul || '',
|
||||||
setIsSubmitting(true);
|
deskripsi: data.deskripsi || '',
|
||||||
|
};
|
||||||
try {
|
setFormData(initial);
|
||||||
const success = await lambangState.update.submit();
|
setOriginalData(initial);
|
||||||
|
|
||||||
if (success) {
|
// Penting untuk isi id di state sebelum submit
|
||||||
toast.success("Data berhasil disimpan");
|
stateProfileDesa.lambangDesa.update.initialize(data);
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
} else {
|
||||||
}
|
setLoadError('Data tidak ditemukan');
|
||||||
} catch (error) {
|
|
||||||
console.error("Error update lambang desa:", error);
|
|
||||||
toast.error("Terjadi kesalahan saat update lambang desa");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading lambang:', error);
|
||||||
|
setLoadError('Gagal memuat data lambang desa');
|
||||||
|
toast.error('Gagal memuat data lambang desa');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => router.back();
|
loadData();
|
||||||
|
|
||||||
// Loading state
|
return () => {
|
||||||
if (lambangState.findUnique.loading || lambangState.update.loading) {
|
stateProfileDesa.lambangDesa.update.reset();
|
||||||
return (
|
stateProfileDesa.lambangDesa.findUnique.reset();
|
||||||
<Box>
|
};
|
||||||
<Center h={400}>
|
}, [params?.id, router]);
|
||||||
<Text>Memuat data...</Text>
|
|
||||||
</Center>
|
// 🔁 Reset form
|
||||||
</Box>
|
const handleResetForm = () => {
|
||||||
);
|
setFormData(originalData);
|
||||||
|
toast.info('Form dikembalikan ke data awal');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 💾 Submit handler
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.judul.trim()) {
|
||||||
|
toast.error('Judul wajib diisi');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error state
|
setIsSubmitting(true);
|
||||||
if (lambangState.findUnique.error) {
|
try {
|
||||||
return (
|
const state = stateProfileDesa.lambangDesa;
|
||||||
<Box>
|
state.update.form.judul = formData.judul;
|
||||||
<Stack gap="md">
|
state.update.form.deskripsi = formData.deskripsi;
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
|
||||||
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
|
||||||
<Text fw="bold">Error</Text>
|
|
||||||
<Text>{lambangState.findUnique.error}</Text>
|
|
||||||
</Alert>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const success = await state.update.submit();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Data berhasil disimpan');
|
||||||
|
router.push('/admin/desa/profile/profile-desa');
|
||||||
|
} else {
|
||||||
|
toast.error('Gagal menyimpan data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error update lambang desa:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat update lambang desa');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 📝 Handlers
|
||||||
|
const handleJudulChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData(prev => ({ ...prev, judul: e.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeskripsiChange = (html: string) => {
|
||||||
|
setFormData(prev => ({ ...prev, deskripsi: html }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => router.back();
|
||||||
|
|
||||||
|
// 🔄 Loading
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Center h={400}>
|
||||||
<Group mb="md">
|
<Stack align="center" gap="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Loader size="lg" color={colors['blue-button']} />
|
||||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
<Text size="lg" fw={500} c="dimmed">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
Memuat data lambang desa...
|
||||||
</Button>
|
</Text>
|
||||||
</Tooltip>
|
</Stack>
|
||||||
<Title order={4} ml="sm" c="dark">Edit Lambang Desa</Title>
|
</Center>
|
||||||
</Group>
|
</Box>
|
||||||
|
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={3}>Edit Lambang Desa</Title>
|
|
||||||
|
|
||||||
{/* Judul */}
|
|
||||||
<TextInput
|
|
||||||
label={<Text fw="bold">Judul</Text>}
|
|
||||||
placeholder="Judul lambang"
|
|
||||||
defaultValue={lambangState.update.form.judul}
|
|
||||||
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
|
|
||||||
error={!lambangState.update.form.judul && "Judul wajib diisi"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Deskripsi */}
|
|
||||||
<Box>
|
|
||||||
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={lambangState.update.form.deskripsi}
|
|
||||||
onChange={(val) => lambangState.update.form.deskripsi = val}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={isSubmitting || lambangState.update.loading}
|
|
||||||
disabled={!lambangState.update.form.judul}
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || lambangState.update.loading}>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Error
|
||||||
|
if (loadError) {
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
|
||||||
|
{loadError}
|
||||||
|
</Alert>
|
||||||
|
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
|
||||||
|
Kembali ke Halaman Utama
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧱 UI utama
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Group mb="sm">
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Lambang Desa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '60%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="xl"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Judul */}
|
||||||
|
<TextInput
|
||||||
|
label={<Text fw="bold" size="sm">Judul</Text>}
|
||||||
|
placeholder="Masukkan judul lambang desa"
|
||||||
|
value={formData.judul}
|
||||||
|
onChange={handleJudulChange}
|
||||||
|
error={!formData.judul.trim() && 'Judul wajib diisi'}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" size="sm" mb={8}>
|
||||||
|
Deskripsi
|
||||||
|
</Text>
|
||||||
|
<EditEditor value={formData.deskripsi} onChange={handleDeskripsiChange} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Tombol Aksi */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ 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 { Box, Button, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title, Tooltip, Center, Alert } 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 { IconArrowBack, IconPhoto, IconUpload, IconX, IconAlertCircle } 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';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -19,8 +19,8 @@ function Page() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [images, setImages] = useState<
|
const [images, setImages] = useState<
|
||||||
Array<{ file: File | null; preview: string; label: string; imageId?: string }>
|
Array<{ file: File | null; preview: string; label: string; imageId?: string }>
|
||||||
>([]);
|
>([]);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
judul: '',
|
judul: '',
|
||||||
deskripsi: '',
|
deskripsi: '',
|
||||||
@@ -28,6 +28,12 @@ function Page() {
|
|||||||
});
|
});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [originalData, setOriginalData] = useState({
|
||||||
|
judul: "",
|
||||||
|
deskripsi: "",
|
||||||
|
images: [] as Array<{ label: string; imageId: string }>
|
||||||
|
});
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -52,6 +58,17 @@ function Page() {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setOriginalData({
|
||||||
|
judul: data.judul || '',
|
||||||
|
deskripsi: data.deskripsi || '',
|
||||||
|
images: (data.images || []).map((img: any) => ({
|
||||||
|
label: img.label,
|
||||||
|
imageId: img.image?.id ?? '',
|
||||||
|
preview: img.image?.link ?? '',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
if (data?.images?.length > 0 && data.images[0].image?.link) {
|
if (data?.images?.length > 0 && data.images[0].image?.link) {
|
||||||
setImages(data.images.map((img: any) => ({
|
setImages(data.images.map((img: any) => ({
|
||||||
file: null,
|
file: null,
|
||||||
@@ -77,15 +94,36 @@ function Page() {
|
|||||||
|
|
||||||
const handleBack = () => router.back();
|
const handleBack = () => router.back();
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setFormData({
|
||||||
|
judul: originalData.judul,
|
||||||
|
deskripsi: originalData.deskripsi,
|
||||||
|
images: originalData.images.map((img) => ({
|
||||||
|
label: img.label,
|
||||||
|
imageId: img.imageId,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
setImages(
|
||||||
|
originalData.images.map((img: any) => ({
|
||||||
|
file: null,
|
||||||
|
preview: img.preview, // pakai preview masing-masing, bukan cuma satu
|
||||||
|
label: img.label,
|
||||||
|
imageId: img.imageId,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.info("Form dikembalikan ke data awal");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (isSubmitting || !formData.judul.trim()) {
|
if (isSubmitting || !formData.judul.trim()) {
|
||||||
toast.error("Judul wajib diisi");
|
toast.error("Judul wajib diisi");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
const uploadedImages = [];
|
const uploadedImages = [];
|
||||||
|
|
||||||
// Upload semua gambar baru
|
// Upload semua gambar baru
|
||||||
@@ -95,7 +133,7 @@ function Page() {
|
|||||||
uploadedImages.push({ imageId: img.imageId, label: img.label });
|
uploadedImages.push({ imageId: img.imageId, label: img.label });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload baru
|
// upload baru
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
file: img.file,
|
file: img.file,
|
||||||
@@ -108,7 +146,7 @@ function Page() {
|
|||||||
}
|
}
|
||||||
uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
|
uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Update ke global state
|
// Update ke global state
|
||||||
maskotState.update.updateField("judul", formData.judul);
|
maskotState.update.updateField("judul", formData.judul);
|
||||||
@@ -161,11 +199,9 @@ function Page() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<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>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title>
|
<Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -177,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"}
|
||||||
/>
|
/>
|
||||||
@@ -233,7 +269,7 @@ function Page() {
|
|||||||
setImages(updated);
|
setImages(updated);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hapus
|
<IconX size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Image
|
<Image
|
||||||
@@ -262,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>
|
||||||
|
|||||||
@@ -1,148 +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, Tooltip } 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 */}
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<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>
|
||||||
</Tooltip>
|
<Title order={4} ml="sm" c="dark">
|
||||||
<Title order={4} ml="sm" c="dark">Edit Sejarah Desa</Title>
|
Edit Sejarah Desa
|
||||||
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
{/* Form */}
|
||||||
<Stack gap="xs">
|
<Paper
|
||||||
<Title order={3}>Edit Sejarah Desa</Title>
|
w={{ base: '100%', md: '60%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
{/* Judul */}
|
p="xl"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Form Title */}
|
||||||
|
<Box>
|
||||||
|
<Title order={3} mb={4}>
|
||||||
|
Informasi Sejarah Desa
|
||||||
|
</Title>
|
||||||
|
</Box>
|
||||||
|
{/* Judul Field */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<Text fw="bold">Judul</Text>}
|
label={<Text fw="bold" size="sm">Judul Sejarah</Text>}
|
||||||
placeholder="Judul sejarah"
|
placeholder="Masukkan judul sejarah desa"
|
||||||
defaultValue={sejarahState.update.form.judul}
|
value={formData.judul}
|
||||||
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
|
onChange={handleJudulChange}
|
||||||
error={!sejarahState.update.form.judul && "Judul wajib diisi"}
|
error={!formData.judul.trim() && 'Judul wajib diisi'}
|
||||||
|
required
|
||||||
|
size="md"
|
||||||
|
radius="md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Deskripsi */}
|
{/* Deskripsi Field */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
|
<Text fw="bold" size="sm" mb={8}>
|
||||||
|
Deskripsi Sejarah
|
||||||
|
</Text>
|
||||||
<EditEditor
|
<EditEditor
|
||||||
value={sejarahState.update.form.deskripsi}
|
value={formData.deskripsi}
|
||||||
onChange={(val) => sejarahState.update.form.deskripsi = val}
|
onChange={handleDeskripsiChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Action Buttons */}
|
||||||
<Group>
|
<Group justify="right">
|
||||||
|
{/* Tombol Batal */}
|
||||||
<Button
|
<Button
|
||||||
bg={colors['blue-button']}
|
variant="outline"
|
||||||
onClick={handleSubmit}
|
color="gray"
|
||||||
loading={isSubmitting || sejarahState.update.loading}
|
radius="md"
|
||||||
disabled={!sejarahState.update.form.judul}
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || sejarahState.update.loading}>
|
{/* Tombol Simpan */}
|
||||||
Batal
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -152,4 +276,4 @@ function Page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
@@ -1,155 +1,247 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
'use client';
|
||||||
'use client'
|
|
||||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||||
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
|
||||||
|
|
||||||
|
// 🔹 Types
|
||||||
|
interface FormData {
|
||||||
|
visi: string;
|
||||||
|
misi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔹 Main Component
|
||||||
function Page() {
|
function Page() {
|
||||||
const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa)
|
const router = useRouter();
|
||||||
const router = useRouter()
|
const params = useParams();
|
||||||
const params = useParams()
|
const [formData, setFormData] = useState<FormData>({ visi: '', misi: '' });
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [originalData, setOriginalData] = useState<FormData>({ visi: '', misi: '' });
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load data
|
// 🧭 Load Data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
toast.error("ID tidak valid");
|
toast.error('ID tidak valid');
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
router.push('/admin/desa/profile/profile-desa');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
setIsLoading(true);
|
||||||
const data = await visiMisiState.findUnique.load(id);
|
setLoadError(null);
|
||||||
visiMisiState.update.initialize(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading visi misi:", error);
|
|
||||||
toast.error("Gagal memuat data visi misi desa");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadData();
|
try {
|
||||||
|
const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id);
|
||||||
|
if (data) {
|
||||||
|
const initialData: FormData = {
|
||||||
|
visi: data.visi || '',
|
||||||
|
misi: data.misi || '',
|
||||||
|
};
|
||||||
|
setFormData(initialData);
|
||||||
|
setOriginalData(initialData);
|
||||||
|
|
||||||
return () => {
|
// set id ke state agar submit pakai endpoint benar
|
||||||
visiMisiState.update.reset();
|
stateProfileDesa.visiMisiDesa.update.initialize(data);
|
||||||
visiMisiState.findUnique.reset();
|
} else {
|
||||||
};
|
setLoadError('Data tidak ditemukan');
|
||||||
}, [params?.id, router]);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
|
|
||||||
toast.error("Visi wajib diisi");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const success = await visiMisiState.update.submit();
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
toast.success("Data berhasil disimpan");
|
|
||||||
router.push("/admin/desa/profile/profile-desa");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error update visi misi desa:", error);
|
|
||||||
toast.error("Terjadi kesalahan saat update visi misi desa");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error load visi misi:', error);
|
||||||
|
setLoadError('Gagal memuat data visi misi desa');
|
||||||
|
toast.error('Gagal memuat data visi misi desa');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => router.back();
|
loadData();
|
||||||
|
|
||||||
// Loading state
|
return () => {
|
||||||
if (visiMisiState.findUnique.loading || visiMisiState.update.loading) {
|
stateProfileDesa.visiMisiDesa.update.reset();
|
||||||
return (
|
stateProfileDesa.visiMisiDesa.findUnique.reset();
|
||||||
<Box>
|
};
|
||||||
<Center h={400}>
|
}, [params?.id, router]);
|
||||||
<Text>Memuat data...</Text>
|
|
||||||
</Center>
|
// 🔄 Reset Form
|
||||||
</Box>
|
const handleResetForm = () => {
|
||||||
);
|
setFormData(originalData);
|
||||||
|
toast.info('Form dikembalikan ke data awal');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 💾 Submit
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.visi.trim()) {
|
||||||
|
toast.error('Visi wajib diisi');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error state
|
setIsSubmitting(true);
|
||||||
if (visiMisiState.findUnique.error) {
|
try {
|
||||||
return (
|
const originalState = stateProfileDesa.visiMisiDesa;
|
||||||
<Box>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Button variant="subtle" onClick={handleBack}>
|
|
||||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
|
||||||
</Button>
|
|
||||||
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
|
||||||
<Text fw="bold">Error</Text>
|
|
||||||
<Text>{visiMisiState.findUnique.error}</Text>
|
|
||||||
</Alert>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// update data form ke state sebelum submit
|
||||||
|
originalState.update.form.visi = formData.visi;
|
||||||
|
originalState.update.form.misi = formData.misi;
|
||||||
|
|
||||||
|
const success = await originalState.update.submit();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Data berhasil disimpan');
|
||||||
|
router.push('/admin/desa/profile/profile-desa');
|
||||||
|
} else {
|
||||||
|
toast.error('Gagal menyimpan data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error update visi misi desa:', error);
|
||||||
|
toast.error('Terjadi kesalahan saat update visi misi desa');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🧭 Field handlers
|
||||||
|
const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html }));
|
||||||
|
const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html }));
|
||||||
|
const handleBack = () => router.back();
|
||||||
|
|
||||||
|
// ⏳ Loading
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Center h={400}>
|
||||||
<Group mb="md">
|
<Stack align="center" gap="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Loader size="lg" color={colors['blue-button']} />
|
||||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
<Text size="lg" fw={500} c="dimmed">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
Memuat data...
|
||||||
</Button>
|
</Text>
|
||||||
</Tooltip>
|
</Stack>
|
||||||
<Title order={4} ml="sm" c="dark">Edit Visi Misi Desa</Title>
|
</Center>
|
||||||
</Group>
|
</Box>
|
||||||
|
|
||||||
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Title order={3}>Edit Visi Misi Desa</Title>
|
|
||||||
|
|
||||||
{/* Visi */}
|
|
||||||
<Box>
|
|
||||||
<Text fz={"md"} fw={"bold"}>Visi</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={visiMisiState.update.form.visi}
|
|
||||||
onChange={(val) => visiMisiState.update.form.visi = val}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Misi */}
|
|
||||||
<Box>
|
|
||||||
<Text fz={"md"} fw={"bold"}>Misi</Text>
|
|
||||||
<EditEditor
|
|
||||||
value={visiMisiState.update.form.misi}
|
|
||||||
onChange={(val) => visiMisiState.update.form.misi = val}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
bg={colors['blue-button']}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
loading={isSubmitting || visiMisiState.update.loading}
|
|
||||||
disabled={!visiMisiState.update.form.visi}
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || visiMisiState.update.loading}>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Error
|
||||||
|
if (loadError) {
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Alert
|
||||||
|
icon={<IconAlertCircle size={20} />}
|
||||||
|
color="red"
|
||||||
|
title="Terjadi Kesalahan"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
{loadError}
|
||||||
|
</Alert>
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push('/admin/desa/profile/profile-desa')}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Kembali ke Halaman Utama
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ UI
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Header */}
|
||||||
|
<Group mb="sm">
|
||||||
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
</Button>
|
||||||
|
<Title order={4} ml="sm" c="dark">
|
||||||
|
Edit Visi & Misi Desa
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '60%' }}
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="xl"
|
||||||
|
radius="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Box>
|
||||||
|
<Title order={3} mb={4}>
|
||||||
|
Informasi Visi & Misi Desa
|
||||||
|
</Title>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Visi */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" size="sm" mb={8}>
|
||||||
|
Visi
|
||||||
|
</Text>
|
||||||
|
<EditEditor value={formData.visi} onChange={handleVisiChange} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Misi */}
|
||||||
|
<Box>
|
||||||
|
<Text fw="bold" size="sm" mb={8}>
|
||||||
|
Misi
|
||||||
|
</Text>
|
||||||
|
<EditEditor value={formData.misi} onChange={handleMisiChange} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Group justify="right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
|
||||||
|
}}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Card, Center, Divider, Grid, GridCol, Image, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useSnapshot } from 'valtio';
|
|
||||||
import stateProfileDesa from '../../../_state/desa/profile';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
import stateProfileDesa from '../../../_state/desa/profile';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -37,7 +37,6 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Sejarah Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Sejarah Desa" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -47,7 +46,6 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -84,7 +82,6 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Visi Misi Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Visi Misi Desa" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -94,7 +91,6 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -134,7 +130,6 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Lambang Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Lambang Desa" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -144,7 +139,6 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -181,7 +175,6 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Maskot Desa</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Maskot Desa" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -191,7 +184,6 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Tooltip
|
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';
|
||||||
@@ -36,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 () => {
|
||||||
@@ -50,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);
|
||||||
@@ -70,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 };
|
||||||
|
|
||||||
@@ -91,17 +121,17 @@ function EditPerbekelDariMasaKeMasa() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating perbekel dari masa ke masa:', error);
|
console.error('Error updating perbekel dari masa ke masa:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
toast.error('Terjadi kesalahan saat memperbarui perbekel dari masa ke masa');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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 Perbekel Dari Masa Ke Masa
|
Edit Perbekel Dari Masa Ke Masa
|
||||||
</Title>
|
</Title>
|
||||||
@@ -138,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"
|
||||||
>
|
>
|
||||||
@@ -157,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>
|
||||||
@@ -197,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"
|
||||||
@@ -207,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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
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 { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
|
import { Box, Button, Group, Image, 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';
|
||||||
@@ -98,7 +98,6 @@ function DetailPerbekelDariMasa() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<Tooltip label="Hapus Perbekel" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -111,9 +110,7 @@ function DetailPerbekelDariMasa() {
|
|||||||
>
|
>
|
||||||
<IconX size={20} />
|
<IconX size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="Edit Perbekel" withArrow position="top">
|
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
onClick={() => router.push(`/admin/desa/profile/profile-perbekel-dari-masa-ke-masa/${data.id}/edit`)}
|
||||||
@@ -123,7 +120,6 @@ function DetailPerbekelDariMasa() {
|
|||||||
>
|
>
|
||||||
<IconEdit size={20} />
|
<IconEdit size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -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, Tooltip } 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,33 +29,39 @@ function CreatePerbekelDariMasaKeMasa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!file) {
|
try {
|
||||||
return toast.warn('Pilih file gambar terlebih dahulu');
|
setIsSubmitting(true);
|
||||||
|
if (!file) {
|
||||||
|
return toast.warn('Pilih file gambar terlebih dahulu');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploaded = res.data?.data;
|
||||||
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
|
|
||||||
|
state.create.form.imageId = uploaded.id;
|
||||||
|
await state.create.create();
|
||||||
|
resetForm();
|
||||||
|
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Gagal menambahkan perbekel dari masa ke masa');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploaded = res.data?.data;
|
|
||||||
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
|
||||||
|
|
||||||
state.create.form.imageId = uploaded.id;
|
|
||||||
await state.create.create();
|
|
||||||
resetForm();
|
|
||||||
router.push('/admin/desa/profile/profile-perbekel-dari-masa-ke-masa');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Back button + Title */}
|
{/* Back button + Title */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<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">
|
||||||
Create Perbekel Dari Masa Ke Masa
|
Create Perbekel Dari Masa Ke Masa
|
||||||
</Title>
|
</Title>
|
||||||
@@ -72,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
|
||||||
/>
|
/>
|
||||||
@@ -104,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"
|
||||||
>
|
>
|
||||||
@@ -120,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"
|
||||||
@@ -149,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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } 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';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -49,7 +49,6 @@ function ListPerbekelDariMasaKeMasa({ 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 Perbekel Dari Masa Ke Masa</Title>
|
<Title order={4}>List Perbekel Dari Masa Ke Masa</Title>
|
||||||
<Tooltip label="Tambah Perbekel Baru" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -58,7 +57,6 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ 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 { Box, Button, Center, Group, Image, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Center, Group, Image, Paper, Stack, Text, Title } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX, IconImageInPicture } from '@tabler/icons-react';
|
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } 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';
|
||||||
@@ -101,11 +101,9 @@ function ProfilePerbekel() {
|
|||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
|
||||||
<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>
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Profil Perbekel
|
Edit Profil Perbekel
|
||||||
</Title>
|
</Title>
|
||||||
@@ -128,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>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -36,7 +36,6 @@ function Page() {
|
|||||||
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 1 }}>
|
<GridCol span={{ base: 12, md: 1 }}>
|
||||||
<Tooltip label="Edit Profil Perbekel" withArrow>
|
|
||||||
<Button
|
<Button
|
||||||
c="green"
|
c="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -46,7 +45,6 @@ function Page() {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user