Merge pull request 'Fix Seeder Image, Menu Landing Page - Desa' (#56) from nico/30-jan-26 into staggingweb

Reviewed-on: http://wibugit.wibudev.com/wibu/desa-darmasaba/pulls/56
This commit is contained in:
2026-01-30 15:57:16 +08:00
61 changed files with 4533 additions and 5162 deletions

View File

@@ -552,7 +552,7 @@ const maskotDesa = proxy({
deskripsi: profileData.deskripsi || "",
images: (profileData.images || []).map((img) => ({
label: img.label,
imageId: img.image.id,
imageId: img?.image?.id || "",
})),
};
},

View File

@@ -462,7 +462,7 @@ function Page() {
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
<Center>
<Image
src={img.image.link}
src={ img?.image?.link || '/no-image.jpg'}
alt={img.label}
w={150}
h={150}
@@ -562,7 +562,7 @@ function Page() {
<Card withBorder key={idx} p="xs" w={{ base: '100%', md: 180 }}>
<Center>
<Image
src={img.image.link}
src={img?.image?.link || '/no-image.jpg'}
alt={img.label}
w={150}
h={150}

View File

@@ -2,77 +2,91 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function maskotDesaUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = await context.body as {
judul: string;
deskripsi: string;
images: { label: string; imageId: string }[];
};
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak boleh kosong",
}), { status: 400 });
}
const existing = await prisma.maskotDesa.findUnique({
where: { id },
include: { images: { include: { image: true } } }
});
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Data tidak ditemukan",
}), { status: 404 });
}
// Hapus semua gambar lama (dan file-nya jika perlu)
for (const old of existing.images) {
try {
await prisma.fileStorage.delete({ where: { id: old.imageId } });
// opsional: hapus file dari disk juga kalau kamu simpan file fisik
// await fs.unlink(path.join(old.image.path, old.image.name));
} catch (error) {
console.warn("Gagal hapus gambar lama:", error);
}
}
// Update profile & re-create images
const updated = await prisma.maskotDesa.update({
where: { id },
data: {
judul: body.judul,
deskripsi: body.deskripsi,
images: {
deleteMany: {},
create: body.images.map((img) => ({
label: img.label,
imageId: img.imageId
}))
}
},
include: {
images: {
include: {
image: true
}
}
}
});
return new Response(JSON.stringify({
success: true,
message: "Data berhasil diperbarui",
data: updated,
}), { status: 200 });
} catch (error) {
console.error("Gagal update MaskotDesa:", error);
return new Response(JSON.stringify({
try {
const id = context.params?.id as string;
const body = (await context.body) as {
judul: string;
deskripsi: string;
images: { label: string; imageId: string }[];
};
if (!id) {
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat update",
}), { status: 500 });
message: "ID tidak boleh kosong",
}),
{ status: 400 },
);
}
const existing = await prisma.maskotDesa.findUnique({
where: { id },
include: { images: { include: { image: true } } },
});
if (!existing) {
return new Response(
JSON.stringify({
success: false,
message: "Data tidak ditemukan",
}),
{ status: 404 },
);
}
// Hapus semua gambar lama (dan file-nya jika perlu)
for (const old of existing.images) {
try {
if (old.imageId) {
await prisma.fileStorage.delete({ where: { id: old.imageId } });
}
// opsional: hapus file dari disk juga kalau kamu simpan file fisik
// await fs.unlink(path.join(old.image.path, old.image.name));
} catch (error) {
console.warn("Gagal hapus gambar lama:", error);
}
}
}
// Update profile & re-create images
const updated = await prisma.maskotDesa.update({
where: { id },
data: {
judul: body.judul,
deskripsi: body.deskripsi,
images: {
deleteMany: {},
create: body.images.map((img) => ({
label: img.label,
imageId: img.imageId,
})),
},
},
include: {
images: {
include: {
image: true,
},
},
},
});
return new Response(
JSON.stringify({
success: true,
message: "Data berhasil diperbarui",
data: updated,
}),
{ status: 200 },
);
} catch (error) {
console.error("Gagal update MaskotDesa:", error);
return new Response(
JSON.stringify({
success: false,
message: "Terjadi kesalahan saat update",
}),
{ status: 500 },
);
}
}

View File

