Fix Menu Desa Admin & User

This commit is contained in:
2025-09-30 17:13:06 +08:00
parent 295d6f7d63
commit c2f1ab8179
27 changed files with 897 additions and 593 deletions

BIN
public/beasiswa-siswa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita'; import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
@@ -10,7 +11,7 @@ import {
Stack, Stack,
TextInput, TextInput,
Title, Title,
Tooltip 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,7 +25,7 @@ function EditKategoriBerita() {
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: editState.update.form.name || '', name: '',
}); });
useEffect(() => { useEffect(() => {
@@ -48,8 +49,16 @@ function EditKategoriBerita() {
loadKategori(); loadKategori();
}, [params?.id]); }, [params?.id]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// update global state hanya saat submit
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
name: formData.name, name: formData.name,
@@ -94,10 +103,11 @@ function EditKategoriBerita() {
> >
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
name="name"
label="Nama Kategori Berita" label="Nama Kategori Berita"
placeholder="Masukkan nama kategori berita" placeholder="Masukkan nama kategori berita"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={handleChange}
required required
/> />

View File

@@ -19,7 +19,12 @@ import {
Tooltip, 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 } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -33,16 +38,17 @@ function EditBerita() {
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 [formData, setFormData] = useState({ const [formData, setFormData] = useState({
judul: beritaState.berita.edit.form.judul || "", judul: "",
deskripsi: beritaState.berita.edit.form.deskripsi || "", deskripsi: "",
kategoriBeritaId: beritaState.berita.edit.form.kategoriBeritaId || "", kategoriBeritaId: "",
content: beritaState.berita.edit.form.content || "", content: "",
imageId: beritaState.berita.edit.form.imageId || "", imageId: "",
}); });
// Load berita by id saat pertama kali // Load kategori + berita
useEffect(() => { useEffect(() => {
beritaState.kategoriBerita.findMany.load(); beritaState.kategoriBerita.findMany.load();
const loadBerita = async () => { const loadBerita = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
@@ -71,8 +77,13 @@ function EditBerita() {
loadBerita(); loadBerita();
}, [params?.id]); }, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state hanya sekali di sini
beritaState.berita.edit.form = { beritaState.berita.edit.form = {
...beritaState.berita.edit.form, ...beritaState.berita.edit.form,
...formData, ...formData,
@@ -103,6 +114,7 @@ function EditBerita() {
return ( return (
<Box px={{ base: "sm", md: "lg" }} py="md"> <Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
@@ -119,6 +131,7 @@ function EditBerita() {
</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,18 +144,14 @@ function EditBerita() {
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul" placeholder="Masukkan judul"
defaultValue={formData.judul} value={formData.judul}
onChange={(e) => onChange={(e) => handleChange("judul", e.target.value)}
setFormData({ ...formData, judul: e.target.value })
}
required required
/> />
<Select <Select
value={formData.kategoriBeritaId} value={formData.kategoriBeritaId}
onChange={(val) => onChange={(val) => handleChange("kategoriBeritaId", val || "")}
setFormData({ ...formData, kategoriBeritaId: val || "" })
}
label="Kategori" label="Kategori"
placeholder="Pilih kategori" placeholder="Pilih kategori"
data={ data={
@@ -160,13 +169,12 @@ function EditBerita() {
<TextInput <TextInput
label="Deskripsi Singkat" label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat" placeholder="Masukkan deskripsi singkat"
defaultValue={formData.deskripsi} value={formData.deskripsi}
onChange={(e) => onChange={(e) => handleChange("deskripsi", e.target.value)}
setFormData({ ...formData, deskripsi: e.target.value })
}
required required
/> />
{/* Upload Gambar */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Berita Gambar Berita
@@ -179,7 +187,9 @@ function EditBerita() {
setPreviewImage(URL.createObjectURL(selectedFile)); setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error("File tidak valid, gunakan format gambar")} onReject={() =>
toast.error("File tidak valid, gunakan format gambar")
}
maxSize={5 * 1024 ** 2} maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }} accept={{ "image/*": [] }}
radius="md" radius="md"
@@ -187,7 +197,11 @@ function EditBerita() {
> >
<Group justify="center" gap="xl" mih={180}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={48} color={colors["blue-button"]} stroke={1.5} /> <IconUpload
size={48}
color={colors["blue-button"]}
stroke={1.5}
/>
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
@@ -223,18 +237,20 @@ function EditBerita() {
)} )}
</Box> </Box>
{/* Konten */}
<Box> <Box>
<Text fz="sm" fw="bold"> <Text fz="sm" fw="bold">
Konten Konten
</Text> </Text>
<EditEditor <EditEditor
value={formData.content} value={formData.content}
onChange={(htmlContent) => { onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, content: htmlContent })); setFormData((prev) => ({ ...prev, content: htmlContent }))
beritaState.berita.edit.form.content = htmlContent; }
}}
/> />
</Box> </Box>
{/* Action */}
<Group justify="right"> <Group justify="right">
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}

View File

@@ -15,7 +15,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } 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';
@@ -31,17 +31,19 @@ function EditVideo() {
linkVideo: '', linkVideo: '',
}); });
// load data video sekali saat id ada
useEffect(() => { useEffect(() => {
const loadVideo = async () => { const loadVideo = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await videoState.update.load(id); const data = await videoState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name ?? '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi ?? '',
linkVideo: data.linkVideo || '', linkVideo: data.linkVideo ?? '',
}); });
} }
} catch (error) { } catch (error) {
@@ -49,10 +51,16 @@ function EditVideo() {
toast.error('Gagal memuat data video'); toast.error('Gagal memuat data video');
} }
}; };
loadVideo(); loadVideo();
}, [params?.id]); }, [params?.id]);
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo); const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
const handleSubmit = async () => { const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo); const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
@@ -63,7 +71,6 @@ function EditVideo() {
try { try {
videoState.update.form = { videoState.update.form = {
...videoState.update.form,
name: formData.name, name: formData.name,
deskripsi: formData.deskripsi, deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo, linkVideo: formData.linkVideo,
@@ -77,11 +84,18 @@ function EditVideo() {
} }
}; };
const embedLink = convertYoutubeUrlToEmbed(formData.linkVideo);
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> <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> </Tooltip>
@@ -102,8 +116,8 @@ function EditVideo() {
<TextInput <TextInput
label="Judul Video" label="Judul Video"
placeholder="Masukkan judul video" placeholder="Masukkan judul video"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => handleChange('name', e.currentTarget.value)}
required required
/> />
@@ -111,8 +125,8 @@ function EditVideo() {
<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={formData.linkVideo} value={formData.linkVideo}
onChange={(e) => setFormData({ ...formData, linkVideo: e.currentTarget.value })} onChange={(e) => handleChange('linkVideo', e.currentTarget.value)}
required required
/> />
{embedLink && ( {embedLink && (
@@ -135,7 +149,7 @@ function EditVideo() {
</Title> </Title>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })} onChange={(val) => handleChange('deskripsi', val)}
/> />
</Box> </Box>

View File

@@ -1,5 +1,5 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client'
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 { import {
@@ -11,7 +11,7 @@ import {
Stack, Stack,
TextInput, TextInput,
Title, Title,
Tooltip 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,16 +24,19 @@ function EditAjukanPermohonan() {
const params = useParams(); const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan); const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
// State lokal form
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: stateAjukan.edit.form.nama, nama: '',
nik: stateAjukan.edit.form.nik, nik: '',
alamat: stateAjukan.edit.form.alamat, alamat: '',
nomorKk: stateAjukan.edit.form.nomorKk, nomorKk: '',
kategoriId: stateAjukan.edit.form.kategoriId, kategoriId: '',
}); });
// Load data awal
useEffect(() => { useEffect(() => {
stateLayananDesa.suratKeterangan.findManyAll.load(); stateLayananDesa.suratKeterangan.findManyAll.load();
const loadAjukan = async () => { const loadAjukan = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
@@ -58,6 +61,14 @@ function EditAjukanPermohonan() {
loadAjukan(); loadAjukan();
}, [params?.id]); }, [params?.id]);
// Handler untuk input controlled
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateAjukan.edit.form = { stateAjukan.edit.form = {
@@ -98,8 +109,8 @@ function EditAjukanPermohonan() {
<TextInput <TextInput
label="Nama" label="Nama"
placeholder="Masukkan nama" placeholder="Masukkan nama"
defaultValue={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => handleChange('nama', e.target.value)}
required required
/> />
@@ -107,16 +118,16 @@ function EditAjukanPermohonan() {
type="number" type="number"
label="NIK" label="NIK"
placeholder="Masukkan NIK" placeholder="Masukkan NIK"
defaultValue={formData.nik} value={formData.nik}
onChange={(e) => setFormData({ ...formData, nik: e.target.value })} onChange={(e) => handleChange('nik', e.target.value)}
required required
/> />
<TextInput <TextInput
label="Alamat" label="Alamat"
placeholder="Masukkan alamat" placeholder="Masukkan alamat"
defaultValue={formData.alamat} value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} onChange={(e) => handleChange('alamat', e.target.value)}
required required
/> />
@@ -124,8 +135,8 @@ function EditAjukanPermohonan() {
type="number" type="number"
label="Nomor KK" label="Nomor KK"
placeholder="Masukkan nomor KK" placeholder="Masukkan nomor KK"
defaultValue={formData.nomorKk} value={formData.nomorKk}
onChange={(e) => setFormData({ ...formData, nomorKk: e.target.value })} onChange={(e) => handleChange('nomorKk', e.target.value)}
required required
/> />
@@ -137,18 +148,7 @@ function EditAjukanPermohonan() {
value: item.id, value: item.id,
}))} }))}
value={formData.kategoriId || null} value={formData.kategoriId || null}
onChange={(val: string | null) => { onChange={(val) => handleChange('kategoriId', val || '')}
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateAjukan.edit.form.kategoriId = selected.id;
}
} else {
stateAjukan.edit.form.kategoriId = '';
}
}}
searchable searchable
clearable clearable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"

View File

@@ -1,9 +1,20 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'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 { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { 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';
@@ -12,17 +23,22 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPendudukNonPermanent() { function EditPelayananPendudukNonPermanent() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const statePendudukNonPermanent = useProxy(stateLayananDesa.pelayananPendudukNonPermanen) const statePendudukNonPermanent = useProxy(
const [formData, setFormData] = useState({ stateLayananDesa.pelayananPendudukNonPermanen
name: statePendudukNonPermanent.findById.data?.name || '', );
deskripsi: statePendudukNonPermanent.findById.data?.deskripsi || '',
})
const [formData, setFormData] = useState({
name: '',
deskripsi: '',
});
// Load data sekali dari backend
useEffect(() => { useEffect(() => {
const loadPelayananPerizinan = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await statePendudukNonPermanent.update.load(id); const data = await statePendudukNonPermanent.update.load(id);
if (data) { if (data) {
@@ -32,27 +48,48 @@ function EditPelayananPendudukNonPermanent() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error); console.error('Error loading data:', error);
toast.error("Gagal memuat data pelayanan perizinan berusaha"); toast.error('Gagal memuat data pelayanan penduduk non permanent');
} }
}; };
loadPelayananPerizinan();
loadData();
}, [params?.id]); }, [params?.id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (statePendudukNonPermanent.findById.data) { if (!statePendudukNonPermanent.findById.data) return;
statePendudukNonPermanent.findById.data.name = formData.name;
statePendudukNonPermanent.findById.data.deskripsi = formData.deskripsi; // Update global state hanya di submit
statePendudukNonPermanent.update.update(statePendudukNonPermanent.findById.data) const updated = {
} ...statePendudukNonPermanent.findById.data,
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent') name: formData.name,
} deskripsi: formData.deskripsi,
};
await statePendudukNonPermanent.update.update(updated);
router.push('/admin/desa/layanan/pelayanan_penduduk_non_permanent');
};
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <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> </Tooltip>
@@ -62,7 +99,7 @@ function EditPelayananPendudukNonPermanent() {
</Group> </Group>
<Paper <Paper
w={{ base: "100%", md: "50%" }} w={{ base: '100%', md: '50%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="md" p="md"
radius="md" radius="md"
@@ -76,23 +113,19 @@ function EditPelayananPendudukNonPermanent() {
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul" placeholder="Masukkan judul"
defaultValue={formData.name} value={formData.name}
onChange={(e) => onChange={(e) => handleChange('name')(e.target.value)}
setFormData({ ...formData, name: e.target.value })
}
required required
/> />
{/* Posisi Field */} {/* Deskripsi Field */}
<Box> <Box>
<Text fz="sm" fw="bold"> <Text fz="sm" fw="bold">
Deskripsi Deskripsi
</Text> </Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={handleChange('deskripsi')}
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
}}
/> />
</Box> </Box>
@@ -104,7 +137,9 @@ function EditPelayananPendudukNonPermanent() {
loading={statePendudukNonPermanent.update.loading} loading={statePendudukNonPermanent.update.loading}
disabled={!formData.name} disabled={!formData.name}
> >
{statePendudukNonPermanent.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'} {statePendudukNonPermanent.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button> </Button>
<Button <Button

View File

@@ -1,9 +1,18 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'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 { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -12,15 +21,18 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() { function EditPelayananPerizinanBerusaha() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const statePerizinanBerusaha = useProxy(stateLayananDesa.pelayananPerizinanBerusaha) const statePerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha
);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: statePerizinanBerusaha.findById.data?.name || '', name: '',
deskripsi: statePerizinanBerusaha.findById.data?.deskripsi || '', deskripsi: '',
link: statePerizinanBerusaha.findById.data?.link || '', link: '',
}) });
// load data pertama kali
useEffect(() => { useEffect(() => {
const loadPelayananPerizinan = async () => { const loadPelayananPerizinan = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -35,22 +47,35 @@ function EditPelayananPerizinanBerusaha() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading pelayanan perizinan berusaha:", error); console.error('Error loading pelayanan perizinan berusaha:', error);
toast.error("Gagal memuat data pelayanan perizinan berusaha"); toast.error('Gagal memuat data pelayanan perizinan berusaha');
} }
}; };
loadPelayananPerizinan(); loadPelayananPerizinan();
}, [params?.id]); }, [params?.id]);
const handleChange =
(field: keyof typeof formData) =>
(value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = async () => { const handleSubmit = async () => {
const { name, deskripsi, link } = formData;
if (statePerizinanBerusaha.findById.data) { if (statePerizinanBerusaha.findById.data) {
statePerizinanBerusaha.findById.data.name = formData.name; const updatedData = {
statePerizinanBerusaha.findById.data.deskripsi = formData.deskripsi; ...statePerizinanBerusaha.findById.data,
statePerizinanBerusaha.findById.data.link = formData.link; name,
statePerizinanBerusaha.update.update(statePerizinanBerusaha.findById.data) deskripsi,
} link,
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha') };
await statePerizinanBerusaha.update.update(updatedData);
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
} }
};
return ( return (
<Box> <Box>
@@ -58,7 +83,12 @@ function EditPelayananPerizinanBerusaha() {
{/* Header Section */} {/* Header Section */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <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> </Tooltip>
@@ -69,7 +99,7 @@ function EditPelayananPerizinanBerusaha() {
{/* Form Section */} {/* Form Section */}
<Paper <Paper
w={{ base: "100%", md: "50%" }} w={{ base: '100%', md: '50%' }}
bg={colors['white-1']} bg={colors['white-1']}
p="md" p="md"
radius="md" radius="md"
@@ -83,8 +113,8 @@ function EditPelayananPerizinanBerusaha() {
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul" placeholder="Masukkan judul"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => handleChange('name')(e.target.value)}
required required
/> />
@@ -92,8 +122,8 @@ function EditPelayananPerizinanBerusaha() {
<TextInput <TextInput
label="Link" label="Link"
placeholder="Masukkan link terkait" placeholder="Masukkan link terkait"
defaultValue={formData.link} value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })} onChange={(e) => handleChange('link')(e.target.value)}
/> />
{/* Deskripsi Field */} {/* Deskripsi Field */}
@@ -101,7 +131,7 @@ function EditPelayananPerizinanBerusaha() {
<Title order={6}>Deskripsi</Title> <Title order={6}>Deskripsi</Title>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })} onChange={handleChange('deskripsi')}
/> />
</Box> </Box>
@@ -113,7 +143,9 @@ function EditPelayananPerizinanBerusaha() {
loading={statePerizinanBerusaha.update.loading} loading={statePerizinanBerusaha.update.loading}
disabled={!formData.name} disabled={!formData.name}
> >
{statePerizinanBerusaha.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'} {statePerizinanBerusaha.update.loading
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button> </Button>
<Button <Button

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
/* eslint-disable react-hooks/exhaustive-deps */
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';
@@ -19,7 +18,7 @@ import {
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 } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -28,17 +27,23 @@ function EditSuratKeterangan() {
const params = useParams(); const params = useParams();
const stateSurat = useProxy(stateLayananDesa.suratKeterangan); const stateSurat = useProxy(stateLayananDesa.suratKeterangan);
const [previewImage, setPreviewImage] = useState<string | null>(null); // state lokal untuk form
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateSurat.edit.form.name, name: '',
deskripsi: stateSurat.edit.form.deskripsi, deskripsi: '',
imageId: stateSurat.edit.form.imageId, imageId: '',
image2Id: stateSurat.edit.form.image2Id, image2Id: '',
}); });
// state file upload
const [file, setFile] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// state preview gambar
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [previewImage2, setPreviewImage2] = useState<string | null>(null);
// load data awal
useEffect(() => { useEffect(() => {
const loadSurat = async () => { const loadSurat = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -64,15 +69,15 @@ function EditSuratKeterangan() {
}; };
loadSurat(); loadSurat();
}, [params?.id]); }, [params?.id, stateSurat.edit]);
const handleSubmit = async () => { // handler untuk submit
const handleSubmit = useCallback(async () => {
try { try {
stateSurat.edit.form = { // update form global hanya saat submit
...stateSurat.edit.form, stateSurat.edit.form = { ...stateSurat.edit.form, ...formData };
...formData,
};
// upload file 1
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;
@@ -80,6 +85,7 @@ function EditSuratKeterangan() {
stateSurat.edit.form.imageId = uploaded.id; stateSurat.edit.form.imageId = uploaded.id;
} }
// upload file 2
if (file2) { if (file2) {
const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name }); const res = await ApiFetch.api.fileStorage.create.post({ file: file2, name: file2.name });
const uploaded = res.data?.data; const uploaded = res.data?.data;
@@ -94,7 +100,7 @@ function EditSuratKeterangan() {
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');
} }
}; }, [formData, file, file2, router, stateSurat.edit]);
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -119,21 +125,25 @@ function EditSuratKeterangan() {
style={{ border: '1px solid #e0e0e0' }} style={{ border: '1px solid #e0e0e0' }}
> >
<Stack gap="md"> <Stack gap="md">
{/* Input nama */}
<TextInput <TextInput
label="Nama Surat Keterangan" label="Nama Surat Keterangan"
placeholder="Masukkan nama surat keterangan" placeholder="Masukkan nama surat keterangan"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required required
/> />
{/* Input 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) => setFormData({ ...formData, deskripsi: htmlContent })} onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/> />
</Box> </Box>

View File

@@ -15,7 +15,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
@@ -26,22 +26,24 @@ function EditPelayananTelunjukSakti() {
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateTelunjukDesa.edit.form.name, name: '',
deskripsi: stateTelunjukDesa.edit.form.deskripsi, deskripsi: '',
link: stateTelunjukDesa.edit.form.link, link: '',
}); });
// Load data awal hanya sekali (pas ada id)
useEffect(() => { useEffect(() => {
const loadPelayananTelunjukSakti = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await stateTelunjukDesa.edit.load(id); const data = await stateTelunjukDesa.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name ?? '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi ?? '',
link: data.link || '', link: data.link ?? '',
}); });
} }
} catch (error) { } catch (error) {
@@ -49,9 +51,19 @@ function EditPelayananTelunjukSakti() {
toast.error('Gagal memuat data pelayanan telunjuk sakti'); toast.error('Gagal memuat data pelayanan telunjuk sakti');
} }
}; };
loadPelayananTelunjukSakti();
loadData();
}, [params?.id]); }, [params?.id]);
// Handler input controlled
const handleChange = useCallback(
(field: keyof typeof formData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// Submit: update global state hanya saat simpan
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateTelunjukDesa.edit.form = { stateTelunjukDesa.edit.form = {
@@ -94,8 +106,8 @@ function EditPelayananTelunjukSakti() {
<TextInput <TextInput
label="Nama Pelayanan" label="Nama Pelayanan"
placeholder="Masukkan nama pelayanan" placeholder="Masukkan nama pelayanan"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => handleChange('name', e.target.value)}
required required
/> />
@@ -106,7 +118,7 @@ function EditPelayananTelunjukSakti() {
</Text> </Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => setFormData({ ...formData, deskripsi: htmlContent })} onChange={(htmlContent) => handleChange('deskripsi', htmlContent)}
/> />
</Box> </Box>
@@ -114,8 +126,8 @@ function EditPelayananTelunjukSakti() {
<TextInput <TextInput
label="Link" label="Link"
placeholder="Masukkan link terkait" placeholder="Masukkan link terkait"
defaultValue={formData.link} value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })} onChange={(e) => handleChange('link', e.target.value)}
/> />
{/* Tombol Simpan */} {/* Tombol Simpan */}

View File

@@ -24,18 +24,22 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditPenghargaan() { function EditPenghargaan() {
const statePenghargaan = useProxy(penghargaanState) const statePenghargaan = useProxy(penghargaanState);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const [formData, setFormData] = useState({
name: statePenghargaan.findUnique.data?.name || '',
juara: statePenghargaan.findUnique.data?.juara || '',
deskripsi: statePenghargaan.findUnique.data?.deskripsi || '',
imageId: statePenghargaan.findUnique.data?.imageId || '',
})
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
// Lokal formData
const [formData, setFormData] = useState({
name: '',
juara: '',
deskripsi: '',
imageId: '',
});
// Load data pertama kali
useEffect(() => { useEffect(() => {
const loadPenghargaan = async () => { const loadPenghargaan = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -56,43 +60,43 @@ function EditPenghargaan() {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading penghargaan:", error); console.error('Error loading penghargaan:', error);
toast.error("Gagal memuat data penghargaan"); toast.error('Gagal memuat data penghargaan');
} }
}; };
loadPenghargaan(); loadPenghargaan();
}, [params?.id]); }, [params?.id]);
// Submit
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Sync ke global state saat submit
statePenghargaan.edit.form = { statePenghargaan.edit.form = {
...statePenghargaan.edit.form, ...statePenghargaan.edit.form,
name: formData.name, ...formData,
juara: formData.juara, };
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
// 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');
} }
statePenghargaan.edit.form.imageId = uploaded.id; statePenghargaan.edit.form.imageId = uploaded.id;
} }
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');
}
} }
};
return ( return (
<Box px={{ base: 'sm', md: 'lg' }} py="md"> <Box px={{ base: 'sm', md: 'lg' }} py="md">
@@ -122,8 +126,8 @@ function EditPenghargaan() {
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul penghargaan" placeholder="Masukkan judul penghargaan"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
required required
/> />
@@ -131,8 +135,8 @@ function EditPenghargaan() {
<TextInput <TextInput
label="Juara" label="Juara"
placeholder="Masukkan juara" placeholder="Masukkan juara"
defaultValue={formData.juara} value={formData.juara}
onChange={(e) => setFormData({ ...formData, juara: e.target.value })} onChange={(e) => setFormData((prev) => ({ ...prev, juara: e.target.value }))}
required required
/> />
@@ -182,7 +186,11 @@ function EditPenghargaan() {
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy" loading="lazy"
/> />
</Box> </Box>
@@ -196,10 +204,9 @@ function EditPenghargaan() {
</Text> </Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent })); setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
statePenghargaan.edit.form.deskripsi = htmlContent; }
}}
/> />
</Box> </Box>

