Fix QC Kak Inno Admin, Fix QC Keano UI User, Fix QC Pak jun tabel apbdes

This commit is contained in:
2025-11-12 17:42:31 +08:00
parent 417a8937f5
commit 9622eb5a9a
354 changed files with 11444 additions and 4012 deletions

View File

@@ -1,152 +1,244 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Alert,
Box,
Button,
Center,
Group,
Loader,
Paper,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// 🧩 Type untuk form
interface FormData {
judul: string;
deskripsi: string;
}
// 🧩 Main Component
function Page() {
const lambangState = useProxy(stateProfileDesa.lambangDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
const router = useRouter();
const params = useParams();
try {
const data = await lambangState.findUnique.load(id);
lambangState.update.initialize(data);
} catch (error) {
console.error("Error loading lambang:", error);
toast.error("Gagal memuat data lambang desa");
}
};
const [formData, setFormData] = useState<FormData>({ judul: '', deskripsi: '' });
const [originalData, setOriginalData] = useState<FormData>({ judul: '', deskripsi: '' });
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
loadData();
// 🧭 Load data awal
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
return;
}
return () => {
lambangState.update.reset();
lambangState.findUnique.reset();
};
}, [params?.id, router]);
setIsLoading(true);
setLoadError(null);
const handleSubmit = async () => {
if (isSubmitting || !lambangState.update.form.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true);
try {
const success = await lambangState.update.submit();
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update lambang desa:", error);
toast.error("Terjadi kesalahan saat update lambang desa");
} finally {
setIsSubmitting(false);
try {
const data = await stateProfileDesa.lambangDesa.findUnique.load(id);
if (data) {
const initial: FormData = {
judul: data.judul || '',
deskripsi: data.deskripsi || '',
};
setFormData(initial);
setOriginalData(initial);
// Penting untuk isi id di state sebelum submit
stateProfileDesa.lambangDesa.update.initialize(data);
} else {
setLoadError('Data tidak ditemukan');
}
} catch (error) {
console.error('Error loading lambang:', error);
setLoadError('Gagal memuat data lambang desa');
toast.error('Gagal memuat data lambang desa');
} finally {
setIsLoading(false);
}
};
const handleBack = () => router.back();
loadData();
// Loading state
if (lambangState.findUnique.loading || lambangState.update.loading) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
return () => {
stateProfileDesa.lambangDesa.update.reset();
stateProfileDesa.lambangDesa.findUnique.reset();
};
}, [params?.id, router]);
// 🔁 Reset form
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit handler
const handleSubmit = async () => {
if (!formData.judul.trim()) {
toast.error('Judul wajib diisi');
return;
}
// Error state
if (lambangState.findUnique.error) {
return (
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{lambangState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
setIsSubmitting(true);
try {
const state = stateProfileDesa.lambangDesa;
state.update.form.judul = formData.judul;
state.update.form.deskripsi = formData.deskripsi;
const success = await state.update.submit();
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
} else {
toast.error('Gagal menyimpan data');
}
} catch (error) {
console.error('Error update lambang desa:', error);
toast.error('Terjadi kesalahan saat update lambang desa');
} finally {
setIsSubmitting(false);
}
};
// 📝 Handlers
const handleJudulChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, judul: e.target.value }));
};
const handleDeskripsiChange = (html: string) => {
setFormData(prev => ({ ...prev, deskripsi: html }));
};
const handleBack = () => router.back();
// 🔄 Loading
if (isLoading) {
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">Edit Lambang Desa</Title>
</Group>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="xs">
<Title order={3}>Edit Lambang Desa</Title>
{/* Judul */}
<TextInput
label={<Text fw="bold">Judul</Text>}
placeholder="Judul lambang"
defaultValue={lambangState.update.form.judul}
onChange={(e) => lambangState.update.form.judul = e.currentTarget.value}
error={!lambangState.update.form.judul && "Judul wajib diisi"}
/>
{/* Deskripsi */}
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={lambangState.update.form.deskripsi}
onChange={(val) => lambangState.update.form.deskripsi = val}
/>
</Box>
{/* Buttons */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || lambangState.update.loading}
disabled={!lambangState.update.form.judul}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || lambangState.update.loading}>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
<Box>
<Center h={400}>
<Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} />
<Text size="lg" fw={500} c="dimmed">
Memuat data lambang desa...
</Text>
</Stack>
</Center>
</Box>
);
}
// ❌ Error
if (loadError) {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Alert icon={<IconAlertCircle size={20} />} color="red" title="Terjadi Kesalahan" radius="md">
{loadError}
</Alert>
<Button onClick={() => router.push('/admin/desa/profile/profile-desa')} variant="outline">
Kembali ke Halaman Utama
</Button>
</Stack>
</Box>
);
}
// 🧱 UI utama
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
{/* Header */}
<Group mb="sm">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Lambang Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
{/* Judul */}
<TextInput
label={<Text fw="bold" size="sm">Judul</Text>}
placeholder="Masukkan judul lambang desa"
value={formData.judul}
onChange={handleJudulChange}
error={!formData.judul.trim() && 'Judul wajib diisi'}
required
size="md"
radius="md"
/>
{/* Deskripsi */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Deskripsi
</Text>
<EditEditor value={formData.deskripsi} onChange={handleDeskripsiChange} />
</Box>
{/* Tombol Aksi */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
disabled={isSubmitting}
>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
loading={isSubmitting}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;

View File

@@ -5,7 +5,7 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Alert, Box, Button, Center, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { Alert, Box, Button, Center, Group, Image, Loader, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
@@ -19,8 +19,8 @@ function Page() {
const params = useParams();
const [images, setImages] = useState<
Array<{ file: File | null; preview: string; label: string; imageId?: string }>
>([]);
Array<{ file: File | null; preview: string; label: string; imageId?: string }>
>([]);
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
@@ -28,6 +28,12 @@ function Page() {
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: "",
deskripsi: "",
images: [] as Array<{ label: string; imageId: string }>
});
// Load data
useEffect(() => {
const loadData = async () => {
@@ -52,6 +58,17 @@ function Page() {
})),
});
setOriginalData({
judul: data.judul || '',
deskripsi: data.deskripsi || '',
images: (data.images || []).map((img: any) => ({
label: img.label,
imageId: img.image?.id ?? '',
preview: img.image?.link ?? '',
})),
});
if (data?.images?.length > 0 && data.images[0].image?.link) {
setImages(data.images.map((img: any) => ({
file: null,
@@ -77,15 +94,36 @@ function Page() {
const handleBack = () => router.back();
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
deskripsi: originalData.deskripsi,
images: originalData.images.map((img) => ({
label: img.label,
imageId: img.imageId,
})),
});
setImages(
originalData.images.map((img: any) => ({
file: null,
preview: img.preview, // pakai preview masing-masing, bukan cuma satu
label: img.label,
imageId: img.imageId,
}))
);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
if (isSubmitting || !formData.judul.trim()) {
toast.error("Judul wajib diisi");
return;
}
setIsSubmitting(true);
try {
setIsSubmitting(true);
const uploadedImages = [];
// Upload semua gambar baru
@@ -95,7 +133,7 @@ function Page() {
uploadedImages.push({ imageId: img.imageId, label: img.label });
continue;
}
// upload baru
const res = await ApiFetch.api.fileStorage.create.post({
file: img.file,
@@ -108,7 +146,7 @@ function Page() {
}
uploadedImages.push({ imageId: uploaded.id, label: img.label || "main" });
}
// Update ke global state
maskotState.update.updateField("judul", formData.judul);
@@ -161,9 +199,9 @@ function Page() {
<Box>
<Stack gap="xs">
<Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">Edit Maskot Desa</Title>
</Group>
@@ -175,7 +213,7 @@ function Page() {
<TextInput
label={<Text fw="bold">Judul</Text>}
placeholder="Masukkan judul maskot"
defaultValue={formData.judul}
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.currentTarget.value })}
error={!formData.judul && "Judul wajib diisi"}
/>
@@ -231,7 +269,7 @@ function Page() {
setImages(updated);
}}
>
Hapus
<IconX size={16} />
</Button>
</Group>
<Image
@@ -260,18 +298,31 @@ function Page() {
</SimpleGrid>
{/* Buttons */}
<Group>
<Group justify="right">
{/* Tombol Batal */}
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || maskotState.update.loading}
disabled={!formData.judul}
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || maskotState.update.loading}>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,146 +1,272 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Alert,
Box,
Button,
Center,
Group,
Loader,
Paper,
Stack,
Text,
TextInput,
Title
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// 🔹 Types
interface FormData {
judul: string;
deskripsi: string;
}
// 🔹 Main Component
function Page() {
const sejarahState = useProxy(stateProfileDesa.sejarahDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const params = useParams();
// Load data
// 🧩 Local State
const [formData, setFormData] = useState<FormData>({
judul: '',
deskripsi: '',
});
const [originalData, setOriginalData] = useState<FormData>({
judul: '',
deskripsi: '',
});
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
// 🧭 Load Initial Data
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
return;
}
setIsLoading(true);
setLoadError(null);
try {
const data = await sejarahState.findUnique.load(id);
const data = await stateProfileDesa.sejarahDesa.findUnique.load(id);
if (data) {
sejarahState.update.initialize(data);
const initialData: FormData = {
judul: data.judul || '',
deskripsi: data.deskripsi || '',
};
setFormData(initialData);
setOriginalData(initialData);
stateProfileDesa.sejarahDesa.update.initialize(data);
} else {
setLoadError('Data tidak ditemukan');
}
} catch (error) {
console.error("Error loading sejarah:", error);
toast.error("Gagal memuat data sejarah desa");
console.error('Error loading sejarah:', error);
setLoadError('Gagal memuat data sejarah desa');
toast.error('Gagal memuat data sejarah desa');
} finally {
setIsLoading(false);
}
};
loadData();
return () => {
sejarahState.update.reset();
sejarahState.findUnique.reset();
stateProfileDesa.sejarahDesa.update.reset();
stateProfileDesa.sejarahDesa.findUnique.reset();
};
}, [params?.id, router]);
// 🔄 Check if form has changes
// 🔁 Reset Form to Original Data
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit Handler
const handleSubmit = async () => {
if (isSubmitting || !sejarahState.update.form.judul.trim()) {
toast.error("Judul wajib diisi");
// Validation
if (!formData.judul.trim()) {
toast.error('Judul wajib diisi');
return;
}
setIsSubmitting(true);
try {
const success = await sejarahState.update.submit();
// Access original state directly (not proxy)
const originalState = stateProfileDesa.sejarahDesa;
// Update form data
originalState.update.form.judul = formData.judul;
originalState.update.form.deskripsi = formData.deskripsi;
// Submit
const success = await originalState.update.submit();
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
} else {
toast.error('Gagal menyimpan data');
}
} catch (error) {
console.error("Error update sejarah desa:", error);
toast.error("Terjadi kesalahan saat update sejarah desa");
console.error('Error update sejarah desa:', error);
toast.error('Terjadi kesalahan saat update sejarah desa');
} finally {
setIsSubmitting(false);
}
};
const handleBack = () => router.back();
// 📝 Form Field Handlers
const handleJudulChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, judul: e.target.value }));
};
// Loading state
if (sejarahState.findUnique.loading || sejarahState.update.loading) {
const handleDeskripsiChange = (html: string) => {
setFormData(prev => ({ ...prev, deskripsi: html }));
};
const handleBack = () => {
router.back();
};
// 🔄 Loading State
if (isLoading) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
<Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} />
<Text size="lg" fw={500} c="dimmed">
Memuat data...
</Text>
</Stack>
</Center>
</Box>
);
}
// Error state
if (sejarahState.findUnique.error) {
// Error State
if (loadError) {
return (
<Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{sejarahState.findUnique.error}</Text>
<Alert
icon={<IconAlertCircle size={20} />}
color="red"
title="Terjadi Kesalahan"
radius="md"
>
{loadError}
</Alert>
<Button
onClick={() => router.push('/admin/desa/profile/profile-desa')}
variant="outline"
>
Kembali ke Halaman Utama
</Button>
</Stack>
</Box>
);
}
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
{/* Header */}
<Group mb="sm">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">Edit Sejarah Desa</Title>
<Title order={4} ml="sm" c="dark">
Edit Sejarah Desa
</Title>
</Group>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="xs">
<Title order={3}>Edit Sejarah Desa</Title>
{/* Judul */}
{/* Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
{/* Form Title */}
<Box>
<Title order={3} mb={4}>
Informasi Sejarah Desa
</Title>
</Box>
{/* Judul Field */}
<TextInput
label={<Text fw="bold">Judul</Text>}
placeholder="Judul sejarah"
defaultValue={sejarahState.update.form.judul}
onChange={(e) => sejarahState.update.form.judul = e.currentTarget.value}
error={!sejarahState.update.form.judul && "Judul wajib diisi"}
label={<Text fw="bold" size="sm">Judul Sejarah</Text>}
placeholder="Masukkan judul sejarah desa"
value={formData.judul}
onChange={handleJudulChange}
error={!formData.judul.trim() && 'Judul wajib diisi'}
required
size="md"
radius="md"
/>
{/* Deskripsi */}
{/* Deskripsi Field */}
<Box>
<Text fz={"md"} fw={"bold"}>Deskripsi</Text>
<Text fw="bold" size="sm" mb={8}>
Deskripsi Sejarah
</Text>
<EditEditor
value={sejarahState.update.form.deskripsi}
onChange={(val) => sejarahState.update.form.deskripsi = val}
value={formData.deskripsi}
onChange={handleDeskripsiChange}
/>
</Box>
{/* Buttons */}
<Group>
{/* Action Buttons */}
<Group justify="right">
{/* Tombol Batal */}
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || sejarahState.update.loading}
disabled={!sejarahState.update.form.judul}
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
Batal
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || sejarahState.update.loading}>
Batal
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
@@ -150,4 +276,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,155 +1,247 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateProfileDesa from '@/app/admin/(dashboard)/_state/desa/profile';
import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Paper, Stack, Text, Title } from '@mantine/core';
import {
Alert,
Box,
Button,
Center,
Group,
Loader,
Paper,
Stack,
Text,
Title,
} from '@mantine/core';
import { IconAlertCircle, IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// 🔹 Types
interface FormData {
visi: string;
misi: string;
}
// 🔹 Main Component
function Page() {
const visiMisiState = useProxy(stateProfileDesa.visiMisiDesa)
const router = useRouter()
const params = useParams()
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState<FormData>({ visi: '', misi: '' });
const [originalData, setOriginalData] = useState<FormData>({ visi: '', misi: '' });
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
// Load data
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/desa/profile/profile-desa");
return;
}
// 🧭 Load Data
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error('ID tidak valid');
router.push('/admin/desa/profile/profile-desa');
return;
}
try {
const data = await visiMisiState.findUnique.load(id);
visiMisiState.update.initialize(data);
} catch (error) {
console.error("Error loading visi misi:", error);
toast.error("Gagal memuat data visi misi desa");
}
};
setIsLoading(true);
setLoadError(null);
loadData();
try {
const data = await stateProfileDesa.visiMisiDesa.findUnique.load(id);
if (data) {
const initialData: FormData = {
visi: data.visi || '',
misi: data.misi || '',
};
setFormData(initialData);
setOriginalData(initialData);
return () => {
visiMisiState.update.reset();
visiMisiState.findUnique.reset();
};
}, [params?.id, router]);
const handleSubmit = async () => {
if (isSubmitting || !visiMisiState.update.form.visi.trim()) {
toast.error("Visi wajib diisi");
return;
}
setIsSubmitting(true);
try {
const success = await visiMisiState.update.submit();
if (success) {
toast.success("Data berhasil disimpan");
router.push("/admin/desa/profile/profile-desa");
}
} catch (error) {
console.error("Error update visi misi desa:", error);
toast.error("Terjadi kesalahan saat update visi misi desa");
} finally {
setIsSubmitting(false);
// set id ke state agar submit pakai endpoint benar
stateProfileDesa.visiMisiDesa.update.initialize(data);
} else {
setLoadError('Data tidak ditemukan');
}
} catch (error) {
console.error('Error load visi misi:', error);
setLoadError('Gagal memuat data visi misi desa');
toast.error('Gagal memuat data visi misi desa');
} finally {
setIsLoading(false);
}
};
const handleBack = () => router.back();
loadData();
// Loading state
if (visiMisiState.findUnique.loading || visiMisiState.update.loading) {
return (
<Box>
<Center h={400}>
<Text>Memuat data...</Text>
</Center>
</Box>
);
return () => {
stateProfileDesa.visiMisiDesa.update.reset();
stateProfileDesa.visiMisiDesa.findUnique.reset();
};
}, [params?.id, router]);
// 🔄 Reset Form
const handleResetForm = () => {
setFormData(originalData);
toast.info('Form dikembalikan ke data awal');
};
// 💾 Submit
const handleSubmit = async () => {
if (!formData.visi.trim()) {
toast.error('Visi wajib diisi');
return;
}
// Error state
if (visiMisiState.findUnique.error) {
return (
<Box>
<Stack gap="md">
<Button variant="subtle" onClick={handleBack}>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
<Alert icon={<IconAlertCircle size={16} />} color="red">
<Text fw="bold">Error</Text>
<Text>{visiMisiState.findUnique.error}</Text>
</Alert>
</Stack>
</Box>
);
}
setIsSubmitting(true);
try {
const originalState = stateProfileDesa.visiMisiDesa;
// update data form ke state sebelum submit
originalState.update.form.visi = formData.visi;
originalState.update.form.misi = formData.misi;
const success = await originalState.update.submit();
if (success) {
toast.success('Data berhasil disimpan');
router.push('/admin/desa/profile/profile-desa');
} else {
toast.error('Gagal menyimpan data');
}
} catch (error) {
console.error('Error update visi misi desa:', error);
toast.error('Terjadi kesalahan saat update visi misi desa');
} finally {
setIsSubmitting(false);
}
};
// 🧭 Field handlers
const handleVisiChange = (html: string) => setFormData(prev => ({ ...prev, visi: html }));
const handleMisiChange = (html: string) => setFormData(prev => ({ ...prev, misi: html }));
const handleBack = () => router.back();
// ⏳ Loading
if (isLoading) {
return (
<Box>
<Stack gap="xs">
<Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Button>
<Title order={4} ml="sm" c="dark">Edit Visi Misi Desa</Title>
</Group>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p="md" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Stack gap="xs">
<Title order={3}>Edit Visi Misi Desa</Title>
{/* Visi */}
<Box>
<Text fz={"md"} fw={"bold"}>Visi</Text>
<EditEditor
value={visiMisiState.update.form.visi}
onChange={(val) => visiMisiState.update.form.visi = val}
/>
</Box>
{/* Misi */}
<Box>
<Text fz={"md"} fw={"bold"}>Misi</Text>
<EditEditor
value={visiMisiState.update.form.misi}
onChange={(val) => visiMisiState.update.form.misi = val}
/>
</Box>
{/* Buttons */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || visiMisiState.update.loading}
disabled={!visiMisiState.update.form.visi}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button variant="outline" onClick={handleBack} disabled={isSubmitting || visiMisiState.update.loading}>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
<Box>
<Center h={400}>
<Stack align="center" gap="md">
<Loader size="lg" color={colors['blue-button']} />
<Text size="lg" fw={500} c="dimmed">
Memuat data...
</Text>
</Stack>
</Center>
</Box>
);
}
// ❌ Error
if (loadError) {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Alert
icon={<IconAlertCircle size={20} />}
color="red"
title="Terjadi Kesalahan"
radius="md"
>
{loadError}
</Alert>
<Button
onClick={() => router.push('/admin/desa/profile/profile-desa')}
variant="outline"
>
Kembali ke Halaman Utama
</Button>
</Stack>
</Box>
);
}
// ✅ UI
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack gap="md">
{/* Header */}
<Group mb="sm">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Visi & Misi Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="xl"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="lg">
<Box>
<Title order={3} mb={4}>
Informasi Visi & Misi Desa
</Title>
</Box>
{/* Visi */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Visi
</Text>
<EditEditor value={formData.visi} onChange={handleVisiChange} />
</Box>
{/* Misi */}
<Box>
<Text fw="bold" size="sm" mb={8}>
Misi
</Text>
<EditEditor value={formData.misi} onChange={handleMisiChange} />
</Box>
{/* Actions */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
loading={isSubmitting}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default Page;