Fix QC Keano FrontEnd

Fix QC Kak Ayu Admin 29 Okt
This commit is contained in:
2025-11-03 17:36:00 +08:00
parent 7b4bb1e58e
commit d128313e71
20 changed files with 1038 additions and 439 deletions

View File

@@ -6,9 +6,9 @@ import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
name: z.string().min(5, "Nama minimal 5 karakter"),
deskripsi: z.string().min(5, "Deskripsi minimal 5 karakter"),
slug: z.string().min(5, "Deskripsi singkat minimal 5 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
@@ -29,26 +29,33 @@ const programKreatifState = proxy({
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
toast.error(err);
return false; // ⬅️ ini penting
}
try {
programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form
);
if (res.status === 200) {
programKreatifState.findMany.load();
return toast.success("success create");
toast.success("success create");
return true;
}
console.log(res);
return toast.error("failed create");
toast.error("failed create");
return false;
} catch (error) {
console.log((error as Error).message);
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat create");
return false;
} finally {
programKreatifState.create.loading = false;
}
},
}
},
findMany: {
data: null as any[] | null,

View File

@@ -5,7 +5,6 @@ import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
@@ -21,6 +20,7 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
import { Dropzone } from '@mantine/dropzone';
import ExifOrientationImg from 'react-exif-orientation-img';
export default function CreateDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState);
@@ -173,17 +173,16 @@ export default function CreateDesaDigital() {
overflow: 'hidden',
}}
>
<Image
src={previewImage}
alt="Preview"
radius="md"
style={{
maxHeight: 220,
objectFit: 'cover',
border: '1px solid #e0e0e0',
}}
loading="lazy"
/>
<ExifOrientationImg
src={previewImage}
alt="Preview"
style={{
maxHeight: 220,
objectFit: 'cover',
border: '1px solid #e0e0e0',
borderRadius: 12,
}}
/>
</Box>
)}
</Box>

View File