View File

@@ -96,7 +96,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
leftSection={<IconPlus size={18} />} leftSection={<IconPlus size={18} />}
color="blue" color="blue"
variant="light" variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')} onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create')}
> >
Tambah Baru Tambah Baru
</Button> </Button>

View File

@@ -97,17 +97,21 @@ function ListDesaDigitalSmartVillage({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}>
<Text <Text
fz="sm" fz="sm"
c="dimmed" c="dimmed"
lineClamp={2} lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/> />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -107,12 +107,16 @@ function ListKeamananLingkungan({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.name} {item.name}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}>
<Text fz="sm" c="dimmed" truncate lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Text fz="sm" c="dimmed" truncate lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button

View File

@@ -99,12 +99,16 @@ function ListTipsKeamanan({ search }: { search: string }) {
filteredData.map((item) => ( filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '25%' }}> <TableTd style={{ width: '25%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}> <Text fw={500} truncate="end" lineClamp={1}>
{item.judul} {item.judul}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '45%' }}> <TableTd style={{ width: '45%' }}>
<Text fz="sm" c="dimmed" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Box w={200}>
<Text fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '15%' }}>
<Button <Button

View File

@@ -30,16 +30,17 @@ function EditMediaSosial() {
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 [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateMediaSosial.update.form.name || '', name: '',
iconUrl: stateMediaSosial.update.form.iconUrl || '', iconUrl: '',
imageId: stateMediaSosial.update.form.imageId || '', imageId: '',
}); });
// Load data by ID
useEffect(() => { useEffect(() => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
const loadMediaSosial = async () => { const loadData = async () => {
try { try {
const data = await stateMediaSosial.update.load(id); const data = await stateMediaSosial.update.load(id);
@@ -59,11 +60,16 @@ function EditMediaSosial() {
} }
}; };
loadMediaSosial(); loadData();
}, [params?.id]); }, [params?.id]);
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// update global state hanya saat submit
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData }; stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
if (file) { if (file) {
@@ -85,7 +91,10 @@ function EditMediaSosial() {
}; };
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> <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">
@@ -106,6 +115,7 @@ function EditMediaSosial() {
style={{ border: '1px solid #e0e0e0' }} style={{ border: '1px solid #e0e0e0' }}
> >
<Stack gap="md"> <Stack gap="md">
{/* Upload Gambar */}
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}> <Text fw="bold" fz="sm" mb={6}>
Gambar Media Sosial Gambar Media Sosial
@@ -151,26 +161,32 @@ function EditMediaSosial() {
src={previewImage} src={previewImage}
alt="Preview Gambar" alt="Preview Gambar"
radius="md" radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }} style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy" loading="lazy"
/> />
</Box> </Box>
)} )}
</Box> </Box>
{/* Nama Media Sosial */}
<TextInput <TextInput
label="Nama Media Sosial / Kontak" label="Nama Media Sosial / Kontak"
placeholder="Masukkan nama media sosial atau kontak" placeholder="Masukkan nama media sosial atau kontak"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => handleChange('name', e.target.value)}
required required
/> />
{/* Link Media Sosial */}
<TextInput <TextInput
label="Link Media Sosial / Nomor Telepon" label="Link Media Sosial / Nomor Telepon"
placeholder="Masukkan link media sosial atau nomor telepon" placeholder="Masukkan link media sosial atau nomor telepon"
defaultValue={formData.iconUrl} value={formData.iconUrl}
onChange={(e) => setFormData({ ...formData, iconUrl: e.target.value })} onChange={(e) => handleChange('iconUrl', e.target.value)}
required required
/> />