@@ -61,7 +61,7 @@ function Semua() {
<Grid gutter={0}>
<GridCol span={{ base: 12, md: 6 }}>
<Image
src={featuredData.image?.link || '/images/placeholder.jpg'}
src={featuredData.image?.link || '/images/placeholderx.jpg'}
alt={featuredData.judul || 'Berita Utama'}
height={400}
fit="cover"

View File

@@ -83,7 +83,7 @@ function MaskotDesa() {
className="hover:scale-105 hover:shadow-lg"
>
<Image
src={img.image.link}
src={img.image?.link || '/no-image.jpg'}
alt={img.label}
w="100%"
h={200}

View File

@@ -19,20 +19,58 @@ interface APBDesProgressProps {
function APBDesProgress({ apbdesData }: APBDesProgressProps) {
// Return null if apbdesData is not available yet
if (!apbdesData) {
return null;
return (
<Paper
mx={{ base: 'md', md: 100 }}
p="xl"
radius="md"
shadow="sm"
withBorder
bg={colors['white-1']}
>
<Stack gap="lg">
<Title order={4} c={colors['blue-button']} ta="center">
Grafik Pelaksanaan APBDes
</Title>
<Text ta="center">Tidak ada data APBDes tersedia.</Text>
</Stack>
</Paper>
);
}
const items = Array.isArray(apbdesData.items) ? apbdesData.items : [];
// Show message if no items exist
if (items.length === 0) {
return (
<Paper
mx={{ base: 'md', md: 100 }}
p="xl"
radius="md"
shadow="sm"
withBorder
bg={colors['white-1']}
>
<Stack gap="lg">
<Title order={4} c={colors['blue-button']} ta="center">
Grafik Pelaksanaan APBDes Tahun {apbdesData.tahun || 'N/A'}
</Title>
<Text ta="center">Tidak ada rincian anggaran tersedia untuk tahun ini.</Text>
</Stack>
</Paper>
);
}
const items = apbdesData.items || [];
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
// Kelompokkan berdasarkan tipe
const pendapatanItems = sortedItems.filter(item => item.tipe === 'pendapatan');
const belanjaItems = sortedItems.filter(item => item.tipe === 'belanja');
const pembiayaanItems = sortedItems.filter(item => item.tipe === 'pembiayaan');
// Items without a type (should be filtered out from calculations)
const untypedItems = sortedItems.filter(item => !item.tipe);
if (untypedItems.length > 0) {
console.warn(`Found ${untypedItems.length} items without a type. These will be excluded from calculations.`);
}
@@ -99,7 +137,7 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
>
<Stack gap="lg">
<Title order={4} c={colors['blue-button']} ta="center">
Grafik Pelaksanaan APBDes Tahun {apbdesData.tahun}
Grafik Pelaksanaan APBDes Tahun {apbdesData.tahun || 'N/A'}
</Title>
<Text ta="center" fw="bold" fz="sm" c="dimmed">

View File

@@ -34,7 +34,36 @@ interface APBDesTableProps {
}
function APBDesTable({ apbdesData }: APBDesTableProps) {
// Handle case where apbdesData is null or undefined
if (!apbdesData) {
return (
<Box pt={"xs"} pb="md" px={{ base: 'md', md: 100 }}>
<Title order={4} c={colors['blue-button']} mb="sm">
Rincian APBDes
</Title>
<Paper withBorder radius="md" shadow="xs" p="md">
<Text>Tidak ada data APBDes tersedia.</Text>
</Paper>
</Box>
);
}
const items = Array.isArray(apbdesData.items) ? apbdesData.items : [];
// Show message if no items exist
if (items.length === 0) {
return (
<Box pt={"xs"} pb="md" px={{ base: 'md', md: 100 }}>
<Title order={4} c={colors['blue-button']} mb="sm">
Rincian APBDes Tahun {apbdesData.tahun || 'N/A'}
</Title>
<Paper withBorder radius="md" shadow="xs" p="md">
<Text>Tidak ada rincian anggaran tersedia untuk tahun ini.</Text>
</Paper>
</Box>
);
}
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
// Calculate totals
@@ -46,7 +75,7 @@ function APBDesTable({ apbdesData }: APBDesTableProps) {
return (
<Box pt={"xs"} pb="md" px={{ base: 'md', md: 100 }}>
<Title order={4} c={colors['blue-button']} mb="sm">
Rincian APBDes Tahun {apbdesData.tahun}
Rincian APBDes Tahun {apbdesData.tahun || 'N/A'}
</Title>
<Paper withBorder radius="md" shadow="xs" p="md">

View File

@@ -32,11 +32,38 @@ export interface APBDesData {
}
export function transformAPBDesData(data: any): APBDesData {
if (!data) {
return {
id: '',
tahun: null,
items: [],
image: null,
file: null
};
}
return {
...data,
items: data.items.map((item: any) => ({
...item,
tipe: isAPBDesTipe(item.tipe) ? item.tipe : null
}))
id: data.id || '',
tahun: data.tahun || null,
items: Array.isArray(data.items)
? data.items.map((item: any) => ({
id: item.id || '',
kode: item.kode || '',
uraian: item.uraian || '',
anggaran: typeof item.anggaran === 'number' ? item.anggaran : 0,
realisasi: typeof item.realisasi === 'number' ? item.realisasi : 0,
selisih: typeof item.selisih === 'number' ? item.selisih : 0,
persentase: typeof item.persentase === 'number' ? item.persentase : 0,
level: typeof item.level === 'number' ? item.level : 1,
tipe: isAPBDesTipe(item.tipe) ? item.tipe : null,
createdAt: item.createdAt || undefined,
updatedAt: item.updatedAt || undefined,
deletedAt: item.deletedAt || null,
isActive: item.isActive !== undefined ? item.isActive : true,
apbdesId: item.apbdesId || undefined
}))
: [],
image: data.image ? { id: data.image.id || '', url: data.image.url || '' } : null,
file: data.file ? { id: data.file.id || '', url: data.file.url || '' } : null
};
}

View File

@@ -9,6 +9,7 @@ import { IconDownload } from '@tabler/icons-react'
import { Link } from 'next-view-transitions'
import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils'
import { toast } from 'react-toastify'
import BackButton from '../../(pages)/desa/layanan/_com/BackButto'
import APBDesTable from './lib/apbDesaTable'
import APBDesProgress from './lib/apbDesaProgress'
@@ -19,6 +20,7 @@ function Page() {
const paDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
const [loading, setLoading] = useState(false)
const [selectedYear, setSelectedYear] = useState<string | null>(null)
useEffect(() => {
const loadData = async () => {
try {
@@ -26,7 +28,8 @@ function Page() {
await state.findMany.load()
await paDesaState.findMany.load()
} catch (error) {
console.error(error)
console.error('Error loading APBDes data:', error)
toast.error('Gagal memuat data APBDes')
} finally {
setLoading(false)
}
@@ -37,7 +40,13 @@ function Page() {
const dataAPBDes = state.findMany.data || []
// Buat daftar tahun unik dari data
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
const years = Array.from(
new Set(
dataAPBDes
.filter((item: any) => item?.tahun != null) // Filter out items with null/undefined tahun
.map((item: any) => item.tahun)
)
)
.sort((a, b) => b - a) // urutkan descending
.map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
@@ -49,9 +58,16 @@ function Page() {
}, [years, selectedYear])
// Transform and filter data based on selected year
const currentApbdes = dataAPBDes.length > 0
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
: null
let currentApbdes = null;
if (dataAPBDes.length > 0 && selectedYear) {
const selectedData = dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear);
if (selectedData) {
currentApbdes = transformAPBDesData(selectedData);
}
} else if (dataAPBDes.length > 0 && !selectedYear && years.length > 0) {
// If no year is selected but data exists, use the first item
currentApbdes = transformAPBDesData(dataAPBDes[0]);
}
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap={32}>

View File

@@ -143,29 +143,29 @@ function Apbdes() {
)}
{/* GRID */}
<SimpleGrid
mx={{ base: 'md', md: 100 }}
cols={{ base: 1, sm: 3 }}
spacing="lg"
pb="xl"
>
{loading ? (
<Center mih={200}>
<Loader size="lg" color="blue" />
</Center>
) : data.length === 0 ? (
<Center mih={200}>
<Stack align="center" gap="xs">
<Text fz="lg" c="dimmed" lh={1.4}>
Belum ada data APBDes yang tersedia
</Text>
<Text fz="sm" c="dimmed" lh={1.4}>
Data akan ditampilkan di sini setelah diunggah
</Text>
</Stack>
</Center>
) : (
data.map((v, k) => (
{loading ? (
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
<Loader size="lg" color="blue" />
</Center>
) : data.length === 0 ? (
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
<Stack align="center" gap="xs">
<Text fz="lg" c="dimmed" lh={1.4}>
Belum ada data APBDes yang tersedia
</Text>
<Text fz="sm" c="dimmed" lh={1.4}>
Data akan ditampilkan di sini setelah diunggah
</Text>
</Stack>
</Center>
) : (
<SimpleGrid
mx={{ base: 'md', md: 100 }}
cols={{ base: 1, sm: 3 }}
spacing="lg"
pb="xl"
>
{data.map((v, k) => (
<BackgroundImage
key={k}
src={v.image?.link || ''}
@@ -213,9 +213,9 @@ function Apbdes() {
</Center>
</Stack>
</BackgroundImage>
))
)}
</SimpleGrid>
))}
</SimpleGrid>
)}
</Stack>
)
}