Compare commits

...

4 Commits

81 changed files with 2282 additions and 1120 deletions

View File

@@ -672,17 +672,18 @@ model GalleryVideo {
// ========================================= LAYANAN DESA ========================================= //
model PelayananSuratKeterangan {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage? @relation("PelayananSuratKeteranganImage", fields: [imageId], references: [id])
imageId String?
image2 FileStorage? @relation("PelayananSuratKeteranganImage2", fields: [image2Id], references: [id])
image2Id String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
AjukanPermohonan AjukanPermohonan[]
}
model PelayananTelunjukSaktiDesa {
@@ -717,6 +718,20 @@ model PelayananPendudukNonPermanen {
isActive Boolean @default(true)
}
model AjukanPermohonan {
id String @id @default(cuid())
nama String
nik String
alamat String
nomorKk String
kategori PelayananSuratKeterangan @relation(fields: [kategoriId], references: [id])
kategoriId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENGHARGAAN ========================================= //
model Penghargaan {
id String @id @default(cuid())
@@ -835,8 +850,8 @@ model JadwalKegiatan {
syaratKetentuanJadwalKegiatanId String
dokumenjadwalkegiatan DokumenJadwalKegiatan @relation(fields: [dokumenJadwalKegiatanId], references: [id])
dokumenJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String
pendaftaranjadwalkegiatan PendaftaranJadwalKegiatan? @relation(fields: [pendaftaranJadwalKegiatanId], references: [id])
pendaftaranJadwalKegiatanId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1254,15 +1269,15 @@ model KontakDaruratToItem {
// ========================================= PENCEGAHAN KRIMINALITAS ========================================= //
model PencegahanKriminalitas {
id String @id @default(cuid())
judul String
deskripsi String
deskripsiSingkat String
linkVideo String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
judul String
deskripsi String
deskripsiSingkat String
linkVideo String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= LAPORAN PUBLIK ========================================= //

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -71,6 +71,22 @@ const pelayananPendudukNonPermanenForm = {
deskripsi: "",
};
const templateAjukanForm = z.object({
nama: z.string().min(1).max(5000),
nik: z.string().min(1).max(5000),
alamat: z.string().min(1).max(5000),
nomorKk: z.string().min(1).max(5000),
kategoriId: z.string().min(1).max(5000),
});
const defaultAjukanForm = {
nama: "",
nik: "",
alamat: "",
nomorKk: "",
kategoriId: "",
};
const suratKeterangan = proxy({
create: {
form: { ...suratKeteranganForm },
@@ -146,6 +162,30 @@ const suratKeterangan = proxy({
}
},
},
findManyAll: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
omit: { isActive: true };
}>[] | null,
loading: false,
load: async () => {
suratKeterangan.findManyAll.loading = true;
try {
const res = await ApiFetch.api.desa.layanan.pelayanansuratketerangan["findManyAll"].get();
if (res.status === 200 && res.data?.success) {
suratKeterangan.findManyAll.data = res.data.data || [];
} else {
suratKeterangan.findManyAll.data = [];
console.error("Failed to load surat keterangan all:", res.data?.message);
}
} catch (error) {
console.error("Error loading surat keterangan all:", error);
suratKeterangan.findManyAll.data = [];
} finally {
suratKeterangan.findManyAll.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PelayananSuratKeteranganGetPayload<{
include: {
@@ -769,11 +809,250 @@ const pelayananPendudukNonPermanen = proxy({
},
});
const ajukanPermohonan = proxy({
create: {
form: { ...defaultAjukanForm },
loading: false,
async create() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.create.loading = true;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"create"
].post(ajukanPermohonan.create.form);
if (res.status === 200) {
ajukanPermohonan.findMany.load();
return toast.success("Ajukan permohonan berhasil disimpan!");
}
return toast.error("Gagal menyimpan ajukan permohonan");
} catch (error) {
console.log((error as Error).message);
} finally {
ajukanPermohonan.create.loading = false;
}
},
resetForm() {
ajukanPermohonan.create.form = { ...defaultAjukanForm };
},
},
findMany: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
};
}>[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
ajukanPermohonan.findMany.loading = true; // Use the full path to access the property
ajukanPermohonan.findMany.page = page;
ajukanPermohonan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.desa.ajukanpermohonan[
"findMany"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
ajukanPermohonan.findMany.data = res.data.data || [];
ajukanPermohonan.findMany.total = res.data.total || 0;
ajukanPermohonan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load ajukan permohonan:", res.data?.message);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading ajukan permohonan:", error);
ajukanPermohonan.findMany.data = [];
ajukanPermohonan.findMany.total = 0;
ajukanPermohonan.findMany.totalPages = 1;
} finally {
ajukanPermohonan.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.AjukanPermohonanGetPayload<{
include: {
kategori: true;
}
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/desa/ajukanpermohonan/${id}`
);
if (res.ok) {
const data = await res.json();
ajukanPermohonan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch ajukan permohonan:", res.statusText);
ajukanPermohonan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
ajukanPermohonan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
ajukanPermohonan.delete.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Ajukan permohonan berhasil dihapus");
await ajukanPermohonan.findMany.load(); // refresh list
} else {
toast.error(result.message || "Gagal menghapus ajukan permohonan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus ajukan permohonan");
} finally {
ajukanPermohonan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultAjukanForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/desa/ajukanpermohonan/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
nik: data.nik,
alamat: data.alamat,
nomorKk: data.nomorKk,
kategoriId: data.kategoriId,
};
return data;
} else {
throw new Error(result.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error fetching ajukan permohonan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateAjukanForm.safeParse(
ajukanPermohonan.edit.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
ajukanPermohonan.edit.loading = true;
const response = await fetch(
`/api/desa/ajukanpermohonan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
nik: this.form.nik,
alamat: this.form.alamat,
nomorKk: this.form.nomorKk,
kategoriId: this.form.kategoriId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success(result.message || "Ajukan permohonan berhasil diupdate");
await ajukanPermohonan.findMany.load(); // refresh list
return true;
} else {
throw new Error(
result.message || "Gagal mengupdate ajukan permohonan"
);
}
} catch (error) {
console.error("Error updating ajukan permohonan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update ajukan permohonan"
);
return false;
} finally {
ajukanPermohonan.edit.loading = false;
}
},
},
});
const stateLayananDesa = proxy({
suratKeterangan,
pelayananPerizinanBerusaha,
pelayananTelunjukSaktiDesa,
pelayananPendudukNonPermanen,
ajukanPermohonan,
});
export default stateLayananDesa;

View File

@@ -26,14 +26,6 @@ const templateForm = z.object({
dokumenJadwalKegiatan: z.object({
content: z.string().min(1, "Content minimal 1 karakter"),
}),
pendaftaranJadwalKegiatan: z.object({
name: z.string().min(1, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
}),
});
const defaultForm = {
@@ -55,15 +47,7 @@ const defaultForm = {
},
dokumenJadwalKegiatan: {
content: "",
},
pendaftaranJadwalKegiatan: {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
},
}
};
const jadwalkegiatanState = proxy({
@@ -116,7 +100,6 @@ const jadwalkegiatanState = proxy({
deskripsijadwalkegiatan: true;
layananjadwalkegiatan: true;
dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
};
}>[]
| null,
@@ -161,7 +144,6 @@ const jadwalkegiatanState = proxy({
layananjadwalkegiatan: true;
syaratketentuanjadwalkegiatan: true;
dokumenjadwalkegiatan: true;
pendaftaranjadwalkegiatan: true;
};
}> | null,
loading: false,
@@ -209,15 +191,7 @@ const jadwalkegiatanState = proxy({
},
dokumenJadwalKegiatan: {
content: data.dokumenjadwalkegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: data.pendaftaranjadwalkegiatan.name,
tanggal: data.pendaftaranjadwalkegiatan.tanggal,
namaOrangtua: data.pendaftaranjadwalkegiatan.namaOrangtua,
nomor: data.pendaftaranjadwalkegiatan.nomor,
alamat: data.pendaftaranjadwalkegiatan.alamat,
catatan: data.pendaftaranjadwalkegiatan.catatan,
},
}
};
},
async submit() {
@@ -259,20 +233,6 @@ const jadwalkegiatanState = proxy({
content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
.namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
},
};
const res = await fetch(

View File

@@ -4,7 +4,7 @@ import colors from '@/con/colors';
import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers } from '@tabler/icons-react';
import { IconFileText, IconBuildingStore, IconSparkles, IconUsers, IconUsersPlus } from '@tabler/icons-react';
function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
const router = useRouter()
@@ -37,6 +37,13 @@ function LayoutTabsLayanan({ children }: { children: React.ReactNode }) {
href: "/admin/desa/layanan/pelayanan_penduduk_non_permanent",
icon: <IconUsers size={18} stroke={1.8} />,
tooltip: "Pendataan penduduk non-permanent"
},
{
label: "Ajukan Permohonan",
value: "ajukanpermohonan",
href: "/admin/desa/layanan/ajukan_permohonan",
icon: <IconUsersPlus size={18} stroke={1.8} />,
tooltip: "Ajukan permohonan"
}
];

View File

@@ -5,7 +5,6 @@ import {
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
@@ -18,7 +17,7 @@ import {
TableTr,
Text,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
@@ -87,7 +86,6 @@ function ListBerita({ search }: { search: string }) {
<TableTr>
<TableTh style={{ width: '30%' }}>Judul</TableTh>
<TableTh style={{ width: '20%' }}>Kategori</TableTh>
<TableTh style={{ width: '25%' }}>Gambar</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -96,7 +94,7 @@ function ListBerita({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
@@ -107,19 +105,6 @@ function ListBerita({ search }: { search: string }) {
{item.kategoriBerita?.name || '-'}
</Text>
</TableTd>
<TableTd style={{ width: '25%' }}>
<Box
w={80}
h={80}
style={{ borderRadius: 8, overflow: 'hidden' }}
>
{item.image?.link ? (
<Image loading='lazy' src={item.image.link} alt="gambar" fit="cover" />
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"

View File

@@ -0,0 +1,178 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Select,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { 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';
function EditAjukanPermohonan() {
const router = useRouter();
const params = useParams();
const stateAjukan = useProxy(stateLayananDesa.ajukanPermohonan);
const [formData, setFormData] = useState({
nama: stateAjukan.edit.form.nama,
nik: stateAjukan.edit.form.nik,
alamat: stateAjukan.edit.form.alamat,
nomorKk: stateAjukan.edit.form.nomorKk,
kategoriId: stateAjukan.edit.form.kategoriId,
});
useEffect(() => {
stateLayananDesa.suratKeterangan.findManyAll.load();
const loadAjukan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateAjukan.edit.load(id);
if (data) {
setFormData({
nama: data.nama || '',
nik: data.nik || '',
alamat: data.alamat || '',
nomorKk: data.nomorKk || '',
kategoriId: data.kategoriId || '',
});
}
} catch (error) {
console.error('Error loading ajukan:', error);
toast.error('Gagal memuat data ajukan');
}
};
loadAjukan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateAjukan.edit.form = {
...stateAjukan.edit.form,
...formData,
};
toast.success('Ajukan berhasil diperbarui!');
router.push('/admin/desa/layanan/ajukan_permohonan');
} catch (error) {
console.error('Error updating ajukan:', error);
toast.error('Terjadi kesalahan saat memperbarui ajukan');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Back Button */}
<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 Ajukan Permohonan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
required
/>
<TextInput
type="number"
label="NIK"
placeholder="Masukkan NIK"
value={formData.nik}
onChange={(e) => setFormData({ ...formData, nik: e.target.value })}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
required
/>
<TextInput
type="number"
label="Nomor KK"
placeholder="Masukkan nomor KK"
value={formData.nomorKk}
onChange={(e) => setFormData({ ...formData, nomorKk: e.target.value })}
required
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={formData.kategoriId || null}
onChange={(val: string | null) => {
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
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<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>
);
}
export default EditAjukanPermohonan;

View File

@@ -0,0 +1,172 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailAjukanPermohonan() {
const ajukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
ajukanPermohonanState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
ajukanPermohonanState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push('/admin/desa/layanan/ajukan_permohonan');
}
};
if (!ajukanPermohonanState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = ajukanPermohonanState.findUnique.data;
return (
<Box py={10}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Surat Keterangan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">
Nama
</Text>
<Text fz="md" c="dimmed">
{data?.nama || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
NIK
</Text>
<Text fz="md" c="dimmed">
{data?.nik || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Alamat
</Text>
<Text fz="md" c="dimmed">
{data?.alamat || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Nomor KK
</Text>
<Text fz="md" c="dimmed">
{data?.nomorKk || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">
Kategori
</Text>
<Text fz="md" c="dimmed">
{data?.kategori.name || '-'}
</Text>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Surat" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={ajukanPermohonanState.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Surat" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/desa/layanan/ajukan_permohonan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus ajukan permohonan ini?"
/>
</Box>
);
}
export default DetailAjukanPermohonan;

View File

@@ -0,0 +1,155 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import stateLayananDesa from '../../../_state/desa/layananDesa';
function AjukanPermohonan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pelayanan Ajukan Permohonan'
placeholder='Cari nama atau deskripsi...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAjukanPermohonan search={search} />
</Box>
);
}
function ListAjukanPermohonan({ search }: { search: string }) {
const AjukanPermohonanState = useProxy(stateLayananDesa.ajukanPermohonan);
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = AjukanPermohonanState.findMany;
useEffect(() => {
load(page, 10, search);
}, [page, search]);
// Loading state
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Title order={4}>List Ajukan Permohonan</Title>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '30%' }}>Nama</TableTh>
<TableTh style={{ width: '45%' }}>Alamat</TableTh>
<TableTh style={{ width: '15%' }}>NIK</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{data.length > 0 ? (
data.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '30%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nik}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() =>
router.push(`/admin/desa/layanan/ajukan_permohonan/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data ajukan permohonan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default AjukanPermohonan;

View File

@@ -94,9 +94,11 @@ function ListPengumuman({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul}
</Text>
</Box>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination } from '@mantine/core';
import { Box, Button, Center, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip, Pagination, Group } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -60,7 +60,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
<Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Box style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 15 }}>
<Group justify="space-between">
<Title order={4}>List Kategori Potensi</Title>
<Tooltip label="Tambah Kategori Potensi" withArrow>
<Button
@@ -72,7 +72,7 @@ function ListKategoriPotensi({ search }: { search: string }) {
Tambah Baru
</Button>
</Tooltip>
</Box>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withRowBorders style={{ minWidth: '700px' }}>

View File

@@ -114,12 +114,22 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.dokterdantenagamedis?.name || '-'}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.tarifdanlayanan?.layanan || '-'}
</Box>
</TableTd>
<TableTd>{item.dokterdantenagamedis?.name || '-'}</TableTd>
<TableTd>{item.tarifdanlayanan?.layanan || '-'}</TableTd>
<TableTd>
<Button
variant="light"

View File

@@ -149,16 +149,30 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
<Box w={150}>
{item.nama}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.jenisKelamin}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.penyakit}
</Box>
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.penyakit}</TableTd>
<TableTd>
<Button
variant="light"
@@ -212,24 +226,26 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? (
<BarChart
width={isMobile ? 450 : isTablet ? 500 : 550}
height={350}
data={diseaseChartData}
>
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis />
<ChartTooltip />
<Legend />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart>
<Center>
<BarChart
width={isMobile ? 320 : isTablet ? 600 : 800} // kecilin biar muat
height={350}
data={diseaseChartData}
>
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis />
<ChartTooltip />
<Legend />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart>
</Center>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}

View File

@@ -41,14 +41,6 @@ interface JadwalKegiatanFormBase {
dokumenJadwalKegiatan: {
content: string;
};
pendaftaranJadwalKegiatan: {
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
};
}
function EditJadwalKegiatan() {
@@ -76,14 +68,6 @@ function EditJadwalKegiatan() {
dokumenJadwalKegiatan: {
content: stateJadwalKegiatan.edit.form.dokumenJadwalKegiatan?.content || '',
},
pendaftaranJadwalKegiatan: {
name: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.name || '',
tanggal: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.tanggal || '',
namaOrangtua: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.namaOrangtua || '',
nomor: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.nomor || '',
alamat: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.alamat || '',
catatan: stateJadwalKegiatan.edit.form.pendaftaranJadwalKegiatan?.catatan || '',
},
});
useEffect(() => {
@@ -115,14 +99,6 @@ function EditJadwalKegiatan() {
dokumenJadwalKegiatan: {
content: form.dokumenJadwalKegiatan?.content || '',
},
pendaftaranJadwalKegiatan: {
name: form.pendaftaranJadwalKegiatan?.name || '',
tanggal: form.pendaftaranJadwalKegiatan?.tanggal || '',
namaOrangtua: form.pendaftaranJadwalKegiatan?.namaOrangtua || '',
nomor: form.pendaftaranJadwalKegiatan?.nomor || '',
alamat: form.pendaftaranJadwalKegiatan?.alamat || '',
catatan: form.pendaftaranJadwalKegiatan?.catatan || '',
},
});
}
} catch (error) {
@@ -142,8 +118,7 @@ function EditJadwalKegiatan() {
deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan },
layananJadwalKegiatan: { ...formData.layananJadwalKegiatan },
syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan },
dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan },
pendaftaranJadwalKegiatan: { ...formData.pendaftaranJadwalKegiatan },
dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan }
};
const success = await stateJadwalKegiatan.edit.submit();
@@ -252,7 +227,7 @@ function EditJadwalKegiatan() {
{/* Dokumen */}
<Box>
<Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text>
<Text fz="md" fw="bold">Dokumen Yang Perlu Dibawa</Text>
<EditEditor
value={formData.dokumenJadwalKegiatan.content}
onChange={(val) => setFormData((prev) => ({
@@ -262,41 +237,6 @@ function EditJadwalKegiatan() {
/>
</Box>
{/* Pendaftaran */}
<Box>
<Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text>
<TextInput label="Nama" value={formData.pendaftaranJadwalKegiatan.name}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, name: e.target.value }
}))}
/>
<TextInput type="date" label="Tanggal" value={formData.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, tanggal: e.target.value }
}))}
/>
<TextInput label="Nama Orangtua" value={formData.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, namaOrangtua: e.target.value }
}))}
/>
<TextInput label="Nomor" value={formData.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, nomor: e.target.value }
}))}
/>
<TextInput label="Alamat" value={formData.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, alamat: e.target.value }
}))}
/>
<TextInput label="Catatan" value={formData.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, catatan: e.target.value }
}))}
/>
</Box>
{/* Submit */}
<Group justify="right">
<Button

View File

@@ -109,18 +109,7 @@ function DetailJadwalKegiatan() {
<Text fz="lg" fw="bold">Dokumen</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.dokumenjadwalkegiatan.content }} />
</Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fz="lg" fw="bold">Prosedur Pendaftaran</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.name}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.tanggal}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.namaOrangtua}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.nomor}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.alamat}</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.pendaftaranjadwalkegiatan.catatan }} />
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">

View File

@@ -42,15 +42,7 @@ function CreateJadwalKegiatan() {
},
dokumenJadwalKegiatan: {
content: '',
},
pendaftaranJadwalKegiatan: {
name: '',
tanggal: '',
namaOrangtua: '',
nomor: '',
alamat: '',
catatan: '',
},
}
};
};
@@ -173,7 +165,7 @@ function CreateJadwalKegiatan() {
</Box>
<Box>
<Text fz="md" fw="bold" mb="sm">Dokumen</Text>
<Text fz="md" fw="bold" mb="sm">Dokumen Yang Perlu Dibawa</Text>
<CreateEditor
value={stateJadwalKegiatan.create.form.dokumenJadwalKegiatan.content}
onChange={(e) => {
@@ -181,65 +173,6 @@ function CreateJadwalKegiatan() {
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold" mb="sm">Pendaftaran Jadwal Kegiatan</Text>
<TextInput
label="Nama"
required
placeholder="Masukkan nama"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name = e.target.value;
}}
/>
<TextInput
type="date"
required
label="Tanggal"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal = e.target.value;
}}
/>
<TextInput
label="Nama Orangtua"
required
placeholder="Masukkan nama orangtua"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua = e.target.value;
}}
/>
<TextInput
label="Nomor"
required
placeholder="Masukkan nomor"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor = e.target.value;
}}
/>
<TextInput
label="Alamat"
required
placeholder="Masukkan alamat"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat = e.target.value;
}}
/>
<TextInput
label="Catatan"
required
placeholder="Masukkan catatan"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan = e.target.value;
}}
/>
</Box>
{/* Save Button */}
<Group justify="right">
<Button

View File

@@ -111,11 +111,14 @@ function ListJadwalKegiatan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.informasijadwalkegiatan.name}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.informasijadwalkegiatan.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString(
'id-ID',
{
@@ -124,12 +127,19 @@ function ListJadwalKegiatan({ search }: { search: string }) {
year: 'numeric',
}
)}
</Box>
</TableTd>
<TableTd>{item.informasijadwalkegiatan.waktu}</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed">
{item.informasijadwalkegiatan.lokasi}
</Text>
<Box w={150}>
{item.informasijadwalkegiatan.waktu}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed">
{item.informasijadwalkegiatan.lokasi}
</Text>
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -124,22 +124,32 @@ function ListKelahiran({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Box>
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>
<Box w={150}>
{item.jenisKelamin}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed">
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -13,152 +13,150 @@ import colors from '@/con/colors';
function DetailKematian() {
const state = useProxy(persentaseKelahiranKematian.kematian);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
const state = useProxy(persentaseKelahiranKematian.kematian);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams();
const router = useRouter();
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran");
}
};
const handleHapus = () => {
if (selectedId) {
state.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran");
}
};
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = state.findUnique.data;
const data = state.findUnique.data;
return (
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
return (
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<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 Data Kematian
</Text>
<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 Data Kematian
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tanggal</Text>
<Text fz="md" c="dimmed">
{data?.tanggal instanceof Date
? data.tanggal.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})
: data?.tanggal || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tanggal</Text>
<Text fz="md" c="dimmed">
{new Date(data.tanggal).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{data?.jenisKelamin || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{data?.jenisKelamin || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data?.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data?.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Penyebab</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.penyebab || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Penyebab</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.penyebab || '-' }} />
</Box>
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${data.id}/edit`
)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${data.id}/edit`
)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus data ini?"
/>
</Box>
);
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus data ini?"
/>
</Box>
);
}

View File

@@ -122,19 +122,33 @@ function ListKematian({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
{item.jenisKelamin}
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed">
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button
variant="light"

View File

@@ -4,7 +4,7 @@ import {
Box,
Button,
Center,
Image,
Group,
Pagination,
Paper,
Skeleton,
@@ -17,16 +17,15 @@ import {
TableTr,
Text,
Title,
Tooltip,
Group,
Tooltip
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
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 infoWabahPenyakit from '../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
function InfoWabahPenyakit() {
const [search, setSearch] = useState("");
@@ -96,7 +95,6 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -105,17 +103,18 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed">
{item.deskripsiSingkat}
</Text>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>
<Box w={200}>
<Text truncate fz="sm" c="dimmed">
{item.deskripsiSingkat}
</Text>
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -4,7 +4,7 @@ import {
Box,
Button,
Center,
Image,
Group,
Pagination,
Paper,
Skeleton,
@@ -17,7 +17,7 @@ import {
TableTr,
Text,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -71,7 +71,7 @@ function ListKontakDarurat({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Judul + Tombol Tambah */}
<Stack mb="md" gap="sm">
<Box display="flex" style={{ justifyContent: "space-between", alignItems: "center" }}>
<Group justify="space-between">
<Title order={4}>Daftar Kontak Darurat</Title>
<Tooltip label="Tambah Kontak Darurat" withArrow>
<Button
@@ -83,7 +83,7 @@ function ListKontakDarurat({ search }: { search: string }) {
Tambah Baru
</Button>
</Tooltip>
</Box>
</Group>
</Stack>
{/* Tabel */}
@@ -93,7 +93,6 @@ function ListKontakDarurat({ search }: { search: string }) {
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -102,15 +101,16 @@ function ListKontakDarurat({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>
<Box w={200}>
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -5,7 +5,6 @@ import {
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
@@ -18,15 +17,15 @@ import {
TableTr,
Text,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat';
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 penangananDarurat from '../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function PenangananDarurat() {
const [search, setSearch] = useState("");
@@ -91,7 +90,6 @@ function ListPenangananDarurat({ search }: { search: string }) {
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -100,21 +98,22 @@ function ListPenangananDarurat({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Text
fz="sm"
c="dimmed"
truncate
lineClamp={1}
<Box w={200}>
<Text
fz="sm"
c="dimmed"
truncate
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" loading="lazy"/>
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -107,21 +107,28 @@ function ListPosyandu({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Box w={150}>
<Text truncate fz="sm" c="dimmed">
{item.nomor || '-'}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Box w={150}>
<Text
lineClamp={1}
truncate
fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button

View File

@@ -5,7 +5,6 @@ import {
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
@@ -18,15 +17,15 @@ import {
TableTr,
Text,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation';
import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
import { useProxy } from 'valtio/utils';
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 programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
function ProgramKesehatan() {
const [search, setSearch] = useState("");
@@ -90,7 +89,7 @@ function ListProgramKesehatan({ search }: { search: string }) {
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -105,11 +104,13 @@ function ListProgramKesehatan({ search }: { search: string }) {
</TableTd>
<TableTd>
<Box w={200}>
<Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box>
</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>
<Box w={200}>
<Text fz="sm" truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -78,8 +78,9 @@ function DetailPuskesmas() {
<Box>
<Text fz="lg" fw="bold">Jam Operasional</Text>
<Text fz="md" c="dimmed">{data?.jam?.workDays || '-'}</Text>
<Text fz="md" c="dimmed">{data?.jam?.weekDays || '-'}</Text>
<Text fz="md" c="dimmed">Senin - Jumat</Text>
<Text fz="md" c="dimmed">{data?.jam?.workDays || '-'} - {data?.jam?.weekDays || '-'}</Text>
<Text fz="md" c="dimmed">Sabtu - Minggu / Hari Libur</Text>
<Text fz="md" c="dimmed">{data?.jam?.holiday || '-'}</Text>
</Box>
@@ -94,9 +95,13 @@ function DetailPuskesmas() {
<Box>
<Text fz="lg" fw="bold">Kontak</Text>
<Text fz="md" c="dimmed">Kontak Puskesmas</Text>
<Text fz="md" c="dimmed">{data?.kontak?.kontakPuskesmas || '-'}</Text>
<Text fz="md" c="dimmed">Email</Text>
<Text fz="md" c="dimmed">{data?.kontak?.email || '-'}</Text>
<Text fz="md" c="dimmed">Facebook</Text>
<Text fz="md" c="dimmed">{data?.kontak?.facebook || '-'}</Text>
<Text fz="md" c="dimmed">Kontak UGD</Text>
<Text fz="md" c="dimmed">{data?.kontak?.kontakUGD || '-'}</Text>
</Box>

View File

@@ -5,7 +5,6 @@ import {
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
@@ -18,7 +17,7 @@ import {
TableTr,
Text,
Title,
Tooltip,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
@@ -90,7 +89,7 @@ function ListPuskesmas({ search }: { search: string }) {
<TableTr>
<TableTh>Nama Puskesmas</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Kontak</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
@@ -99,13 +98,25 @@ function ListPuskesmas({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Image w={100} src={item.image.link} alt="image" radius="md" loading="lazy"/>
<Box w={150}>
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.alamat}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text truncate fz="sm" c="dimmed" lineClamp={1}>
{item.kontak.kontakPuskesmas}
</Text>
</Box>
</TableTd>
<TableTd>
<Button

View File

@@ -87,7 +87,9 @@ function ListAPBDes({ search }: { search: string }) {
<Text fw={500} truncate="end">{item.name}</Text>
</TableTd>
<TableTd>
<Text>Rp. {item.jumlah}</Text>
<Box w={150}>
<Text>Rp. {item.jumlah}</Text>
</Box>
</TableTd>
<TableTd>
{item.file?.link ? (

View File

@@ -93,7 +93,9 @@ function ListKategoriKegiatan({ search }: { search: string }) {
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500}>{item.name}</Text>
<Box w={200}>
<Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>

View File

@@ -98,9 +98,11 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
</Text>
</TableTd>
<TableTd style={{ width: '30%' }}>
<Text fz="sm" c="dimmed">
<Box w={200}>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.kategori?.name || '-'}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>
<Button

View File

@@ -154,7 +154,7 @@ function Page() {
withLabels
withTooltip
labelsType="percent"
size={220}
size={180}
data={donutDataJenisKelamin}
/>
</Center>
@@ -185,7 +185,7 @@ function Page() {
withLabels
withTooltip
labelsType="percent"
size={220}
size={180}
data={donutDataRating}
/>
</Center>
@@ -216,7 +216,7 @@ function Page() {
withLabels
withTooltip
labelsType="percent"
size={220}
size={180}
data={donutDataKelompokUmur}
/>
</Center>

View File

@@ -88,66 +88,74 @@ function ListResponden({ search }: ListRespondenProps) {
<Title order={4} mb="sm">
Daftar Responden
</Title>
<Table
striped
highlightOnHover
withRowBorders
verticalSpacing="sm"
>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '25%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<Box style={{ overflowX: 'auto' }}>
<Table
striped
highlightOnHover
withRowBorders
verticalSpacing="sm"
>
<TableThead>
<TableTr>
<TableTd colSpan={5}>
<Text ta="center" c="dimmed">
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '25%', textAlign: 'center' }}>Nama</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jenis Kelamin</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Aksi</TableTh>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{index + 1}</TableTd>
<TableTd ta="center">{item.name}</TableTd>
<TableTd ta="center">
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID', {
</TableThead>
<TableTbody>
{filteredData.length === 0 ? (
<TableTr>
<TableTd colSpan={5}>
<Text ta="center" c="dimmed">
Tidak ditemukan data dengan kata kunci pencarian
</Text>
</TableTd>
</TableTr>
) : (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{index + 1}</TableTd>
<TableTd ta="center">{item.name}</TableTd>
<TableTd ta="center">
<Box w={150}>
{item.tanggal
? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</TableTd>
<TableTd ta="center">{item.jenisKelamin.name}</TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
: '-'}
</Box>
</TableTd>
<TableTd ta="center">
<Box w={100}>
{item.jenisKelamin.name}
</Box>
</TableTd>
<TableTd ta="center">
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/landing-page/indeks-kepuasan-masyarakat/responden/${item.id}`
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
)}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination

View File

@@ -96,7 +96,11 @@ function ListKategoriPrestasi({ search }: { search: string }) {
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Box w={200}>
<Text truncate="end" fz={"sm"}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center', width: '120px' }}>
<Tooltip label="Edit" withArrow position="top">
<Button

View File

@@ -78,9 +78,9 @@ function DetailPrestasiDesa() {
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Box
fz="md"
c="dimmed"
<Box
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: detailState.findUnique.data?.deskripsi || '-' }}
/>
</Box>
@@ -91,10 +91,11 @@ function DetailPrestasiDesa() {
<Image
src={detailState.findUnique.data.image.link}
alt={detailState.findUnique.data.name || 'Gambar Prestasi'}
w={300}
w="100%"
maw={300} // max width 300px
fit="contain"
style={{ borderRadius: '8px', border: '1px solid #e0e0e0' }}
loading='lazy'
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>

View File

@@ -104,7 +104,9 @@ function ListPrestasi({ search }: { search: string }) {
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd style={{ width: '25%' }}>
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
<Box w={150}>
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
</Box>
</TableTd>
<TableTd style={{ width: '25%', textAlign: 'center' }}>
<Tooltip label="Kelola Prestasi" withArrow>

View File

@@ -51,8 +51,10 @@ function DetailMediaSosial() {
</Button>
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
withBorder
w="100%"
maw={500} // <= tambahkan ini, biar tidak lebih dari 500px
mx="auto" // center di layar
bg={colors['white-1']}
p="lg"
radius="md"
@@ -70,9 +72,9 @@ function DetailMediaSosial() {
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Box >
<Text fz="lg" fw="bold">Link / Nomor Telepon</Text>
<Text fz="md" c="dimmed">{data.iconUrl || '-'}</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-all", whiteSpace: "pre-wrap" }}>{data.iconUrl || '-'}</Text>
</Box>
<Box>
@@ -81,12 +83,14 @@ function DetailMediaSosial() {
<Image
src={data.image.link}
alt={data.name || 'Gambar Media Sosial'}
w={120}
h={120}
w="100%"
maw={120} // max width biar tidak keluar layar
h="auto"
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}

View File

@@ -224,7 +224,7 @@ function EditPejabatDesa() {
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
maxHeight: '150px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',

View File

@@ -72,7 +72,7 @@ function Page() {
<Image
pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
w={{ base: 150, md: 350 }}
alt="Foto Profil Pejabat"
radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
@@ -87,7 +87,7 @@ function Page() {
className="glass3"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1rem", md: "1.6rem" }}>
{item.name}
</Text>
</Paper>

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } 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 { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -49,9 +49,7 @@ function ListProgramInovasi({ search }: { search: string }) {
return (
<Box py={15}>
<Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
<Box mb="md" display="flex"
style={{ justifyContent: 'space-between', alignItems: 'center' }}
>
<Group justify='space-between'>
<Title order={4}>Daftar Program Inovasi</Title>
<Tooltip label="Tambah Program Inovasi" withArrow>
<Button
@@ -64,7 +62,7 @@ function ListProgramInovasi({ search }: { search: string }) {
Tambah Program
</Button>
</Tooltip>
</Box>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>

View File

@@ -72,7 +72,8 @@ function Page() {
<Image
pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"}
w={{ base: 250, md: 350 }}
w="100%"
maw={300}
alt="Foto Profil PPID"
radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}

View File

@@ -117,14 +117,14 @@ function ListPegawaiPPID({ search }: { search: string }) {
).map((item) => (
<TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Box w={150}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.namaLengkap}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Box w={150}>
<Badge variant="light" color="blue">
{item.posisi?.nama || 'Belum diatur'}
</Badge>

View File

@@ -68,10 +68,10 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Posisi Organisasi PPID</Title>
<Tooltip label="Tambah Posisi Organisasi" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ppid/struktur-ppid/posisi-organisasi/create')}
>
Tambah Baru
@@ -82,51 +82,54 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '45%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%' }}>Hierarki</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh>
<TableTh style={{ width: '20%' }}>Nama Posisi</TableTh>
<TableTh style={{ width: '20%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '20%' }}>Hierarki</TableTh>
<TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh style={{ width: '20%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ width: '25%' }}>
<TableTd style={{ width: '20%' }}>
<Text fw={500} truncate="end" lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd style={{ width: '45%' }}>
<Text lineClamp={2} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
<TableTd style={{ width: '20%' }}>
<Box w={200}>
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi || '-' }} />
</Box>
</TableTd>
<TableTd style={{ width: '15%' }}>
<TableTd style={{ width: '20%' }}>
<Text>{item.hierarki || '-'}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Group gap="xs">
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</Group>
<TableTd style={{ width: '20%' }}>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="green"
size="sm"
onClick={() => router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))

View File

@@ -11,6 +11,7 @@ import KategoriPotensi from "./potensi/kategori-potensi";
import KategoriBerita from "./berita/kategori-berita";
import KategoriPengumuman from "./pengumuman/kategori-pengumuman";
import MantanPerbekel from "./profile/profile-mantan-perbekel";
import AjukanPermohonan from "./layanan/ajukan_permohonan";
const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
@@ -26,6 +27,7 @@ const Desa = new Elysia({ prefix: "/api/desa", tags: ["Desa"] })
.use(KategoriPotensi)
.use(KategoriBerita)
.use(KategoriPengumuman)
.use(AjukanPermohonan)
export default Desa;

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
nama: string;
nik: string;
alamat: string;
nomorKk: string;
kategoriId: string;
}
export default async function createAjukanPermohonan(context: Context){
const body = context.body as FormCreate;
await prisma.ajukanPermohonan.create({
data: {
nama: body.nama,
nik: body.nik,
alamat: body.alamat,
nomorKk: body.nomorKk,
kategoriId: body.kategoriId
}
})
return {
success: true,
message: 'Ajukan permohonan berhasil dibuat',
data: {
...body
}
}
}

View File

@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function deleteAjukanPermohonan(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
message: "ID tidak diberikan",
};
}
await prisma.ajukanPermohonan.delete({
where: { id },
});
return {
status: 200,
message: "Ajukan permohonan berhasil dihapus",
};
}

View File

@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function findManyAjukanPermohonan(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ nama: { contains: search, mode: 'insensitive' } },
{ nik: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
{ nomorKk: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.ajukanPermohonan.findMany({
where,
include: {
kategori: true
},
skip,
take: limit,
orderBy: { createdAt: 'asc' },
}),
prisma.ajukanPermohonan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil data dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data",
};
}
}
export default findManyAjukanPermohonan;

View File

@@ -0,0 +1,66 @@
import prisma from "@/lib/prisma";
export default async function findUniqueAjukanPermohonan(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json(
{
success: false,
message: "ID tidak boleh kosong",
},
{ status: 400 }
);
}
try {
if (typeof id !== "string") {
return Response.json(
{
success: false,
message: "ID tidak valid",
},
{ status: 400 }
);
}
const data = await prisma.ajukanPermohonan.findUnique({
where: { id },
include: {
kategori: true,
},
});
if (!data) {
return Response.json(
{
success: false,
message: "Ajukan permohonan tidak ditemukan",
},
{ status: 404 }
);
}
return Response.json(
{
success: true,
message: "Success fetch ajukan permohonan by ID",
data,
},
{ status: 200 }
);
} catch (e) {
console.error("Find by ID error:", e);
return Response.json(
{
success: false,
message:
"Gagal mengambil ajukan permohonan: " +
(e instanceof Error ? e.message : "Unknown error"),
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,36 @@
import Elysia, { t } from "elysia";
import createAjukanPermohonan from "./create";
import findManyAjukanPermohonan from "./findMany";
import findUniqueAjukanPermohonan from "./findUnique";
import updateAjukanPermohonan from "./updt";
import deleteAjukanPermohonan from "./del";
const AjukanPermohonan = new Elysia({
prefix: "ajukanpermohonan",
tags: ["Desa/Layanan/AjukanPermohonan"],
})
.get("/findMany", findManyAjukanPermohonan)
.post("/create", createAjukanPermohonan, {
body: t.Object({
nama: t.String(),
nik: t.String(),
alamat: t.String(),
nomorKk: t.String(),
kategoriId: t.String(),
})
})
.get("/:id", findUniqueAjukanPermohonan)
.put("/:id", updateAjukanPermohonan, {
body: t.Object({
nama: t.String(),
nik: t.String(),
alamat: t.String(),
nomorKk: t.String(),
kategoriId: t.String(),
})
})
.delete("/del/:id", deleteAjukanPermohonan)
export default AjukanPermohonan;

View File

@@ -0,0 +1,48 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
id: string;
nama: string;
nik: string;
alamat: string;
nomorKk: string;
kategoriId: string;
};
export default async function updateAjukanPermohonan(context: Context) {
const id = context.params?.id;
const body = context.body as FormUpdate;
if (!id) {
return {
success: false,
message: "ID tidak diberikan",
};
}
const existing = await prisma.ajukanPermohonan.findUnique({
where: { id },
include: {
kategori: true,
},
});
if (!existing) {
return {
success: false,
message: "Ajukan permohonan tidak ditemukan",
};
}
const updated = await prisma.ajukanPermohonan.update({
where: { id },
data: body,
});
return {
success: true,
message: "Success update ajukan permohonan",
data: updated,
};
}

View File

@@ -0,0 +1,27 @@
import prisma from "@/lib/prisma";
export default async function pelayananSuratKeteranganFindManyAll() {
try {
const data = await prisma.pelayananSuratKeterangan.findMany({
where: { isActive: true },
orderBy: { createdAt: "desc" },
include: {
image: true,
image2: true,
},
});
return {
success: true,
message: "Berhasil ambil semua data pelayanan surat keterangan",
data,
};
} catch (e) {
console.error("Error di findManyAll:", e);
return {
success: false,
message: "Gagal mengambil data pelayanan surat keterangan",
data: [],
};
}
}

View File

@@ -4,11 +4,12 @@ import pelayananSuratKeteranganFindUnique from "./findUnique";
import pelayananSuratKeteranganCreate from "./create";
import pelayananSuratKeteranganUpdate from "./updt";
import pelayananSuratKeteranganDelete from "./del";
import pelayananSuratKeteranganFindManyAll from "./findManyAll";
import { t } from "elysia";
const PelayananSuratKeterangan = new Elysia({ prefix: "/pelayanansuratketerangan", tags: ["Desa/Layanan/Pelayanan Surat Keterangan"] })
.get("/find-many", pelayananSuratKeteranganFindMany)
.get("/findManyAll", pelayananSuratKeteranganFindManyAll)
.get("/:id", async (context) => {
const response = await pelayananSuratKeteranganFindUnique(new Request(context.request));
return response;

View File

@@ -8,7 +8,6 @@ type JadwalKegiatanInput = {
layananJadwalKegiatan: { content: string };
syaratKetentuanJadwalKegiatan: { content: string };
dokumenJadwalKegiatan: { content: string };
pendaftaranJadwalKegiatan: { name: string, tanggal: string, namaOrangtua: string, nomor: string, alamat: string, catatan: string };
}
const jadwalKegiatanCreate = async (context: Context) => {
@@ -21,16 +20,14 @@ const jadwalKegiatanCreate = async (context: Context) => {
layananJadwalKegiatan,
syaratKetentuanJadwalKegiatan,
dokumenJadwalKegiatan,
pendaftaranJadwalKegiatan,
} = body;
const [createdInformasiJadwalKegiatan, createdDeskripsiJadwalKegiatan, createdLayananJadwalKegiatan, createdSyaratKetentuanJadwalKegiatan, createdDokumenJadwalKegiatan, createdPendaftaranJadwalKegiatan] = await Promise.all([
const [createdInformasiJadwalKegiatan, createdDeskripsiJadwalKegiatan, createdLayananJadwalKegiatan, createdSyaratKetentuanJadwalKegiatan, createdDokumenJadwalKegiatan] = await Promise.all([
prisma.informasiJadwalKegiatan.create({ data: informasiJadwalKegiatan }),
prisma.deskripsiJadwalKegiatan.create({ data: deskripsiJadwalKegiatan }),
prisma.layananJadwalKegiatan.create({ data: layananJadwalKegiatan }),
prisma.syaratKetentuanJadwalKegiatan.create({ data: syaratKetentuanJadwalKegiatan }),
prisma.dokumenJadwalKegiatan.create({ data: dokumenJadwalKegiatan }),
prisma.pendaftaranJadwalKegiatan.create({ data: pendaftaranJadwalKegiatan }),
])
const jadwalKegiatan = await prisma.jadwalKegiatan.create({
@@ -41,7 +38,6 @@ const jadwalKegiatanCreate = async (context: Context) => {
layananJadwalKegiatanId: createdLayananJadwalKegiatan.id,
syaratKetentuanJadwalKegiatanId: createdSyaratKetentuanJadwalKegiatan.id,
dokumenJadwalKegiatanId: createdDokumenJadwalKegiatan.id,
pendaftaranJadwalKegiatanId: createdPendaftaranJadwalKegiatan.id,
},
include: {
informasijadwalkegiatan: true,
@@ -49,7 +45,6 @@ const jadwalKegiatanCreate = async (context: Context) => {
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
}
})
return {

View File

@@ -19,7 +19,6 @@ const jadwalKegiatanDelete = async (context: Context) => {
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
}
})

View File

@@ -20,7 +20,6 @@ export default async function jadwalKegiatanFindMany(context: Context) {
{layananjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
{syaratketentuanjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
{dokumenjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
{pendaftaranjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
];
}
try {
@@ -33,8 +32,7 @@ export default async function jadwalKegiatanFindMany(context: Context) {
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
},
},
skip,
take: limit,
orderBy: { createdAt: "desc" },

View File

@@ -28,7 +28,6 @@ export default async function jadwalKegiatanFindUnique(request: Request) {
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
}
})

View File

@@ -30,14 +30,6 @@ const JadwalKegiatan = new Elysia({
dokumenJadwalKegiatan: t.Object({
content: t.String(),
}),
pendaftaranJadwalKegiatan: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
}),
})
.get("/find-many", jadwalKegiatanFindMany)
@@ -75,14 +67,6 @@ const JadwalKegiatan = new Elysia({
dokumenJadwalKegiatan: t.Object({
content: t.String(),
}),
pendaftaranJadwalKegiatan: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
}),
}
);

View File

@@ -13,14 +13,6 @@ type JadwalKegiatanUpdateInput = {
layananJadwalKegiatan: { content: string };
syaratKetentuanJadwalKegiatan: { content: string };
dokumenJadwalKegiatan: { content: string };
pendaftaranJadwalKegiatan: {
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
};
};
const jadwalKegiatanUpdate = async (context: Context) => {
@@ -50,7 +42,6 @@ const jadwalKegiatanUpdate = async (context: Context) => {
layananJadwalKegiatan,
syaratKetentuanJadwalKegiatan,
dokumenJadwalKegiatan,
pendaftaranJadwalKegiatan,
} = body;
await Promise.all([
@@ -74,10 +65,6 @@ const jadwalKegiatanUpdate = async (context: Context) => {
where: { id: existing.dokumenJadwalKegiatanId },
data: dokumenJadwalKegiatan
}),
prisma.pendaftaranJadwalKegiatan.update({
where: { id: existing.pendaftaranJadwalKegiatanId },
data: pendaftaranJadwalKegiatan,
}),
]);
const updated = await prisma.jadwalKegiatan.update({
@@ -91,7 +78,6 @@ const jadwalKegiatanUpdate = async (context: Context) => {
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
},
});
return {

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Box, Container, Grid, GridCol, ScrollArea, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { Box, Group, Stack, Tabs, TabsList, TabsTab, Text, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect, useState } from 'react';
@@ -23,95 +23,52 @@ function LayoutTabsBerita({
const pathname = usePathname();
const searchParams = useSearchParams();
// Get active tab from URL path
const activeTab = pathname.split('/').pop() || 'semua';
// Get initial search value from URL
const initialSearch = searchParams.get('search') || '';
const [searchValue, setSearchValue] = useState(initialSearch);
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
// Update active tab state when pathname changes
const [activeTabState, setActiveTabState] = useState(activeTab);
useEffect(() => {
setActiveTabState(activeTab);
}, [activeTab]);
// Clean up timeouts on unmount
useEffect(() => {
return () => {
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
if (searchTimeout !== null) clearTimeout(searchTimeout);
};
}, [searchTimeout]);
// Handle search input change with debounce
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchValue(value);
// Clear previous timeout
if (searchTimeout !== null) {
clearTimeout(searchTimeout);
}
if (searchTimeout !== null) clearTimeout(searchTimeout);
// Set new timeout
const newTimeout = window.setTimeout(() => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set('search', value);
} else {
params.delete('search');
}
if (value) params.set('search', value);
else params.delete('search');
// Only update URL if the search value has actually changed
if (params.toString() !== searchParams.toString()) {
router.push(`/darmasaba/desa/berita/${activeTab}?${params.toString()}`);
}
}, 500); // 500ms debounce delay
}, 500);
setSearchTimeout(newTimeout);
};
const tabs = [
{
label: "Semua",
value: "semua",
href: "/darmasaba/desa/berita/semua"
},
{
label: "Budaya",
value: "budaya",
href: "/darmasaba/desa/berita/budaya"
},
{
label: "Pemerintahan",
value: "pemerintahan",
href: "/darmasaba/desa/berita/pemerintahan"
},
{
label: "Ekonomi",
value: "ekonomi",
href: "/darmasaba/desa/berita/ekonomi"
},
{
label: "Pembangunan",
value: "pembangunan",
href: "/darmasaba/desa/berita/pembangunan"
},
{
label: "Sosial",
value: "sosial",
href: "/darmasaba/desa/berita/sosial"
},
{
label: "Teknologi",
value: "teknologi",
href: "/darmasaba/desa/berita/teknologi"
},
const tabs = [
{ label: "Semua", value: "semua", href: "/darmasaba/desa/berita/semua" },
{ label: "Budaya", value: "budaya", href: "/darmasaba/desa/berita/budaya" },
{ label: "Pemerintahan", value: "pemerintahan", href: "/darmasaba/desa/berita/pemerintahan" },
{ label: "Ekonomi", value: "ekonomi", href: "/darmasaba/desa/berita/ekonomi" },
{ label: "Pembangunan", value: "pembangunan", href: "/darmasaba/desa/berita/pembangunan" },
{ label: "Sosial", value: "sosial", href: "/darmasaba/desa/berita/sosial" },
{ label: "Teknologi", value: "teknologi", href: "/darmasaba/desa/berita/teknologi" },
];
const handleTabChange = (value: string | null) => {
if (!value) return;
const tab = tabs.find(t => t.value === value);
@@ -127,16 +84,29 @@ function LayoutTabsBerita({
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Stack align="center" gap="0" >
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Portal Berita Darmasaba
</Text>
<Text ta="center" px="md">
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
</Container>
<Box px={{ base: 'md', md: 100 }}>
<Group justify='space-between' align="center">
<Stack gap="0">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" >
Portal Berita Darmasaba
</Text>
<Text>
Temukan berbagai potensi dan keunggulan yang dimiliki Desa Darmasaba
</Text>
</Stack>
<Box>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</Box>
</Group>
</Box>
<Tabs
color={colors['blue-button']}
@@ -145,33 +115,25 @@ function LayoutTabsBerita({
onChange={handleTabChange}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
value={searchValue}
onChange={handleSearchChange}
/>
</GridCol>
</Grid>
{/* SCROLLABLE TABS */}
<Box style={{ overflowX: 'auto', whiteSpace: 'nowrap' }}>
<TabsList style={{ display: 'flex', flexWrap: 'nowrap', gap: '0.5rem' }}>
{tabs.map((tab, index) => (
<TabsTab
key={index}
value={tab.value}
onClick={() => router.push(tab.href)}
style={{
flex: '0 0 auto', // Prevent shrinking
minWidth: 100, // optional: makes them touch-friendly
textAlign: 'center'
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</Box>
</Box>
{children}
@@ -180,4 +142,4 @@ function LayoutTabsBerita({
);
}
export default LayoutTabsBerita;
export default LayoutTabsBerita;

View File

@@ -1,12 +1,12 @@
'use client';
import { useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Box, Container, Grid, GridCol, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import BackButton from '../../layanan/_com/BackButto';
import dynamic from 'next/dynamic';
import type { SearchBarProps } from './searchBar';
import colors from '@/con/colors';
import { Box, Container, Stack, Tabs, TabsList, TabsTab, Text } from '@mantine/core';
import dynamic from 'next/dynamic';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import BackButton from '../../layanan/_com/BackButto';
import type { SearchBarProps } from './searchBar';
// Define tabs outside the component to ensure consistency between server and client
const TABS = [
@@ -35,7 +35,7 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
const router = useRouter();
const pathname = usePathname();
const [isClient, setIsClient] = useState(false);
// Set default active tab to empty string to prevent hydration mismatch
const [activeTab, setActiveTab] = useState('');
@@ -47,7 +47,7 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
// Update active tab based on current route - only on client side
useEffect(() => {
if (!isClient) return;
const currentTab = TABS.find(tab => pathname.includes(tab.value));
if (currentTab) {
setActiveTab(currentTab.value);
@@ -75,42 +75,38 @@ function LayoutTabsGalery({ children }: HeaderSearchProps) {
<Box px={{ base: "md", md: 100 }}>
<BackButton />
</Box>
<Container size="lg" px="md">
<Box px={{ base: "md", md: 100 }}>
<Stack align="center" gap="0">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Galeri Kegiatan Desa Darmasaba
</Text>
</Stack>
</Container>
<Box>
<SearchBar />
</Box>
</Box>
<Tabs
<Tabs
value={isClient ? activeTab : undefined}
defaultValue={TABS[0].value}
onChange={handleTabChange}
color={colors['blue-button']}
color={colors['blue-button']}
variant="pills"
keepMounted={false}
>
<Box px={{ base: "md", md: 100 }} py="md" bg={colors['BG-trans']}>
<Grid>
<GridCol span={{ base: 12, md: 9, lg: 8, xl: 9 }}>
<TabsList>
{TABS.map((tab) => (
<TabsTab
key={tab.value}
value={tab.value}
component="button"
type="button"
>
{tab.label}
</TabsTab>
))}
</TabsList>
</GridCol>
<GridCol span={{ base: 12, md: 3, lg: 4, xl: 3 }}>
<SearchBar />
</GridCol>
</Grid>
<TabsList>
{TABS.map((tab) => (
<TabsTab
key={tab.value}
value={tab.value}
component="button"
type="button"
>
{tab.label}
</TabsTab>
))}
</TabsList>
</Box>
<Container size={'xl'}>

View File

@@ -2,7 +2,7 @@
'use client';
import { TextInput } from '@mantine/core';
import { Group, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
@@ -65,13 +65,15 @@ export function SearchBar({
}, []);
return (
<TextInput
<Group justify='center'>
<TextInput
radius="lg"
placeholder={placeholder}
leftSection={searchIcon}
w="100%"
w={{ base: '100%', md: '50%' }}
value={searchValue}
onChange={handleSearchChange}
/>
</Group>
);
}

View File

@@ -2,19 +2,24 @@
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { Box, Center, Pagination, Paper, SimpleGrid, Spoiler, Stack, Text } from '@mantine/core';
import {
Box,
Center,
Pagination,
Paper,
SimpleGrid,
Spoiler,
Stack,
Text,
} from '@mantine/core';
import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
export default function VideoContent() {
const [expanded, setExpanded] = useState(false);
// ✅ expanded state per index
const [expandedMap, setExpandedMap] = useState<Record<number, boolean>>({});
const videoState = useSnapshot(stateGallery.video);
const {
data,
page,
totalPages,
loading,
} = videoState.findMany;
const { data, page, totalPages, loading } = videoState.findMany;
// Handle search and pagination changes
const loadData = useCallback((pageNum: number, searchTerm: string) => {
@@ -27,24 +32,18 @@ export default function VideoContent() {
const urlParams = new URLSearchParams(window.location.search);
const urlSearch = urlParams.get('search') || '';
const urlPage = parseInt(urlParams.get('page') || '1');
loadData(urlPage, urlSearch);
};
// Handle search updates from the search bar
const handleSearchUpdate = (e: Event) => {
const { search } = (e as CustomEvent).detail;
loadData(1, search);
};
// Initial load
handleRouteChange();
// Set up event listeners
window.addEventListener('popstate', handleRouteChange);
window.addEventListener('searchUpdate', handleSearchUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.removeEventListener('searchUpdate', handleSearchUpdate as EventListener);
@@ -57,6 +56,13 @@ export default function VideoContent() {
loadData(newPage, search);
};
const toggleExpanded = (index: number, value: boolean) => {
setExpandedMap((prev) => ({
...prev,
[index]: value,
}));
};
const dataVideo = data || [];
if (loading && !data) {
@@ -72,7 +78,13 @@ export default function VideoContent() {
<SimpleGrid cols={{ base: 1, md: 3 }}>
{dataVideo.map((v, k) => (
<Box key={k}>
<Paper mb={50} p="md" radius={26} bg={colors['white-trans-1']} w={{ base: '100%', md: '100%' }}>
<Paper
mb={50}
p="md"
radius={26}
bg={colors['white-trans-1']}
w={{ base: '100%', md: '100%' }}
>
<Box>
<Center>
<Box
@@ -109,8 +121,8 @@ export default function VideoContent() {
Hide details
</Text>
}
expanded={expanded}
onExpandedChange={setExpanded}
expanded={expandedMap[k] || false}
onExpandedChange={(val) => toggleExpanded(k, val)}
>
<Text
ta="justify"
@@ -137,15 +149,15 @@ export default function VideoContent() {
);
}
// ✅ Fix: HAPUS SPASI BERLEBIH DI URL
// ✅ Fix: convert YouTube URL ke embed
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get('v');
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`; // ✅ tanpa spasi!
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
console.error('Error converting YouTube URL to embed:', err);
return youtubeUrl;
}
}
}

View File

@@ -2,11 +2,12 @@
'use client'
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors';
import { Box, Button, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Button, Center, Container, Group, Image, Modal, Paper, Select, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../_com/BackButto';
import { useDisclosure } from '@mantine/hooks';
interface LayananData {
id: string;
@@ -31,7 +32,12 @@ function Page() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<LayananData | null>(null);
const [opened, { open, close }] = useDisclosure(false);
const stateCreate = useProxy(stateLayananDesa.ajukanPermohonan);
useEffect(() => {
state.suratKeterangan.findManyAll.load()
const loadData = async () => {
if (!id) return;
try {
@@ -48,6 +54,22 @@ function Page() {
loadData();
}, [id]);
const resetForm = () => {
stateCreate.create.form = {
nama: '',
nik: '',
alamat: '',
nomorKk: '',
kategoriId: '',
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
close();
}
if (loading) {
return (
<Center h="100vh" bg={colors.Bg}>
@@ -105,12 +127,76 @@ function Page() {
size="lg"
variant="gradient"
gradient={{ from: '#1C6EA4', to: '#63B3ED' }}
onClick={open}
>
Ajukan Permohonan
</Button>
</Group>
</Stack>
</Box>
<Modal
opened={opened}
onClose={close}
radius={0}
transitionProps={{ transition: 'fade', duration: 200 }}
>
<Paper p="md" withBorder>
<Stack gap="xs">
<Title order={3}>Ajukan Permohonan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
onChange={(val) => (stateCreate.create.form.nama = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">NIK</Text>}
placeholder="masukkan NIK"
onChange={(val) => (stateCreate.create.form.nik = val.target.value)}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
onChange={(val) => (stateCreate.create.form.alamat = val.target.value)}
/>
<TextInput
type="number"
label={<Text fz="sm" fw="bold">Nomor KK</Text>}
placeholder="masukkan Nomor KK"
onChange={(val) => (stateCreate.create.form.nomorKk = val.target.value)}
/>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateLayananDesa.suratKeterangan.findManyAll.data?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={stateCreate.create.form.kategoriId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateLayananDesa.suratKeterangan.findMany.data?.find(
(item) => item.id === val
);
if (selected) {
stateCreate.create.form.kategoriId = selected.id;
}
} else {
stateCreate.create.form.kategoriId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan
</Button>
</Stack>
</Paper>
</Modal>
</Stack>
);
}

View File

@@ -22,7 +22,7 @@ export default function Page() {
<Container w={{ base: "100%", md: "50%" }} >
<Stack align="center" gap={0}>
{/* Bagian Layanan */}
<Text fz={{ base: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Text fz={{ base: "1.8rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Layanan Desa Darmasaba
</Text>
<Text

View File

@@ -1,7 +1,7 @@
'use client'
import stateDesaPengumuman from '@/app/admin/(dashboard)/_state/desa/pengumuman';
import colors from '@/con/colors';
import { Box, Container, Flex, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { Box, Container, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
@@ -31,7 +31,7 @@ function Page() {
</Box>
<Container size="lg" px="md">
<Stack gap="xs" >
<Flex justify={"space-between"} align={"center"}>
<Group justify={"space-between"} align={"center"}>
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
{detail.data?.judul}
</Text>
@@ -40,7 +40,7 @@ function Page() {
<Text c={colors['white-1']}>{detail.data?.CategoryPengumuman?.name}</Text>
</Paper>
</Group>
</Flex>
</Group>
<Paper bg={colors["white-1"]} p="md">
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >

View File

@@ -2,7 +2,7 @@
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Divider, Group, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Divider, Group, Image, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconAlertCircle, IconCalendar, IconInfoCircle } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
@@ -42,6 +42,24 @@ function Page() {
</Text>
</Box>
<Box p="lg">
<Box style={{ position: 'relative', width: '100%', maxWidth: '800px', margin: '0 auto' }}>
<Image
src={state.findUnique.data.image?.link}
alt={state.findUnique.data.title}
height={0}
style={{
height: 'auto',
width: '100%',
maxHeight: '500px',
objectFit: 'contain',
borderRadius: '8px'
}}
loading="lazy"
/>
</Box>
</Box>
<Box p="lg">
<Stack gap="lg">
<Group gap="xs">
@@ -56,7 +74,7 @@ function Page() {
</Group>
<Stack gap="lg">
<Box>
<Text fz="h4" fw="bold">Pendahuluan</Text>
<Divider my="xs" />

View File

@@ -3,9 +3,9 @@
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { ActionIcon, Anchor, AspectRatio, Badge, Box, Button, Card, Chip, CopyButton, Divider, Grid, Group, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowRight, IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconFileDownload, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react';
import { IconBrandWhatsapp, IconCheck, IconCopy, IconDeviceLandlinePhone, IconHeart, IconInfoCircle, IconMail, IconMapPin, IconMoodEmpty, IconSearch, IconStethoscope, IconUser, IconUsersGroup, IconWallet } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { useProxy } from 'valtio/utils';
@@ -16,14 +16,6 @@ interface Kontak {
email: string;
}
interface LayananItem {
nama: string;
keterangan?: string;
}
interface LayananUnggulan {
items: LayananItem[];
}
interface Lokasi {
mapsEmbed: string;
@@ -46,21 +38,23 @@ function Page() {
const data = state.findUnique.data as any; // Temporary any to fix type issues
const nama = data?.name || 'Fasilitas Kesehatan';
const prosedur = data?.prosedurpendaftaran.content || '';
console.log("Prosedur:", data?.prosedurpendaftaran);
const alamat = data?.informasiumum?.alamat || '-';
const jam = data?.informasiumum?.jamOperasional || '-';
const layananUnggulan = (data?.layananunggulan as LayananUnggulan)?.items || [];
const layananUnggulan = data?.layananunggulan || '';
const tenaga = data?.dokterdantenagamedis || null;
const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || '';
const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null;
const kontak = (data?.kontak as Kontak) || {
telepon: '(0361) 123456',
whatsapp: '6289647037426',
email: 'info@fasilitas-kesehatan.id'
const kontak = (data?.kontak as Kontak) || {
telepon: '(0361) 123456',
whatsapp: '6289647037426',
email: 'info@fasilitas-kesehatan.id'
};
const lokasi = (data?.lokasi as Lokasi) || {
const lokasi = (data?.lokasi as Lokasi) || {
mapsEmbed: 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3945.272172359321!2d115.21836257533302!3d-8.569807186941553!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23e9d99b9395f%3A0xb002795fdcb33b30!2sUPTD%20Puskesmas%20Abiansemal%20III!5e0!3m2!1sid!2sid!4v1744792682341!5m2!1sid!2sid'
};
const gratisBpjs = (data?.tarifdanlayanan as TarifDanLayanan)?.gratisBpjs ?? true;
const formatRupiah = useMemo(
@@ -187,22 +181,14 @@ function Page() {
</Group>
<Divider />
<Title order={4}>Layanan Unggulan</Title>
{Array.isArray(layananUnggulan) && layananUnggulan.length > 0 ? (
<List spacing="xs" icon={<ThemeIcon variant="light" radius="xl"><IconArrowRight size={16} /></ThemeIcon>}>
{layananUnggulan.map((x: any, idx: number) => (
<ListItem key={idx}>
<Group gap="xs" wrap="wrap">
<Text fw={600}>{x?.nama || 'Layanan'}</Text>
{x?.keterangan && <Badge variant="light" radius="sm">{x.keterangan}</Badge>}
</Group>
</ListItem>
))}
</List>
<Divider />
{layananUnggulan ? (
<Text fz="md" style={{ lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: layananUnggulan }} />
) : (
<Paper withBorder radius="md" p="md">
<Group gap="sm">
<IconMoodEmpty />
<Text>Tidak ada layanan unggulan yang tercatat.</Text>
<Text>Belum ada informasi fasilitas pendukung.</Text>
</Group>
</Paper>
)}
@@ -322,9 +308,6 @@ function Page() {
<Text fw={600}>Gratis dengan BPJS Kesehatan</Text>
</Group>
)}
<Group>
<Button variant="gradient" gradient={{ from: 'blue', to: 'cyan' }} leftSection={<IconFileDownload size={18} />}>Unduh Brosur (PDF)</Button>
</Group>
</Stack>
</Card>
</Grid.Col>
@@ -336,13 +319,11 @@ function Page() {
<Stack gap="md">
<Title order={3}>Prosedur Pendaftaran</Title>
<Divider />
<List type="ordered" spacing="xs" icon={<ThemeIcon variant="light" radius="xl"><IconArrowRight size={16} /></ThemeIcon>}>
<ListItem>Pendaftaran dibuka pukul 07.30 WITA</ListItem>
<ListItem>Bawa KTP dan Kartu BPJS (jika ada)</ListItem>
<ListItem>Ambil nomor antrian di loket pendaftaran</ListItem>
<ListItem>Pengunjung baru mengisi formulir pendaftaran</ListItem>
<ListItem>Pendaftaran online tersedia melalui aplikasi S-Sehat</ListItem>
</List>
{prosedur ? (
<Text fz="md" style={{ lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: prosedur }} />
) : (
<Text fz="md" c="dimmed">Belum ada prosedur pendaftaran</Text>
)}
</Stack>
</Paper>
</Box>

View File

@@ -4,7 +4,6 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import {
Box,
Button,
Divider,
Group,
Paper,
@@ -13,7 +12,7 @@ import {
Text
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDownload, IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreatePendaftaran from '../create/page';
@@ -107,12 +106,6 @@ function Page() {
</Group>
</Stack>
</Paper>
<Group>
<Button size="md" radius="lg" leftSection={<IconDownload size={18} />} color="green">
Unduh Jadwal Posyandu 2025 (PDF)
</Button>
</Group>
</Stack>
</Box>
</Paper>

View File

@@ -32,7 +32,7 @@ function Page() {
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 6, search);
load(page, 3, search);
}, [page, search]);
if (loading || !data) {
@@ -153,7 +153,7 @@ function Page() {
)}
</Box>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
@@ -164,7 +164,7 @@ function Page() {
mt="lg"
/>
</Center>
)}
</Stack>
);
}

View File

@@ -1,7 +1,7 @@
'use client'
import posyandustate from "@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu";
import colors from "@/con/colors";
import { Badge, Box, Center, Flex, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core";
import { Badge, Box, Center, Flex, Group, Image, List, ListItem, Pagination, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconCalendar, IconInfoCircle, IconPhone, IconSearch } from "@tabler/icons-react";
import { useState } from "react";
@@ -82,14 +82,14 @@ export default function Page() {
}}
>
<Stack gap="sm">
<Flex justify="space-between" align="center">
<Group justify="space-between" align="center">
<Text c={colors["blue-button"]} fw="bold" fz="lg" lineClamp={1}>
{v.name}
</Text>
<Badge color="blue" variant="light" size="sm" radius="sm">
Aktif
</Badge>
</Flex>
</Group>
<Center>
<Image
src={v.image.link}

View File

@@ -90,18 +90,25 @@ function Page() {
<Divider />
<Box>
<Stack gap={"xs"}>
<Title order={3} mb={10}>Jam Operasional</Title>
<Text fw="bold" fz="md">Senin - Jumat</Text>
<Group gap={8}>
<IconClock size={18} />
<Text fw="bold" fz="md">
{data.jam.workDays} - {data.jam.weekDays}
</Text>
<Text fw="bold" fz="md">{data.jam.workDays} - {data.jam.weekDays}</Text>
<Tooltip label="Hari aktif pelayanan puskesmas" position="top" withArrow>
<Badge size="sm" variant="light" color="blue">Aktif</Badge>
</Tooltip>
</Group>
</Box>
<Text fw="bold" fz="md">Sabtu - Minggu / Hari Libur</Text>
<Group gap={8}>
<IconClock size={18} />
<Text fw="bold" fz="md">{data.jam.holiday}</Text>
<Tooltip label="Hari aktif pelayanan puskesmas" position="top" withArrow>
<Badge size="sm" variant="light" color="blue">Aktif</Badge>
</Tooltip>
</Group>
</Stack>
</Stack>
</GridCol>

View File

@@ -27,11 +27,7 @@ function Page() {
if (loading || !data) {
return (
<Box py="xl" px={{ base: 'md', md: 100 }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} height={320} radius="lg" />
))}
</SimpleGrid>
<Skeleton height={500} radius="lg" />
</Box>
)
}

View File

@@ -0,0 +1,128 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Divider,
Paper,
Skeleton,
Stack,
Text,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
export default function DetailInformasiPublikUser() {
const state = useProxy(daftarInformasiPublik);
const router = useRouter();
const params = useParams();
useEffect(() => {
if (params?.id) state.findUnique.load(params.id as string);
}, [params?.id]);
const data = state.findUnique.data;
if (!state.findUnique.data) {
return (
<Center py="xl">
<Skeleton height={500} radius="md" />
</Center>
);
}
if (!data) {
return (
<Center py="xl">
<Stack align="center" gap="sm">
<Text fz="lg" fw="bold">
Informasi tidak ditemukan
</Text>
<Button variant="light" onClick={() => router.push('/informasi-publik')}>
Kembali ke Daftar
</Button>
</Stack>
</Center>
);
}
return (
<Box py="lg" px={{ base: 'md', md: 100 }} bg={colors.Bg}>
{/* Tombol Kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={18} color={colors['blue-button']} />}
mb="md"
c={colors['blue-button']}
>
Kembali
</Button>
<Paper
withBorder
radius="lg"
p={{ base: 'md', md: 'xl' }}
mx="auto"
maw={800}
bg="white"
shadow="xs"
>
<Stack gap="xl">
<Text
fz={{ base: 'xl', md: '2xl' }}
fw="bold"
ta="center"
c={colors['blue-button']}
>
Detail Informasi Publik
</Text>
<Divider />
<Stack gap="lg">
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Jenis Informasi
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
{data.jenisInformasi || '-'}
</Text>
</Box>
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Tanggal Publikasi
</Text>
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed">
{data.tanggal
? new Date(data.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
: '-'}
</Text>
</Box>
<Box>
<Text fz={{ base: 'md', md: 'lg' }} fw="bold" mb={4}>
Deskripsi
</Text>
<Box
className="prose max-w-none leading-relaxed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box>
</Stack>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -3,10 +3,13 @@
import daftarInformasiPublik from '@/app/admin/(dashboard)/_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
import colors from '@/con/colors';
import {
Badge,
Box,
Button,
Center,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
@@ -17,21 +20,20 @@ import {
TableTr,
Text,
TextInput,
Paper,
Badge,
Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSearch, IconFileInfo, IconMail, IconBrandWhatsapp } from '@tabler/icons-react';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconDeviceImacCog, IconFileInfo, IconMail, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useDebouncedValue } from '@mantine/hooks';
import { useTransitionRouter } from 'next-view-transitions';
function Page() {
const listData = useProxy(daftarInformasiPublik)
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useTransitionRouter()
const {
data,
page,
@@ -63,7 +65,7 @@ function Page() {
<BackButton />
</Box>
<Center>
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy"/>
<Image src="/darmasaba-icon.png" w={{ base: 70, md: 100 }} alt="Logo Desa Darmasaba" loading="lazy" />
</Center>
<Text ta="center" fz={{ base: "1.8rem", md: "2.5rem" }} c={colors["blue-button"]} fw="bold" lh={1.4}>
Daftar Informasi Publik Desa Darmasaba
@@ -99,38 +101,63 @@ function Page() {
</Stack>
</Center>
) : (
<Table withRowBorders withColumnBorders withTableBorder highlightOnHover verticalSpacing="md">
<TableThead bg={colors['blue-button']}>
<TableTr c={colors['white-1']}>
<TableTh fz="sm" ta="center" w="5%">No</TableTh>
<TableTh fz="sm" ta="center" w="25%">Jenis Informasi</TableTh>
<TableTh fz="sm" ta="center" w="40%">Deskripsi</TableTh>
<TableTh fz="sm" ta="center" w="20%">Tanggal Publikasi</TableTh>
</TableTr>
</TableThead>
<TableTbody bg={colors['white-1']}>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
<TableTd>
<Badge variant="light" size="lg" color="blue">
{item.jenisInformasi}
</Badge>
</TableTd>
<TableTd>
<Text fz="sm" c="dark" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd ta="center">
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</TableTd>
<Box style={{ overflowX: 'auto' }}>
<Table withRowBorders withColumnBorders withTableBorder highlightOnHover verticalSpacing="md">
<TableThead bg={colors['blue-button']}>
<TableTr c={colors['white-1']}>
<TableTh fz="sm" ta="center" w="5%">No</TableTh>
<TableTh fz="sm" ta="center" w="25%">Jenis Informasi</TableTh>
<TableTh fz="sm" ta="center" w="40%">Deskripsi</TableTh>
<TableTh fz="sm" ta="center" w="20%">Tanggal Publikasi</TableTh>
<TableTh fz="sm" ta="center" w="15%">Aksi</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody bg={colors['white-1']}>
{data.map((item, index) => (
<TableTr key={item.id}>
<TableTd ta="center">{(page - 1) * 5 + index + 1}</TableTd>
<TableTd>
<Box w={150}>
<Badge variant="light" size="lg" color="blue">
{item.jenisInformasi}
</Badge>
</Box>
</TableTd>
<TableTd>
<Box w={150}>
<Text lineClamp={1} fz="sm" c="dark" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd>
<TableTd ta="center">
<Box w={150}>
{item.tanggal ? new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
}) : '-'}
</Box>
</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Box w={150}>
<Tooltip label="Lihat Detail" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImacCog size={16} />}
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</Box>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
)}
<Center>

View File

@@ -4,7 +4,7 @@ import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/ind
import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts';
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import { useProxy } from "valtio/utils";
@@ -24,7 +24,8 @@ function Kepuasan() {
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const [opened, { open, close }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 768px)");
const resetForm = () => {
state.create.form = {
@@ -140,12 +141,12 @@ function Kepuasan() {
if ((loading && !data) || !data) {
return (
<Stack py={10} px="xl">
<Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={300} />
<Skeleton height={300} />
<Skeleton height={300} />
<Stack py={10} px="sm">
<Skeleton height={200} mb="md" />
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md">
<Skeleton height={200} />
<Skeleton height={200} />
<Skeleton height={200} />
</SimpleGrid>
</Stack>
);
@@ -412,50 +413,41 @@ function Kepuasan() {
);
}
return (
<Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Stack gap={"xs"}>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify={"center"}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={isMobile ? "md" : "xl"}>
<Stack gap="xs">
<Text ta="center" fz={{ base: "2rem", md: "3rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify="center">
<Button radius="lg" bg={colors["blue-button"]} onClick={open}>
Ajukan Responden
</Button>
</Group>
</Stack>
</Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box px={isMobile ? "sm" : "xl"}>
<Paper p="lg" bg={colors.Bg}>
<Paper p={isMobile ? "sm" : "lg"}>
<Stack gap="xs">
<Flex direction={isMobile ? "column" : "row"} justify="space-between" align={isMobile ? "start" : "center"}>
<Text fw="bold" mb={isMobile ? "sm" : 0}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
<Text fz="sm" fw="bold" c={colors["blue-button"]}>Total Responden</Text>
<Text ta="end" fz="h1" fw="bold" c={colors["blue-button"]}>
{state.findMany.total.toLocaleString("id-ID")}
</Text>
</Box>
</Flex>
<BarChart
h={300}
h={isMobile ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
series={[{ name: "count", color: colors["blue-button"] }]}
withTooltip
tooltipAnimationDuration={200}
/>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
<Box py="xl">
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg">
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
@@ -465,28 +457,17 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
data={donutDataJenisKelamin}
withTooltip
/>
</Center>
</Stack>
</Paper>
)}
</Stack>
@@ -501,35 +482,18 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
data={donutDataRating}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataRating}
/>
</Center>
</Stack>
</Paper>
)}
</Stack>
@@ -544,35 +508,18 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
data={donutDataKelompokUmur}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
<Paper p="md" radius="md">
<Stack>
<Center>
<PieChart
size={isMobile ? 150 : 200}
withLabels
labelsPosition="outside"
withLabelsLine
data={donutDataKelompokUmur}
/>
</Center>
</Stack>
</Paper>
)}
</Stack>

View File

@@ -1,47 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Container, Group, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
import { useRouter } from 'next/navigation';
import prestasiState from '@/app/admin/(dashboard)/_state/landing-page/prestasi-desa';
import colors from '@/con/colors';
import { BackgroundImage, Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useEffect, useState } from 'react';
import BackButton from '../../(pages)/desa/layanan/_com/BackButto';
import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
function Page() {
const state = useProxy(prestasiState.prestasiDesa);
const [loading, setLoading] = useState(true);
const [prestasiData, setPrestasiData] = useState<any[]>([]);
const router = useRouter();
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => {
load(page, 3, debouncedSearch)
}, [page, debouncedSearch])
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
)
}
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
await prestasiState.kategoriPrestasi.findMany.load();
await state.findMany.load();
setPrestasiData(state.findMany.data || []);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
loadData();
}, []);
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} >
<Box px={{ base: "md", md: 100 }}><BackButton /></Box>
<Container w={{ base: "100%", md: "50%" }}>
<Text ta={"center"} fz={"3.4rem"} c={colors["blue-button"]} fw={"bold"}>
Prestasi Desa
</Text>
<Text ta={"center"} py={10}>
Temukan berbagai prestasi dan keunggulan yang dimiliki Desa Darmasaba.
</Text>
</Container>
<Group justify='space-between' px={{ base: "md", md: 100 }}>
<Box>
<Text fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
Prestasi Desa
</Text>
<Text py={10}>
Temukan berbagai prestasi dan keunggulan yang dimiliki Desa Darmasaba.
</Text>
</Box>
<Box>
<TextInput
radius="xl"
placeholder="Cari prestasi atau kategori prestasi..."
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={300}
size="md"
/>
</Box>
</Group>
<SimpleGrid
px={{ base: "md", md: 100 }}
pb={20}
@@ -50,65 +61,77 @@ function Page() {
md: 3,
}}
>
{loading ? (
<Center>
<Text fz={"2.4rem"}>Memuat Data...</Text>
</Center>
{data.length === 0 ? (
<Text fz={"1.4rem"}>Tidak ada prestasi yang ditemukan</Text>
) : (
prestasiData.map((v, k) => {
data.map((v, k) => {
return (
<BackgroundImage
key={k}
src={v.image?.link}
radius={16}
pos={"relative"}
>
<Box
style={{
borderRadius: 16,
zIndex: 0
}}
pos={"absolute"}
w={"100%"}
h={"100%"}
bg={colors.trans.dark[2]}
/>
<Stack justify='space-between' h={"100%"} gap={0} p={"lg"} pos={"relative"}>
<Group>
<Paper radius={"lg"} py={7} px={10}>
<Text>{v.kategori?.name}</Text>
</Paper>
</Group>
<Box p={"lg"}>
<Text
fw={"bold"}
c={"white"}
size={"1.8rem"}
style={{
textAlign: "center",
lineHeight: 1.4,
minHeight: '5.4rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Box>
<Group justify="center">
<Button px={20} radius={"100"} size="md" bg={colors["blue-button"]}
onClick={() => router.push(`/darmasaba/prestasi-desa/${v.id}`)}>
Detail
</Button>
</Group>
</Stack>
</BackgroundImage>
)
})
)}
src={v.image?.link || ''}
radius={16}
pos={"relative"}
>
<Box
style={{
borderRadius: 16,
zIndex: 0
}}
pos={"absolute"}
w={"100%"}
h={"100%"}
bg={colors.trans.dark[2]}
/>
<Stack justify='space-between' h={"100%"} gap={0} p={"lg"} pos={"relative"}>
<Group>
<Paper radius={"lg"} py={7} px={10}>
<Text>{v.kategori?.name}</Text>
</Paper>
</Group>
<Box p={"lg"}>
<Text
fw={"bold"}
c={"white"}
size={"1.8rem"}
style={{
textAlign: "center",
lineHeight: 1.4,
minHeight: '5.4rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Box>
<Group justify="center">
<Button px={20} radius={"100"} size="md" bg={colors["blue-button"]}
onClick={() => router.push(`/darmasaba/prestasi-desa/${v.id}`)}>
Detail
</Button>
</Group>
</Stack>
</BackgroundImage>
)
})
)}
</SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Stack>
);
}

View File

@@ -32,9 +32,9 @@ function DesaAntiKorupsi() {
<Stack gap={"0"} bg={colors.Bg} p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"} >
<Center>
<Text fz={{ base: "2.4rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
<Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Desa Anti Korupsi</Text>
</Center>
<Text ta={"center"} fz={{ base: "1.2rem", md: "1.4rem" }}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola terbuka dengan melibatkan warga mengawasi anggaran, sehingga digunakan tepat sasaran sesuai kebutuhan.</Text>
<Center py={20}>
<Button radius={"lg"} fz={"h4"} bg={colors["blue-button"]} component={Link} href={"/darmasaba/desa-anti-korupsi/detail"}>Selengkapnya</Button>
</Center>
@@ -49,6 +49,7 @@ function DesaAntiKorupsi() {
cols={{ base: 1, sm: 2, md: 3 }}
spacing="lg"
mt="lg"
mb="xl"
>
{data.map((v, k) => (
<Paper

View File

@@ -160,11 +160,11 @@ function Kepuasan() {
</Center>
<Text fz={{ base: "1.2rem", md: "1.4rem" }} ta={"center"}>Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!</Text>
<Center mt={10}>
<Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
<Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
</Center>
</Container>
@@ -182,7 +182,7 @@ function Kepuasan() {
</Box>
</Flex>
<BarChart
h={300}
h={window.innerWidth < 480 ? 200 : 300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
@@ -196,12 +196,9 @@ function Kepuasan() {
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
cols={{ base: 1, sm: 2, lg: 3 }}
spacing="md"
verticalSpacing="md"
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
@@ -220,7 +217,7 @@ function Kepuasan() {
withLabels
withTooltip
labelsType="percent"
size={200}
size={250} // Fixed size in pixels
data={donutDataJenisKelamin}
/>
</Center>
@@ -259,7 +256,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
size={250}
data={donutDataRating}
/>
</Center>
@@ -302,7 +299,7 @@ function Kepuasan() {
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
size={250}
data={donutDataKelompokUmur}
/>
</Center>
@@ -419,7 +416,7 @@ function Kepuasan() {
}
return (
<Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Container size="lg" px="md">
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</Center>
@@ -432,9 +429,15 @@ function Kepuasan() {
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Flex
direction={{ base: "column", sm: "row" }}
justify="space-between"
align={{ base: "flex-start", sm: "center" }}
>
<Text fw="bold" ta={{ base: "center", sm: "left" }}>
Pelayanan Terhadap Publik Desa Darmasaba
</Text>
<Box mt={{ base: "sm", sm: 0 }}>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}

View File

@@ -107,9 +107,8 @@ function ModuleView() {
return (
<ScrollArea h={280} // ✅ tinggi fixed, bisa disesuaikan
scrollbarSize={8}
scrollbarSize={2}
offsetScrollbars
type="never"
styles={{
viewport: { paddingRight: 8 }, // kasih jarak biar scroll nggak dempet
}}

View File

@@ -6,11 +6,13 @@ import { IconAward, IconArrowRight } from "@tabler/icons-react";
import { useTransitionRouter } from 'next-view-transitions';
import { useEffect, useState } from "react";
import { useProxy } from "valtio/utils";
import { useMediaQuery } from "@mantine/hooks";
function Penghargaan() {
const router = useTransitionRouter();
const state = useProxy(penghargaanState);
const [loading, setLoading] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)');
useEffect(() => {
const loadData = async () => {
@@ -24,13 +26,12 @@ function Penghargaan() {
loadData();
}, []);
const data = state.findMany.data?.slice(0, 3);
// kalau mobile ambil 1 data aja, kalau desktop ambil 3
const data = state.findMany.data?.slice(0, isMobile ? 1 : 3);
return (
<Stack pos="relative" h={720}>
<Stack pos="relative" h="auto" mih={{ base: 500, md: 720 }}>
<video
width="320"
height="240"
loop
autoPlay
muted
@@ -38,11 +39,15 @@ function Penghargaan() {
width: "100%",
height: "100%",
objectFit: "cover",
overflow: "hidden",
position: "absolute",
top: 0,
left: 0,
zIndex: 0,
}}
>
<source src="/assets/videos/award.mp4" type="video/mp4" />
</video>
<Box
style={{
width: "100%",
@@ -51,9 +56,10 @@ function Penghargaan() {
top: 0,
left: 0,
background: "linear-gradient(to bottom, rgba(0,0,0,0.6), rgba(0,0,0,0.85))",
zIndex: 1,
}}
>
<Container w={{ base: "100%", md: "80%" }} h={720} p="xl">
<Container w={{ base: "100%", md: "80%" }} mih={{ base: 500, md: 720 }} p="xl">
<Stack justify="center" align="center" gap="xl" h="100%">
<Text
fw={900}

View File

@@ -100,11 +100,11 @@ function Potensi() {
style={{ zIndex: 1 }}
>
<Tooltip label={v.name} position="top-start">
<Text fw={700} c="white" size="2.2rem" truncate>
<Text fw={700} c="white" fz={{ base: "1.2rem", md: "1.4rem" }} truncate>
{v.name}
</Text>
</Tooltip>
<Text lineClamp={2} c="gray.2" size="sm">
<Text lineClamp={2} c="gray.2" fz={{ base: "0.8rem", md: "1rem" }}>
{v.deskripsi}
</Text>
</Stack>

View File

@@ -144,9 +144,9 @@ export default function SDGS() {
mt={40}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)" }}
style={{ boxShadow: "0 6px 14px rgba(18,65,112,0.25)"}}
>
Jelajahi Semua Tujuan SDGs Desa
<Text c="white" fz={{ base: "md", md: "lg" }} fw="bold">Jelajahi Semua Tujuan SDGs Desa</Text>
</Button>
</Center>
</Box>