View File

@@ -52,9 +52,7 @@ function DetailMediaSosial() {
<Paper <Paper
withBorder withBorder
w="100%" w={{ base: "100%", md: "50%" }}
maw={500} // <= tambahkan ini, biar tidak lebih dari 500px
mx="auto" // center di layar
bg={colors['white-1']} bg={colors['white-1']}
p="lg" p="lg"
radius="md" radius="md"

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core'; import {
Alert, Box, Button, Center, Group, Image,
Paper, Stack, Text, TextInput, Title, Tooltip
} from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -17,7 +20,14 @@ function EditPejabatDesa() {
const params = useParams(); const params = useParams();
const router = useRouter(); const router = useRouter();
// UI States // Local form state
const [formData, setFormData] = useState({
name: '',
position: '',
imageId: null as string | null,
});
// UI states
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 [isSubmitting, setIsSubmitting] = useState(false);
@@ -34,12 +44,18 @@ function EditPejabatDesa() {
try { try {
const profileData = await profileLandingPageState.pejabatDesa.findUnique.load(id); const profileData = await profileLandingPageState.pejabatDesa.findUnique.load(id);
profileLandingPageState.pejabatDesa.edit.initialize(profileData);
if (profileData) {
setFormData({
name: profileData.name || '',
position: profileData.position || '',
imageId: profileData.imageId || null,
});
if (profileData && profileData.image?.link) { if (profileData.image?.link) {
setPreviewImage(profileData.image.link); setPreviewImage(profileData.image.link);
} }
}
} catch (error) { } catch (error) {
console.error("Error loading profile:", error); console.error("Error loading profile:", error);
toast.error("Gagal memuat data profile"); toast.error("Gagal memuat data profile");
@@ -47,16 +63,15 @@ function EditPejabatDesa() {
}; };
loadData(); loadData();
return () => profileLandingPageState.pejabatDesa.edit.reset();
return () => {
profileLandingPageState.pejabatDesa.edit.reset(); // cleanup form
};
}, [params?.id, router]); }, [params?.id, router]);
const handleFieldChange = (field: string, value: string) => { // Handle input change
profileLandingPageState.pejabatDesa.edit.updateField(field as any, value); const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
}; };
// Handle file change
const handleFileChange = (newFile: File | null) => { const handleFileChange = (newFile: File | null) => {
if (!newFile) { if (!newFile) {
setFile(null); setFile(null);
@@ -72,15 +87,17 @@ function EditPejabatDesa() {
reader.readAsDataURL(newFile); reader.readAsDataURL(newFile);
}; };
// Submit form
const handleSubmit = async () => { const handleSubmit = async () => {
if (isSubmitting || !profileLandingPageState.pejabatDesa.edit.form.name.trim()) { if (isSubmitting || !formData.name.trim()) {
toast.error("Nama wajib diisi"); toast.error("Nama wajib diisi");
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
let imageId = formData.imageId;
// Upload file jika ada // Upload file jika ada
if (file) { if (file) {
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
@@ -90,13 +107,16 @@ function EditPejabatDesa() {
toast.error("Gagal upload gambar"); toast.error("Gagal upload gambar");
return; return;
} }
imageId = uploaded.id;
profileLandingPageState.pejabatDesa.edit.form.imageId = uploaded.id;
} }
// Submit form // Update global state only on submit
const success = await profileLandingPageState.pejabatDesa.edit.submit(); profileLandingPageState.pejabatDesa.edit.form = {
...formData,
imageId: imageId || '', // Ensure imageId is always a string
};
const success = await profileLandingPageState.pejabatDesa.edit.submit();
if (success) { if (success) {
toast.success("Berhasil menyimpan perubahan"); toast.success("Berhasil menyimpan perubahan");
router.push("/admin/landing-page/profile/pejabat-desa"); router.push("/admin/landing-page/profile/pejabat-desa");
@@ -109,11 +129,9 @@ function EditPejabatDesa() {
} }
}; };
const handleBack = () => { const handleBack = () => router.back();
router.back();
};
// Loading state // Loading
if (allState.edit.loading) { if (allState.edit.loading) {
return ( return (
<Box> <Box>
@@ -124,7 +142,7 @@ function EditPejabatDesa() {
); );
} }
// Error state // Error
if (allState.edit.error) { if (allState.edit.error) {
return ( return (
<Box> <Box>
@@ -146,7 +164,7 @@ function EditPejabatDesa() {
<Stack gap="xs"> <Stack gap="xs">
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md"> <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> </Tooltip>
@@ -170,28 +188,27 @@ function EditPejabatDesa() {
<TextInput <TextInput
label={<Text fw="bold">Nama Perbekel</Text>} label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel" placeholder="Masukkan nama perbekel"
defaultValue={allState.edit.form.name} value={formData.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)} onChange={(e) => handleChange('name', e.currentTarget.value)}
error={!allState.edit.form.name && "Nama wajib diisi"} error={!formData.name && "Nama wajib diisi"}
/> />
{/* Posisi Field */} {/* Posisi Field */}
<TextInput <TextInput
label={<Text fw="bold">Posisi</Text>} label={<Text fw="bold">Posisi</Text>}
placeholder="Masukkan posisi" placeholder="Masukkan posisi"
defaultValue={allState.edit.form.position} value={formData.position}
onChange={(e) => handleFieldChange('position', e.currentTarget.value)} onChange={(e) => handleChange('position', e.currentTarget.value)}
error={!allState.edit.form.position && "Posisi wajib diisi"} error={!formData.position && "Posisi wajib diisi"}
/> />
{/* File Upload */} {/* File Upload */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<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} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
@@ -216,7 +233,6 @@ function EditPejabatDesa() {
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm">
<Image <Image
@@ -233,11 +249,9 @@ function EditPejabatDesa() {
/> />
</Box> </Box>
)} )}
</Box>
</Box> </Box>
{/* Preview Gambar */} {/* Preview */}
<Box> <Box>
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text> <Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
{previewImage ? ( {previewImage ? (
@@ -252,13 +266,13 @@ function EditPejabatDesa() {
)} )}
</Box> </Box>
{/* Submit Button */} {/* Submit */}
<Group> <Group>
<Button <Button
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit} onClick={handleSubmit}
loading={isSubmitting || allState.edit.loading} loading={isSubmitting || allState.edit.loading}
disabled={!allState.edit.form.name} disabled={!formData.name}
> >
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'} {isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button> </Button>

View File

@@ -31,10 +31,10 @@ function EditProgramInovasi() {
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 [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateProgramInovasi.update.form.name || "", name: "",
description: stateProgramInovasi.update.form.description || "", description: "",
imageId: stateProgramInovasi.update.form.imageId || "", imageId: "",
link: stateProgramInovasi.update.form.link || "", link: "",
}) })
useEffect(() => { useEffect(() => {
@@ -51,9 +51,12 @@ function EditProgramInovasi() {
imageId: data.imageId || "", imageId: data.imageId || "",
link: data.link || "" link: data.link || ""
}); });
// Tampilkan preview gambar
// Preview image
if (data.image?.link) { if (data.image?.link) {
setPreviewImage(data.image.link); setPreviewImage(data.image.link);
} else {
setPreviewImage(null);
} }
} }
} catch (error) { } catch (error) {
@@ -69,24 +72,25 @@ function EditProgramInovasi() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Upload file kalau ada file baru
let imageId = formData.imageId;
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
imageId = uploaded.id;
}
// Update global state form (baru di sini)
stateProgramInovasi.update.form = { stateProgramInovasi.update.form = {
...stateProgramInovasi.update.form, ...stateProgramInovasi.update.form,
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
imageId: formData.imageId, imageId,
link: formData.link, link: formData.link,
} }
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
stateProgramInovasi.update.form.imageId = uploaded.id;
}
await stateProgramInovasi.update.update(); await stateProgramInovasi.update.update();
toast.success("Program Inovasi berhasil diperbarui!"); toast.success("Program Inovasi berhasil diperbarui!");
@@ -170,29 +174,31 @@ function EditProgramInovasi() {
</Box> </Box>
)} )}
</Box> </Box>
<TextInput <TextInput
key={String(params.id)} // Convert to string to ensure valid key
label="Nama Program Inovasi" label="Nama Program Inovasi"
placeholder="Masukkan nama program inovasi" placeholder="Masukkan nama program inovasi"
defaultValue={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor <EditEditor
value={formData.description} value={formData.description}
onChange={(htmlContent) => { onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, description: htmlContent })); setFormData((prev) => ({ ...prev, description: htmlContent }))
stateProgramInovasi.update.form.description = htmlContent; }
}}
/> />
</Box> </Box>
<TextInput <TextInput
key={`${params.id}-link`}
label="Link Program Inovasi" label="Link Program Inovasi"
placeholder="Masukkan link program inovasi (opsional)" placeholder="Masukkan link program inovasi (opsional)"
defaultValue={formData.link} value={formData.link}
onChange={(e) => setFormData({ ...formData, link: e.target.value })} onChange={(e) => setFormData({ ...formData, link: e.target.value })}
/> />