@@ -54,8 +54,8 @@ function DetailInfoTeknologiTepatGuna() {
{/* Card Utama */}
<Paper
withBorder
w={{ base: "100%", md: "70%", lg: "60%" }}
bg="#ECEEF8"
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
@@ -65,7 +65,7 @@ function DetailInfoTeknologiTepatGuna() {
Detail Info Teknologi Tepat Guna
</Text>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import {
@@ -11,8 +12,7 @@ import {
Stack,
Text,
TextInput,
Title,
Tooltip,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -20,7 +20,6 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
interface FormProgramKreatif {
name: string;
@@ -41,6 +40,15 @@ function EditProgramKreatifDesa() {
icon: '',
});
const [originalData, setOriginalData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
});
const [isDataChanged, setIsDataChanged] = useState(false);
// Load data hanya sekali berdasarkan params.id
useEffect(() => {
const loadProgramKreatif = async () => {
@@ -51,12 +59,14 @@ function EditProgramKreatifDesa() {
const data = await stateProgramKreatif.update.load(id);
if (data) {
stateProgramKreatif.update.id = id;
setFormData({
const loadedData = {
name: data.name || '',
slug: data.slug || '',
deskripsi: data.deskripsi || '',
icon: data.icon || '',
});
};
setFormData(loadedData);
setOriginalData(loadedData);
}
} catch (error) {
console.error('Error loading program kreatif:', error);
@@ -67,12 +77,49 @@ function EditProgramKreatifDesa() {
loadProgramKreatif();
}, [params?.id]);
// Deteksi perubahan data
useEffect(() => {
const hasChanged =
formData.name !== originalData.name ||
formData.slug !== originalData.slug ||
formData.deskripsi !== originalData.deskripsi ||
formData.icon !== originalData.icon;
setIsDataChanged(hasChanged);
}, [formData, originalData]);
// Prevent browser back/refresh jika ada perubahan
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isDataChanged) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [isDataChanged]);
const handleChange =
(field: keyof FormProgramKreatif) =>
(value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleBackClick = () => {
if (isDataChanged) {
const confirmed = window.confirm(
'Anda memiliki perubahan yang belum disimpan. Apakah Anda yakin ingin keluar dari halaman ini? Semua perubahan akan hilang.'
);
if (confirmed) {
router.back();
}
} else {
router.back();
}
};
const handleSubmit = async () => {
try {
stateProgramKreatif.update.form = {
@@ -82,6 +129,11 @@ function EditProgramKreatifDesa() {
icon: formData.icon.trim(),
};
await stateProgramKreatif.update.submit();
// Reset isDataChanged agar tidak muncul konfirmasi setelah save
setOriginalData(formData);
setIsDataChanged(false);
router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) {
console.error('Error updating program kreatif:', error);
@@ -92,16 +144,14 @@ function EditProgramKreatifDesa() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Button
variant="subtle"
onClick={handleBackClick}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Program Kreatif Desa
</Title>
@@ -172,4 +222,4 @@ function EditProgramKreatifDesa() {
);
}
export default EditProgramKreatifDesa;
export default EditProgramKreatifDesa;

View File

@@ -32,10 +32,14 @@ function CreateProgramKreatifDesa() {
};
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa");
const success = await stateCreate.create.create();
if (success) {
resetForm();
router.push("/admin/inovasi/program-kreatif-desa");
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik';
@@ -52,15 +53,14 @@ function EditLaporanPublik() {
try {
const data = await stateLaporan.edit.load(id);
if (data) {
setFormData((prev) => ({
...prev,
judul: data.judul ?? prev.judul,
lokasi: data.lokasi ?? prev.lokasi,
tanggalWaktu: data.tanggalWaktu ?? prev.tanggalWaktu,
status: (data.status as Status) ?? prev.status,
penanganan: data.penanganan?.[0]?.deskripsi ?? prev.penanganan,
kronologi: data.kronologi ?? prev.kronologi,
}));
setFormData({
judul: data.judul ?? '',
lokasi: data.lokasi ?? '',
tanggalWaktu: data.tanggalWaktu ?? '',
status: (data.status as Status) ?? 'Proses',
penanganan: data.penanganan?.[0]?.deskripsi ?? '',
kronologi: data.kronologi ?? '',
});
}
} catch (error) {
console.error("Error loading laporan publik:", error);
@@ -69,7 +69,8 @@ function EditLaporanPublik() {
};
loadLaporanPublik();
}, [params?.id, stateLaporan.edit]);
}, [params?.id]);
const handleChange = (field: string, value: string | Status) => {

View File

@@ -1,5 +1,4 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
@@ -26,10 +25,10 @@ function Page() {
}
return (
<Box p={{ base: 'md', md: 'xl' }}>
<Paper withBorder radius="md" p={{ base: 'md', md: 'lg' }} bg={colors['white-1']}>
<Box p="md">
<Paper withBorder p={{ base: 'md', md: 'lg' }} radius="md">
{/* Header */}
<Grid align="center" mb="lg">
<Grid align="center" mb={{ base: 'md', md: 'lg' }}>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} fw={600} c="dark">
Preview Bentuk Konservasi Berdasarkan Adat
@@ -55,8 +54,8 @@ function Page() {
{/* Konten */}
<Stack gap="md">
<Paper radius="md" p={{ base: 'md', md: 'xl' }} bg={colors['BG-trans']} shadow="sm">
<Box mb="md">
<Paper p={{ base: 'md', md: 'xl' }} bg="#ECEEF8" radius="md">
<Box mb="md" px={{ base: 0, md: 20 }}>
<Text
fz={{ base: 'xl', md: '2xl' }}
fw={600}
@@ -67,7 +66,7 @@ function Page() {
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
<Box>
<Box px={{ base: 0, md: 20 }}>
<Text
fz={{ base: 'md', md: 'lg' }}
ta="justify"

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconRecycle, IconTrash } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconTrash, IconRecycle } from '@tabler/icons-react';
import colors from '@/con/colors';
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter();
@@ -16,14 +16,12 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
icon: <IconTrash size={18} stroke={1.8} />,
tooltip: "Kelola data pengelolaan sampah bank sampah",
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
icon: <IconRecycle size={18} stroke={1.8} />,
tooltip: "Kelola data bank sampah terdekat",
},
];
@@ -74,14 +72,8 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
@@ -92,7 +84,6 @@ function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.R
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>

View File

@@ -65,26 +65,32 @@ function ListDataPerpustakaan({ search }: { search: string }) {
<Table striped highlightOnHover withRowBorders style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Judul</TableTh>
<TableTh>Kategori</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '5%' }}>No</TableTh>
<TableTh style={{ width: '25%' }}>Judul</TableTh>
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
<TableTh style={{ width: '23%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '22%' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>
<TableTd style={{ width: '5%' }}>
<Text truncate fz="sm">{index + 1}</Text>
</TableTd>
<TableTd>
<TableTd style={{ width: '20%' }}>
<Text truncate fz="sm">{item.judul}</Text>
</TableTd>
<TableTd>
<TableTd style={{ width: '20%' }}>
<Text truncate fz="sm">{item.kategori.name}</Text>
</TableTd>
<TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={150}>
<Text dangerouslySetInnerHTML={{ __html: item.deskripsi }} lineClamp={1} truncate fz="sm"/>
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
variant="light"
color="blue"