Compare commits

...

3 Commits

25 changed files with 688 additions and 409 deletions

2
.gitignore vendored
View File

@@ -48,3 +48,5 @@ next-env.d.ts
.env.*
*.tar.gz

View File

@@ -3,11 +3,9 @@
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prisma:seed": "bun run prisma/seed.ts"
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
},
"prisma": {
"seed": "bun run prisma/seed.ts"

View File

@@ -140,7 +140,7 @@ function EditSuratKeterangan() {
{/* Upload Gambar 1 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar 1
Gambar Konten Pelayanan
</Text>
<Dropzone
onDrop={(files) => {
@@ -196,7 +196,7 @@ function EditSuratKeterangan() {
{/* Upload Gambar 2 */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar 2
Gambar Alur Pelayanan Surat
</Text>
<Dropzone
onDrop={(files) => {

View File

@@ -100,7 +100,7 @@ function DetailSuratKeterangan() {
<Box>
<Text fz="lg" fw="bold">
Gambar
Gambar Konten Pelayanan
</Text>
{data?.image?.link ? (
<Image
@@ -120,7 +120,7 @@ function DetailSuratKeterangan() {
<Box>
<Text fz="lg" fw="bold">
Gambar 2
Gambar Alur Pelayanan Surat
</Text>
{data?.image2?.link ? (
<Image

View File

@@ -126,10 +126,10 @@ function CreateSuratKeterangan() {
/>
</Box>
{/* Gambar Utama */}
{/* Gambar Konten Pelayanan */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Utama
Gambar Konten Pelayanan
</Text>
<Dropzone
onDrop={(files) => {
@@ -175,10 +175,10 @@ function CreateSuratKeterangan() {
)}
</Box>
{/* Gambar Tambahan */}
{/* Gambar Alur Pelayanan Surat */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Tambahan (Opsional)
Gambar Alur Pelayanan Surat
</Text>
<Dropzone
onDrop={(files) => {

View File

@@ -11,15 +11,21 @@ import {
Stack,
Text,
TextInput,
Title
Title,
Tooltip,
} from "@mantine/core";
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import {
IconArrowBack,
IconImageInPicture,
IconPhoto,
IconUpload,
IconX,
} 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";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";
@@ -34,24 +40,24 @@ function EditKeamananLingkungan() {
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({
name: keamananState.edit.form.name || '',
deskripsi: keamananState.edit.form.deskripsi || '',
imageId: keamananState.edit.form.imageId || ''
name: keamananState.edit.form.name || "",
deskripsi: keamananState.edit.form.deskripsi || "",
imageId: keamananState.edit.form.imageId || "",
});
// Load berita by id saat pertama kali
// Load data by id
useEffect(() => {
const loadBerita = async () => {
const loadData = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await keamananState.edit.load(id); // akses langsung, bukan dari proxy
const data = await keamananState.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
name: data.name || "",
deskripsi: data.deskripsi || "",
imageId: data.imageId || "",
});
if (data?.image?.link) {
@@ -64,30 +70,29 @@ function EditKeamananLingkungan() {
}
};
loadBerita();
}, [params?.id]); // ✅ hapus beritaState dari dependency
loadData();
}, [params?.id]);
const handleSubmit = async () => {
try {
// Update global state with form data
keamananState.edit.form = {
...keamananState.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId // Keep existing imageId if not changed
imageId: formData.imageId,
};
// Jika ada file baru, upload
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;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Update imageId in global state
keamananState.edit.form.imageId = uploaded.id;
}
@@ -101,36 +106,72 @@ function EditKeamananLingkungan() {
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Keamanan Lingkungan</Title>
<Box px={{ base: "sm", md: "lg" }} py="md">
{/* Header */}
<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>
<Title order={4} ml="sm" c="dark">
Edit Keamanan Lingkungan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
onReject={() => toast.error("File tidak valid.")}
maxSize={5 * 1024 ** 2}
accept={{ "image/*": [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: "none" }}
>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload
size={52}
color="var(--mantine-color-blue-6)"
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX
size={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle>
<div>
@@ -151,15 +192,21 @@ function EditKeamananLingkungan() {
<IconImageInPicture />
</Center>
)}
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul Keamanan Lingkungan</Text>}
placeholder="masukkan judul"
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
label="Judul Keamanan Lingkungan"
placeholder="Masukkan judul"
required
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<Text fz="sm" fw="bold">
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
@@ -169,7 +216,20 @@ function EditKeamananLingkungan() {
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
<Group justify="right">
<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)",
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>

View File

@@ -1,9 +1,9 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -36,64 +36,95 @@ function DetailKeamananLingkungan() {
if (!keamananState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
<Skeleton height={500} radius="md" />
</Stack>
)
}
const data = keamananState.findUnique.data
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Keamanan Lingkungan</Text>
{keamananState.findUnique.data ? (
<Paper key={keamananState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 490}} src={keamananState.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Keamanan Lingkungan</Text>
<Text fz={"lg"}>{keamananState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: keamananState.findUnique.data?.deskripsi }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Box py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Keamanan Lingkungan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
<Image
w={{ base: 150, md: 490 }}
src={data?.image?.link}
alt="gambar keamanan lingkungan"
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Judul Keamanan Lingkungan</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi }} />
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
if (keamananState.findUnique.data) {
setSelectedId(keamananState.findUnique.data.id);
setModalHapus(true);
}
setSelectedId(data.id);
setModalHapus(true);
}}
disabled={keamananState.delete.loading || !keamananState.findUnique.data}
color={"red"}
variant="light"
radius="md"
size="md"
>
<IconX size={20} />
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
onClick={() => {
if (keamananState.findUnique.data) {
router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${keamananState.findUnique.data.id}/edit`);
}
}}
disabled={!keamananState.findUnique.data}
color={"green"}
color="green"
onClick={() =>
router.push(
`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
@@ -102,10 +133,10 @@ function DetailKeamananLingkungan() {
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus keamanan lingkungan ini?'
text="Apakah anda yakin ingin menghapus keamanan lingkungan ini?"
/>
</Box>
);
}
export default DetailKeamananLingkungan;
export default DetailKeamananLingkungan;

View File

@@ -1,9 +1,25 @@
'use client'
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import {
IconArrowBack,
IconPhoto,
IconUpload,
IconX,
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
@@ -11,37 +27,36 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import keamananLingkunganState from '../../../_state/keamanan/keamanan-lingkungan';
function CreateKeamananLingkungan() {
const keamananState = useProxy(keamananLingkunganState)
const keamananState = useProxy(keamananLingkunganState);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const resetForm = () => {
keamananState.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
name: '',
deskripsi: '',
imageId: '',
};
setPreviewImage(null);
setFile(null);
}
};
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
return toast.warn('Pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
})
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal mengupload file");
return toast.error('Gagal mengupload file');
}
keamananState.create.form.imageId = uploaded.id;
@@ -49,86 +64,127 @@ function CreateKeamananLingkungan() {
await keamananState.create.create();
resetForm();
router.push("/admin/keamanan/keamanan-lingkungan-pecalang-patwal")
}
router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal');
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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>
<Title order={4} ml="sm" c="dark">
Tambah Data Keamanan Lingkungan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Keamanan Lingkungan</Title>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
<Text fz="sm" fw="bold">
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
>
<Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: 'none' }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
<Dropzone.Accept>
<IconUpload
size={52}
color="var(--mantine-color-blue-6)"
stroke={1.5}
/>
</Box>
)}
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
size={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle>
</Box>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
{/* Input Nama */}
<TextInput
value={keamananState.create.form.name}
onChange={(val) => {
keamananState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Keamanan Lingkungan</Text>}
placeholder='Masukkan nama Keamanan Lingkungan'
label={<Text fw="bold" fz="sm">Nama Keamanan Lingkungan</Text>}
placeholder="Masukkan nama Keamanan Lingkungan"
required
/>
{/* Input Deskripsi */}
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Keamanan Lingkungan</Text>
<Text fw="bold" fz="sm">
Deskripsi Keamanan Lingkungan
</Text>
<CreateEditor
value={keamananState.create.form.deskripsi}
onChange={(val) => {
@@ -136,8 +192,21 @@ function CreateKeamananLingkungan() {
}}
/>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
{/* Tombol Submit */}
<Group justify="right">
<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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,26 +1,46 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import keamananLingkunganState from '../../_state/keamanan/keamanan-lingkungan';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import keamananLingkunganState from '../../_state/keamanan/keamanan-lingkungan';
function KeamananLingkungan() {
const [search, setSearch] = useState("");
return (
<Box>
{/* Header Search */}
<HeaderSearch
title='Keamanan Lingkungan'
placeholder='pencarian'
placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKeamananLingkungan search={search} />
</Box>
);
@@ -47,54 +67,94 @@ function ListKeamananLingkungan({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Keamanan Lingkungan'
href='/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Keamanan Lingkungan</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={180}>
<Text fz={"md"} truncate={"end"} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={250}>
<Text fz={"md"} truncate={"end"} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Judul + Tombol Tambah */}
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Keamanan Lingkungan</Title>
<Tooltip label="Tambah Data Keamanan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/keamanan/keamanan-lingkungan-pecalang-patwal/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed" truncate lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/keamanan/keamanan-lingkungan-pecalang-patwal/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data keamanan lingkungan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
my={"md"}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -1,26 +1,45 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import polsekTerdekat from '../../_state/keamanan/polsek-terdekat';
import { useState } from 'react';
function PolsekTerdekat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Polsek Terdekat'
placeholder='pencarian'
placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPolsekTerdekat search={search} />
</Box>
);
@@ -47,60 +66,90 @@ function ListPolsekTerdekat({ search }: { search: string }) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Polsek Terdekat'
href='/admin/keamanan/polsek-terdekat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Polsek Terdekat</TableTh>
<TableTh>Jarak Polsek</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={180}>
<Text fz='md' truncate={"end"} lineClamp={1}>{item.nama}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={180}>
<Text fz='md' truncate={"end"} lineClamp={1}>{item.jarakKeDesa}</Text>
</Box>
</TableTd>
<TableTd>
<Box w={250}>
<Text fz='md' truncate={"end"} lineClamp={1}>{item.alamat}</Text>
</Box>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Polsek Terdekat</Title>
<Tooltip label="Tambah Polsek" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/polsek-terdekat/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Polsek</TableTh>
<TableTh>Jarak</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd>{item.jarakKeDesa}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data Polsek yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
my="md"
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>

View File

@@ -107,7 +107,7 @@ function EditAPBDes() {
await apbdesState.edit.update();
toast.success('APBDes berhasil diperbarui!');
router.push('/admin/landing-page/APBDes');
router.push('/admin/landing-page/apbdes');
} catch (error) {
console.error('Error updating APBDes:', error);
toast.error('Terjadi kesalahan saat memperbarui APBDes');

View File

@@ -28,7 +28,7 @@ function DetailAPBDes() {
apbdesState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/APBDes")
router.push("/admin/landing-page/apbdes")
}
}
@@ -135,7 +135,7 @@ function DetailAPBDes() {
<Tooltip label="Edit APBDes" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/APBDes/${data.id}/edit`)}
onClick={() => router.push(`/admin/landing-page/apbdes/${data.id}/edit`)}
variant="light"
radius="md"
size="md"

View File

@@ -73,7 +73,7 @@ function CreateAPBDes() {
toast.success("Berhasil menambahkan APBDes");
resetForm();
router.push("/admin/landing-page/APBDes");
router.push("/admin/landing-page/apbdes");
} catch (error) {
console.error("Gagal submit:", error);
toast.error("Gagal menyimpan data");

View File

@@ -62,7 +62,7 @@ function ListAPBDes({ search }: { search: string }) {
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/landing-page/APBDes/create')}
onClick={() => router.push('/admin/landing-page/apbdes/create')}
>
Tambah Baru
</Button>
@@ -110,7 +110,7 @@ function ListAPBDes({ search }: { search: string }) {
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/landing-page/APBDes/${item.id}`)}
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
fullWidth
>
<IconDeviceImacCog size={20} />

View File

@@ -12,6 +12,7 @@ import korupsiState from '@/app/admin/(dashboard)/_state/landing-page/desa-anti-
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import { useShallowEffect } from '@mantine/hooks';
interface FormDesaAntiKorupsi {
name: string;
@@ -35,6 +36,10 @@ export default function EditDesaAntiKorupsi() {
fileId: '',
});
useShallowEffect(() => {
korupsiState.kategoriDesaAntiKorupsi.findMany.load();
}, []);
useEffect(() => {
const loadDesaAntiKorupsi = async () => {
const id = params?.id as string;

View File

@@ -91,7 +91,7 @@ function EditKolaborasiInovasi() {
await sdgsState.edit.update();
toast.success("sdgs desa berhasil diperbarui!");
router.push("/admin/landing-page/SDGs-Desa");
router.push("/admin/landing-page/sdgs");
} catch (error) {
console.error("Error updating sdgs desa:", error);
toast.error("Terjadi kesalahan saat memperbarui sdgs desa");

View File

@@ -27,7 +27,7 @@ function DetailSDGSDesa() {
sdgsState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/landing-page/SDGs-Desa")
router.push("/admin/landing-page/sdgs")
}
}
@@ -113,7 +113,7 @@ function DetailSDGSDesa() {
<Tooltip label="Edit SDGs Desa" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${data.id}/edit`)}
onClick={() => router.push(`/admin/landing-page/sdgs/${data.id}/edit`)}
variant="light"
radius="md"
size="md"

View File

@@ -53,7 +53,7 @@ function CreateSDGsDesa() {
await stateSDGSDesa.create.create();
resetForm();
router.push("/admin/landing-page/SDGs-Desa")
router.push("/admin/landing-page/sdgs")
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">

View File

@@ -64,7 +64,7 @@ function ListSdgsDesa({ search }: { search: string }) {
leftSection={<IconPlus size={18} />}
color={colors['blue-button']}
variant="light"
onClick={() => router.push('/admin/landing-page/SDGs-Desa/create')}
onClick={() => router.push('/admin/landing-page/sdgs/create')}
>
Tambah Baru
</Button>
@@ -103,7 +103,7 @@ function ListSdgsDesa({ search }: { search: string }) {
leftSection={<IconPlus size={18} />}
color={colors['blue-button']}
variant="light"
onClick={() => router.push('/admin/landing-page/SDGs-Desa/create')}
onClick={() => router.push('/admin/landing-page/sdgs/create')}
>
Tambah Baru
</Button>
@@ -137,7 +137,7 @@ function ListSdgsDesa({ search }: { search: string }) {
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/landing-page/SDGs-Desa/${item.id}`)}
onClick={() => router.push(`/admin/landing-page/sdgs/${item.id}`)}
>
<IconDeviceImacCog size={18} />
</Button>

View File

@@ -22,12 +22,12 @@ export const navBar = [
{
id: "Landing_Page_4",
name: "SDGs Desa",
path: "/admin/landing-page/SDGs-Desa"
path: "/admin/landing-page/sdgs"
},
{
id: "Landing_Page_5",
name: "APBDes",
path: "/admin/landing-page/APBDes"
path: "/admin/landing-page/apbdes"
},
{
id: "Landing_Page_6",

View File

@@ -26,6 +26,10 @@ export default async function pelayananSuratKeteranganFindMany(context: Context)
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
image: true,
image2: true,
},
}),
prisma.pelayananSuratKeterangan.count({ where }),
]);

View File

@@ -26,8 +26,6 @@ import Pendidikan from "./_lib/pendidikan";
import User from "./_lib/user";
import Role from "./_lib/user/role";
const ROOT = process.cwd();
if (!process.env.WIBU_UPLOAD_DIR)
@@ -72,11 +70,14 @@ const Utils = new Elysia({
return { version };
});
if (!process.env.WIBU_UPLOAD_DIR)
throw new Error("WIBU_UPLOAD_DIR is not defined");
const ApiServer = new Elysia()
.use(swagger({ path: "/api/docs" }))
.use(
staticPlugin({
assets: process.env.WIBU_UPLOAD_DIR || "./uploads",
assets: process.env.WIBU_UPLOAD_DIR,
prefix: "/uploads",
})
)
@@ -94,7 +95,7 @@ const ApiServer = new Elysia()
.use(Pendidikan)
.use(User)
.use(Role)
.onError(({ code }) => {
if (code === "NOT_FOUND") {
return {
@@ -105,116 +106,116 @@ const ApiServer = new Elysia()
})
.group("/api", (app) =>
app
.get("/layanan", layanan)
.get("/potensi", getPotensi)
.get(
"/img/:name",
({ params, query }) => {
return img({
name: params.name,
UPLOAD_DIR_IMAGE,
ROOT,
size: query.size,
});
},
{
params: t.Object({
name: t.String(),
}),
query: t.Optional(
t.Object({
size: t.Optional(t.Number()),
})
),
}
)
.delete(
"/img/:name",
({ params }) => {
return imgDel({
name: params.name,
UPLOAD_DIR_IMAGE,
});
},
{
params: t.Object({
name: t.String(),
}),
}
)
.get(
"/imgs",
({ query }) => {
return imgs({
search: query.search,
page: query.page,
count: query.count,
UPLOAD_DIR_IMAGE,
});
},
{
query: t.Optional(
t.Object({
page: t.Number({ default: 1 }),
count: t.Number({ default: 10 }),
search: t.String({ default: "" }),
})
),
}
)
.post(
"/upl-img",
({ body }) => {
console.log(body.title);
return uplImg({ files: body.files, UPLOAD_DIR_IMAGE });
},
{
body: t.Object({
title: t.String(),
files: t.Files({ multiple: true }),
}),
}
)
.post(
"/upl-img-single",
({ body }) => {
return uplImgSingle({
fileName: body.name,
file: body.file,
UPLOAD_DIR_IMAGE,
});
},
{
body: t.Object({
name: t.String(),
file: t.File(),
}),
}
)
.post(
"/upl-csv-single",
({ body }) => {
return uplCsvSingle({ fileName: body.name, file: body.file });
},
{
body: t.Object({
name: t.String(),
file: t.File(),
}),
}
)
.post(
"/upl-csv",
({ body }) => {
return uplCsv({ files: body.files });
},
{
body: t.Object({
files: t.Files(),
}),
}
)
);
.get("/layanan", layanan)
.get("/potensi", getPotensi)
.get(
"/img/:name",
({ params, query }) => {
return img({
name: params.name,
UPLOAD_DIR_IMAGE,
ROOT,
size: query.size,
});
},
{
params: t.Object({
name: t.String(),
}),
query: t.Optional(
t.Object({
size: t.Optional(t.Number()),
})
),
}
)
.delete(
"/img/:name",
({ params }) => {
return imgDel({
name: params.name,
UPLOAD_DIR_IMAGE,
});
},
{
params: t.Object({
name: t.String(),
}),
}
)
.get(
"/imgs",
({ query }) => {
return imgs({
search: query.search,
page: query.page,
count: query.count,
UPLOAD_DIR_IMAGE,
});
},
{
query: t.Optional(
t.Object({
page: t.Number({ default: 1 }),
count: t.Number({ default: 10 }),
search: t.String({ default: "" }),
})
),
}
)
.post(
"/upl-img",
({ body }) => {
console.log(body.title);
return uplImg({ files: body.files, UPLOAD_DIR_IMAGE });
},
{
body: t.Object({
title: t.String(),
files: t.Files({ multiple: true }),
}),
}
)
.post(
"/upl-img-single",
({ body }) => {
return uplImgSingle({
fileName: body.name,
file: body.file,
UPLOAD_DIR_IMAGE,
});
},
{
body: t.Object({
name: t.String(),
file: t.File(),
}),
}
)
.post(
"/upl-csv-single",
({ body }) => {
return uplCsvSingle({ fileName: body.name, file: body.file });
},
{
body: t.Object({
name: t.String(),
file: t.File(),
}),
}
)
.post(
"/upl-csv",
({ body }) => {
return uplCsv({ files: body.files });
},
{
body: t.Object({
files: t.Files(),
}),
}
)
);
export const GET = ApiServer.handle;
export const POST = ApiServer.handle;

View File

@@ -91,7 +91,7 @@ function Lokal() {
</Text>
</Flex>
) : (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl">
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="xl">
{filteredData.map(item => {
const handleDownload = (e: React.MouseEvent) => {
e.stopPropagation();
@@ -116,7 +116,7 @@ function Lokal() {
}}
>
<Stack h="100%" justify="space-between">
<Text fz="lg" fw={600} c={colors['blue-button']} lineClamp={2}>
<Text fz="lg" fw={600} c={colors['blue-button']}>
{item.name}
</Text>
{item?.file && (

View File

@@ -83,7 +83,7 @@ function Slider() {
const slides = data.map((item) => (
<Carousel.Slide key={item.id} >
<Paper h={"100%"} pos={"relative"} style={{
<Paper h={height} pos={"relative"} style={{
backgroundImage: `url(${item.image?.link})`,
backgroundSize: "cover",
backgroundPosition: "center",

View File

@@ -24,4 +24,4 @@ const colors = {
}
export default colors
export type ColorsType = typeof colors
export type ColorsType = typeof colors