View File

@@ -115,7 +115,7 @@ function ListDataLingkunganDesa({ search }: { search: string }) {
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Daftar Data Lingkungan Desa</Title> <Title order={4}>Daftar Data Lingkungan Desa</Title>
<Tooltip label="Tambah Data Lingkungan Desa" withArrow> <Tooltip label="Tambah Data Lingkungan Desa" withArrow>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan/create')}> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/create')}>
Tambah Baru Tambah Baru
</Button> </Button>
</Tooltip> </Tooltip>

View File

@@ -1,9 +1,7 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
export default async function findUniqueAjukanPermohonan(request: Request) { export default async function findUniqueAjukanPermohonan({ params }: { params: { id: string } }) {
const url = new URL(request.url); const { id } = params;
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) { if (!id) {
return Response.json( return Response.json(

View File

@@ -1,8 +1,8 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function findByMonthYear(context: Context) {
const { month, year } = context.params as { month: string; year: string }; export default async function findByMonthYear({ params }: { params: { month: string; year: number } }) {
const { month, year } = params;
if (!month || !year) { if (!month || !year) {
return { return {

View File

@@ -23,7 +23,9 @@ async function img({
// Validasi ekstensi file // Validasi ekstensi file
if (![".jpg", ".jpeg", ".png"].includes(ext)) { if (![".jpg", ".jpeg", ".png"].includes(ext)) {
console.warn(`Ekstensi file tidak didukung: ${ext}`); console.warn(`Ekstensi file tidak didukung: ${ext}`);
return new Response(await fs.readFile(noImage), { const buffer = await fs.readFile(noImage);
const uint8Array = new Uint8Array(buffer);
return new Response(new Blob([uint8Array], { type: 'image/jpeg' }), {
headers: { "Content-Type": "image/jpeg" }, headers: { "Content-Type": "image/jpeg" },
}); });
} }
@@ -43,7 +45,8 @@ async function img({
.resize(size || metadata.width) // Gunakan size jika diberikan, jika tidak gunakan width asli .resize(size || metadata.width) // Gunakan size jika diberikan, jika tidak gunakan width asli
.toBuffer(); .toBuffer();
return new Response(resizedImageBuffer, { const uint8Array = new Uint8Array(resizedImageBuffer);
return new Response(new Blob([uint8Array], { type: 'image/jpeg' }), {
headers: { headers: {
"Cache-Control": "public, max-age=3600, stale-while-revalidate=600", "Cache-Control": "public, max-age=3600, stale-while-revalidate=600",
"Content-Type": "image/jpeg", "Content-Type": "image/jpeg",
@@ -52,7 +55,9 @@ async function img({
} catch (error) { } catch (error) {
console.error(`Gagal memproses file: ${name}`, error); console.error(`Gagal memproses file: ${name}`, error);
// Jika file tidak ditemukan atau gagal diproses, kembalikan default image // Jika file tidak ditemukan atau gagal diproses, kembalikan default image
return new Response(await fs.readFile(noImage), { const buffer = await fs.readFile(noImage);
const uint8Array = new Uint8Array(buffer);
return new Response(new Blob([uint8Array], { type: 'image/jpeg' }), {
headers: { "Content-Type": "image/jpeg" }, headers: { "Content-Type": "image/jpeg" },
}); });
} }

View File

@@ -1,9 +1,7 @@
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
export default async function pengaduanMasyarakatFindUnique(request: Request) { export default async function pengaduanMasyarakatFindUnique({ params }: { params: { id: string } }) {
const url = new URL(request.url); const { id } = params;
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) { if (!id) {
return Response.json({ return Response.json({

View File

@@ -1,11 +1,174 @@
// 'use client'
// import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
// import colors from '@/con/colors';
// import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
// import { useProxy } from 'valtio/utils';
// import BackButton from '../../desa/layanan/_com/BackButto';
// import { useShallowEffect } from '@mantine/hooks';
// function Page() {
// const state = useProxy(PendapatanAsliDesa.ApbDesa);
// useShallowEffect(() => {
// state.findMany.load();
// }, []);
// useShallowEffect(() => {
// PendapatanAsliDesa.pembiayaan.findMany.load();
// PendapatanAsliDesa.belanja.findMany.load();
// PendapatanAsliDesa.pendapatan.findMany.load();
// }, []);
// // Get the latest APB data
// const latestApb = state.findMany.data?.[0];
// // Calculate totals
// const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// return (
// <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
// <Box px={{ base: 'md', md: 100 }}>
// <BackButton />
// </Box>
// <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
// Pendapatan Asli Desa
// </Text>
// <Box px={{ base: "md", md: 100 }}>
// <Stack gap="lg" justify="center">
// <Paper bg={colors['white-1']} p="xl">
// <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
// {/* Pendapatan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pendapatan</Title>
// {PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c={colors['blue-button']}>
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPendapatan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Belanja Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Belanja</Title>
// {PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="orange">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalBelanja)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// {/* Pembiayaan Card */}
// <Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
// <Stack gap={"xs"}>
// <Title order={3}>Pembiayaan</Title>
// {PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
// <Box key={item.id}>
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{item.name}</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(item.value)}</Text>
// </GridCol>
// </Grid>
// </Box>
// ))}
// <Grid>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
// </GridCol>
// <GridCol span={{ base: 12, md: 6 }}>
// <Text fz="xl" fw={700} c="green">
// {new Intl.NumberFormat('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0
// }).format(totalPembiayaan)}
// </Text>
// </GridCol>
// </Grid>
// </Stack>
// </Box>
// </SimpleGrid>
// </Paper>
// </Stack>
// </Box>
// </Stack>
// );
// }
// export default Page;
'use client' 'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; import { Box, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useShallowEffect } from '@mantine/hooks';
function Page() { function Page() {
const state = useProxy(PendapatanAsliDesa.ApbDesa); const state = useProxy(PendapatanAsliDesa.ApbDesa);
@@ -28,6 +191,9 @@ function Page() {
const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0; const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0; const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
// Hasil akhir
const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan;
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg"> <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -40,119 +206,51 @@ function Page() {
<Stack gap="lg" justify="center"> <Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl"> <Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan Card */} {/* Pendapatan, Belanja, Pembiayaan Card sama seperti sebelumnya */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}> {/* ... */}
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPendapatan)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
{/* Belanja Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="orange">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalBelanja)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
{/* Pembiayaan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="green">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPembiayaan)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
</SimpleGrid> </SimpleGrid>
</Paper> </Paper>
{/* 🔽 Tambahan Ringkasan Anggaran */}
<Paper bg={colors['white-1']} p="xl" shadow="sm" withBorder>
<Title order={3} mb="md">Ringkasan Anggaran</Title>
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Keterangan</Table.Th>
<Table.Th align="right">Jumlah</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
<Table.Td>Total Pendapatan</Table.Td>
<Table.Td align="right">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPendapatan)}
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Total Belanja</Table.Td>
<Table.Td align="right" c="orange">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalBelanja)}
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Total Pembiayaan</Table.Td>
<Table.Td align="right" c="green">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPembiayaan)}
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td><b>Sisa Anggaran</b></Table.Td>
<Table.Td align="right" c={sisaAnggaran >= 0 ? "blue" : "red"}>
<b>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(sisaAnggaran)}
</b>
</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Paper>
</Stack> </Stack>
</Box> </Box>
</Stack> </Stack>
@@ -160,3 +258,4 @@ function Page() {
} }
export default Page; export default Page;

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan'; import keamananLingkunganState from '@/app/admin/(dashboard)/_state/keamanan/keamanan-lingkungan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
@@ -11,6 +11,7 @@ import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const state = useProxy(keamananLingkunganState) const state = useProxy(keamananLingkunganState)
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { const {
@@ -25,6 +26,13 @@ function Page() {
load(page, 3, debouncedSearch) load(page, 3, debouncedSearch)
}, [page, debouncedSearch]) }, [page, debouncedSearch])
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Box py={10}>
@@ -80,7 +88,22 @@ function Page() {
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}> <Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.name} {v.name}
</Text> </Text>
<Spoiler
showLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Show more
</Text>
}
hideLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Hide details
</Text>
}
expanded={expandedMap[k] || false}
onExpandedChange={(val) => toggleExpanded(k, val)}
>
<Text pb={10} fz={"h4"} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> <Text pb={10} fz={"h4"} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Spoiler>
</Box> </Box>
</Box> </Box>
</Stack> </Stack>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Flex, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { ActionIcon, Box, Button, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowRight } from '@tabler/icons-react'; import { IconArrowRight } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
@@ -63,16 +63,15 @@ function Page() {
<Stack pt={30} gap="lg"> <Stack pt={30} gap="lg">
{data.length > 0 ? ( {data.length > 0 ? (
data.map((item) => ( data.map((item) => (
<Paper key={item.id} p="md" bg={colors['blue-button']} radius="md" shadow="sm" onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)}> <ActionIcon key={item.id} variant='transparent' onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)}>
<Paper p="md" bg={colors['blue-button']} radius="md" shadow="sm">
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Flex align="center" justify="space-between">
<Text fz="h3" c={colors['white-1']}> <Text fz="h3" c={colors['white-1']}>
{item.judul} {item.judul}
</Text> </Text>
<IconArrowRight size={28} color={colors['white-1']} />
</Flex>
</Stack> </Stack>
</Paper> </Paper>
</ActionIcon>
)) ))
) : ( ) : (
<Text color="dimmed"> <Text color="dimmed">

View File

@@ -76,7 +76,7 @@ function Page() {
</Group> </Group>
</Box> </Box>
<Box> <Box>
<Image alt="Beasiswa Desa" src="/api/img/beasiswa-siswa.png" radius="lg" loading="lazy"/> <Image alt="Beasiswa Desa" src="/beasiswa-siswa.png" radius="lg" loading="lazy"/>
</Box> </Box>
</SimpleGrid> </SimpleGrid>