QC User & Admin Responsive : Menu Kesehatan - Ekonomi

This commit is contained in:
2025-10-03 10:17:06 +08:00
parent 8a6d8ed8db
commit f7fd9be255
55 changed files with 754 additions and 372 deletions

View File

@@ -1,6 +1,6 @@
[ [
{ {
"id": "1", "id": "edit",
"name": "Pelayanan Penduduk Non-Permanent", "name": "Pelayanan Penduduk Non-Permanent",
"deskripsi": "<p>Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.</p>" "deskripsi": "<p>Surat Keterangan Penduduk Non-Permanent adalah dokumen yang dikeluarkan oleh pihak berwenang untuk memberikan keterangan bahwa seseorang atau kelompok orang memiliki status penduduk non-permanent di suatu wilayah. Dokumen ini biasanya digunakan untuk keperluan administratif atau legal, seperti mendapatkan akses ke layanan kesehatan, pendidikan, atau pelayanan publik lainnya.</p>"
} }

View File

@@ -1,6 +1,6 @@
[ [
{ {
"id": "1", "id": "edit",
"name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)", "name": "Pelayanan Perizinan Berusaha Berbasis Risiko Melalui Sistem ONLINE SINGLE SUBMISSION (OSS)",
"deskripsi": "<p>Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.</p>", "deskripsi": "<p>Penyelenggaraan Perizinan Berusaha Berbasis Risiko melalui Sistem Online Single Submission (OSS) merupakan pelaksanaan Undang-Undang Nomor 11 Tahun 2020 Tentang Cipta Kerja. OSS Berbasis Risiko wajib digunakan oleh Pelaku Usaha, Kementerian/Lembaga, Pemerintah Daerah, Administrator Kawasan Ekonomi Khusus (KEK), dan Badan Pengusahaan Kawasan Perdagangan Bebas Pelabuhan Bebas (KPBPB).Berdasarkan Peraturan Pemerintah Nomor 5 Tahun 2021 terdapat 1.702 kegiatan usaha yang terdiri atas 1.349 Klasifikasi Baku Lapangan Usaha Indonesia (KBLI) yang sudah diimplementasikan dalam Sistem OSS Berbasis Risiko.</p>",
"link" : "https://oss.go.id/" "link" : "https://oss.go.id/"

View File

@@ -1167,6 +1167,7 @@ model KontakDarurat {
deskripsi String deskripsi String
image FileStorage @relation(fields: [imageId], references: [id]) image FileStorage @relation(fields: [imageId], references: [id])
imageId String imageId String
whatsapp String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
@@ -1340,6 +1341,7 @@ model PasarDesa {
harga Int harga Int
rating Float rating Float
alamatUsaha String alamatUsaha String
kontak String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())

View File

@@ -581,33 +581,24 @@ const pelayananPerizinanBerusaha = proxy({
findById: { findById: {
data: null as pelayananPerizinanBerusahaForm | null, data: null as pelayananPerizinanBerusahaForm | null,
loading: false, loading: false,
initialize() {
pelayananPerizinanBerusaha.findById.data = {
id: "",
name: "",
deskripsi: "",
link: "",
} as pelayananPerizinanBerusahaForm;
},
async load(id: string) { async load(id: string) {
try { try {
pelayananPerizinanBerusaha.findById.loading = true; this.loading = true;
const res = await fetch( const response = await fetch(`/api/desa/layanan/pelayananperizinanberusaha/${id}`);
`/api/desa/layanan/pelayananperizinanberusaha/${id}` if (!response.ok) {
); throw new Error(`HTTP error! status: ${response.status}`);
if (res.ok) {
const data = await res.json();
pelayananPerizinanBerusaha.findById.data = data.data ?? null;
} else {
console.error(
"Failed to fetch pelayanan perizinan berusaha:",
res.statusText
);
pelayananPerizinanBerusaha.findById.data = null;
} }
const result = await response.json();
if (result?.success) {
this.data = result.data; // Make sure this matches your API response structure
}
return result?.data || null;
} catch (error) { } catch (error) {
console.error("Error fetching pelayanan perizinan berusaha:", error); console.error('Error loading data:', error);
pelayananPerizinanBerusaha.findById.data = null; toast.error('Gagal memuat data');
return null;
} finally {
this.loading = false;
} }
}, },
}, },

View File

@@ -12,6 +12,7 @@ const templatePasarDesaForm = z.object({
imageId: z.string().min(1, "Gambar wajib dipilih"), imageId: z.string().min(1, "Gambar wajib dipilih"),
rating: z.number().min(1, "Rating minimal 1"), rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"), kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
kontak: z.string().min(1, "Kontak wajib diisi"),
}); });
const defaultPasarDesaForm = { const defaultPasarDesaForm = {
@@ -21,6 +22,7 @@ const defaultPasarDesaForm = {
imageId: "", imageId: "",
rating: 0, rating: 0,
kategoriId: [] as string[], kategoriId: [] as string[],
kontak: "",
}; };
const pasarDesa = proxy({ const pasarDesa = proxy({
@@ -188,6 +190,7 @@ const pasarDesa = proxy({
imageId: data.imageId, imageId: data.imageId,
rating: data.rating, rating: data.rating,
kategoriId: data.kategoriId, kategoriId: data.kategoriId,
kontak: data.kontak,
}; };
return data; return data;
} else { } else {
@@ -225,6 +228,7 @@ const pasarDesa = proxy({
imageId: this.form.imageId, imageId: this.form.imageId,
rating: this.form.rating, rating: this.form.rating,
kategoriId: this.form.kategoriId, kategoriId: this.form.kategoriId,
kontak: this.form.kontak,
}), }),
}); });
if (!response.ok) { if (!response.ok) {
@@ -336,6 +340,40 @@ const kategoriProduk = proxy({
} }
}, },
}, },
// ✅ Versi findManyAll (ambil semua tanpa pagination)
findManyAll: {
data: null as
| Prisma.KategoriProdukGetPayload<{
omit: { isActive: true };
}>[]
| null,
loading: false,
search: "",
load: async (search = "") => {
kategoriProduk.findManyAll.loading = true;
kategoriProduk.findManyAll.search = search;
try {
const query: any = {};
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.kategoriproduk["find-many-all"].get({
query,
});
if (res.status === 200 && res.data?.success) {
kategoriProduk.findManyAll.data = res.data.data ?? [];
} else {
kategoriProduk.findManyAll.data = [];
}
} catch (err) {
console.error("Gagal fetch kategori produk (all):", err);
kategoriProduk.findManyAll.data = [];
} finally {
kategoriProduk.findManyAll.loading = false;
}
},
},
findUnique: { findUnique: {
data: null as Prisma.KategoriProdukGetPayload<{ data: null as Prisma.KategoriProdukGetPayload<{
omit: { isActive: true }; omit: { isActive: true };

View File

@@ -9,12 +9,14 @@ const templateForm = z.object({
name: z.string().min(3, "Judul minimal 3 karakter"), name: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"), deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
imageId: z.string().nonempty(), imageId: z.string().nonempty(),
whatsapp: z.string().min(10, "Whatsapp minimal 10 karakter"),
}); });
const defaultForm = { const defaultForm = {
name: "", name: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
whatsapp: "",
}; };
const kontakDarurat = proxy({ const kontakDarurat = proxy({
@@ -171,6 +173,7 @@ const kontakDarurat = proxy({
name: data.name, name: data.name,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
imageId: data.imageId, imageId: data.imageId,
whatsapp: data.whatsapp,
}; };
return data; // Return the loaded data return data; // Return the loaded data
} else { } else {
@@ -207,6 +210,7 @@ const kontakDarurat = proxy({
name: this.form.name, name: this.form.name,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
imageId: this.form.imageId, imageId: this.form.imageId,
whatsapp: this.form.whatsapp,
}), }),
} }
); );

View File

@@ -59,7 +59,7 @@ function PelayananPendudukNonPermanent() {
radius="md" radius="md"
onClick={() => onClick={() =>
router.push( router.push(
'/admin/desa/layanan/pelayanan_penduduk_non_permanent/edit' `/admin/desa/layanan/pelayanan_penduduk_non_permanent/${data.id}`
) )
} }
> >

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa'; import stateLayananDesa from '@/app/admin/(dashboard)/_state/desa/layananDesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
@@ -8,6 +9,7 @@ import {
Button, Button,
Group, Group,
Paper, Paper,
Skeleton,
Stack, Stack,
TextInput, TextInput,
Title, Title,
@@ -21,66 +23,82 @@ import { useProxy } from 'valtio/utils';
function EditPelayananPerizinanBerusaha() { function EditPelayananPerizinanBerusaha() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams<{ id: string }>();
const statePerizinanBerusaha = useProxy( const id = params?.id; // ini langsung string
stateLayananDesa.pelayananPerizinanBerusaha const state = useProxy(stateLayananDesa.pelayananPerizinanBerusaha);
);
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
id: '',
name: '', name: '',
deskripsi: '', deskripsi: '',
link: '', link: '',
}); });
// load data pertama kali // Load data detail
useEffect(() => { useEffect(() => {
const loadPelayananPerizinan = async () => { if (!id) {
const id = params?.id as string; toast.error("ID tidak valid");
if (!id) return; return;
}
const loadData = async () => {
try { try {
const data = await statePerizinanBerusaha.update.load(id); setLoading(true);
const data = await state.findById.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', id: data.id,
deskripsi: data.deskripsi || '', name: data.name || "",
link: data.link || '', deskripsi: data.deskripsi || "",
link: data.link || "",
}); });
} else {
toast.error("Data tidak ditemukan");
} }
} catch (error) { } catch (error) {
console.error('Error loading pelayanan perizinan berusaha:', error); console.error("Error loading data:", error);
toast.error('Gagal memuat data pelayanan perizinan berusaha'); toast.error("Gagal memuat data");
} finally {
setLoading(false);
} }
}; };
loadPelayananPerizinan();
}, [params?.id]); loadData();
}, [id]);
const handleChange = const handleChange =
(field: keyof typeof formData) => (field: keyof typeof formData) =>
(value: string) => { (value: string) => {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[field]: value, [field]: value,
})); }));
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const { name, deskripsi, link } = formData; try {
if (statePerizinanBerusaha.findById.data) { await state.update.update(formData);
const updatedData = {
...statePerizinanBerusaha.findById.data,
name,
deskripsi,
link,
};
await statePerizinanBerusaha.update.update(updatedData);
router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha'); router.push('/admin/desa/layanan/pelayanan_perizinan_berusaha');
} catch (error) {
console.error('Error updating pelayanan perizinan berusaha:', error);
toast.error('Terjadi kesalahan saat update data');
} }
}; };
if (loading) {
return (
<Stack align="center" justify="center" py="xl">
<Skeleton height={800} radius="md" />
</Stack>
);
}
return ( return (
<Box> <Box>
<Stack gap="xs"> <Stack gap="xs">
{/* Header Section */} {/* Header */}
<Group mb="md"> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button <Button
@@ -97,7 +115,7 @@ function EditPelayananPerizinanBerusaha() {
</Title> </Title>
</Group> </Group>
{/* Form Section */} {/* Form */}
<Paper <Paper
w={{ base: '100%', md: '50%' }} w={{ base: '100%', md: '50%' }}
bg={colors['white-1']} bg={colors['white-1']}
@@ -109,7 +127,6 @@ function EditPelayananPerizinanBerusaha() {
<Stack gap="xs"> <Stack gap="xs">
<Title order={3}>Edit Pelayanan Perizinan Berusaha</Title> <Title order={3}>Edit Pelayanan Perizinan Berusaha</Title>
{/* Nama Field */}
<TextInput <TextInput
label="Judul" label="Judul"
placeholder="Masukkan judul" placeholder="Masukkan judul"
@@ -118,7 +135,6 @@ function EditPelayananPerizinanBerusaha() {
required required
/> />
{/* Link Field */}
<TextInput <TextInput
label="Link" label="Link"
placeholder="Masukkan link terkait" placeholder="Masukkan link terkait"
@@ -126,7 +142,6 @@ function EditPelayananPerizinanBerusaha() {
onChange={(e) => handleChange('link')(e.target.value)} onChange={(e) => handleChange('link')(e.target.value)}
/> />
{/* Deskripsi Field */}
<Box> <Box>
<Title order={6}>Deskripsi</Title> <Title order={6}>Deskripsi</Title>
<EditEditor <EditEditor
@@ -135,23 +150,20 @@ function EditPelayananPerizinanBerusaha() {
/> />
</Box> </Box>
{/* Action Buttons */}
<Group> <Group>
<Button <Button
bg={colors['blue-button']} bg={colors['blue-button']}
onClick={handleSubmit} onClick={handleSubmit}
loading={statePerizinanBerusaha.update.loading} loading={state.update.loading}
disabled={!formData.name} disabled={!formData.name}
> >
{statePerizinanBerusaha.update.loading {state.update.loading ? 'Menyimpan...' : 'Simpan Perubahan'}
? 'Menyimpan...'
: 'Simpan Perubahan'}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => router.back()} onClick={() => router.back()}
disabled={statePerizinanBerusaha.update.loading} disabled={state.update.loading}
> >
Batal Batal
</Button> </Button>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
@@ -19,35 +20,58 @@ import {
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconEdit } from '@tabler/icons-react'; import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react';
import { useState } from 'react';
import stateLayananDesa from '../../../_state/desa/layananDesa'; import stateLayananDesa from '../../../_state/desa/layananDesa';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useRouter } from 'next/navigation';
function PerizinanBerusaha() { function PerizinanBerusaha() {
const router = useRouter(); const router = useRouter();
const pelayananPerizinanBerusaha = useProxy( const pelayananPerizinanBerusaha = useProxy(
stateLayananDesa.pelayananPerizinanBerusaha stateLayananDesa.pelayananPerizinanBerusaha
); );
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [active, setActive] = useState(1); const [active, setActive] = useState(1);
const nextStep = () => const nextStep = () =>
setActive((current) => (current < 6 ? current + 1 : current)); setActive((current) => (current < 6 ? current + 1 : current));
const prevStep = () => const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current)); setActive((current) => (current > 0 ? current - 1 : current));
useShallowEffect(() => { useEffect(() => {
pelayananPerizinanBerusaha.findById.load('1'); const loadData = async () => {
try {
setLoading(true);
// You should get the ID from your router query or params
const id = '1'; // Replace with actual ID or get from URL params
await pelayananPerizinanBerusaha.findById.load(id);
} catch (err) {
setError('Gagal memuat data');
console.error('Error:', err);
} finally {
setLoading(false);
}
};
loadData();
}, []); }, []);
if (!pelayananPerizinanBerusaha.findById.data) { if (loading) {
return ( return (
<Stack align="center" justify="center" py="xl"> <Stack align="center" justify="center" py="xl">
<Skeleton radius="md" height={800} /> <Skeleton height={800} radius="md" />
</Stack> </Stack>
); );
} }
if (error || !pelayananPerizinanBerusaha.findById.data) {
return (
<Center h={200}>
<Text>{error || 'Data tidak ditemukan'}</Text>
</Center>
);
}
const data = pelayananPerizinanBerusaha.findById.data; const data = pelayananPerizinanBerusaha.findById.data;
return ( return (
@@ -69,7 +93,7 @@ function PerizinanBerusaha() {
radius="md" radius="md"
onClick={() => onClick={() =>
router.push( router.push(
'/admin/desa/layanan/pelayanan_perizinan_berusaha/edit' `/admin/desa/layanan/pelayanan_perizinan_berusaha/${data.id}`
) )
} }
> >
@@ -183,3 +207,4 @@ function PerizinanBerusaha() {
} }
export default PerizinanBerusaha; export default PerizinanBerusaha;

View File

@@ -44,39 +44,37 @@ function EditSuratKeterangan() {
const [previewImage2, setPreviewImage2] = useState<string | null>(null); const [previewImage2, setPreviewImage2] = useState<string | null>(null);
// load data awal // load data awal
useEffect(() => { useEffect(() => {
const loadSurat = async () => { const loadSurat = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try {
const data = await stateSurat.edit.load(id);
if (!data) return;
try {
const data = await stateSurat.edit.load(id);
if (data) {
// merge style -> isi hanya field kosong
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
name: prev.name || data.name || '', ...{
deskripsi: prev.deskripsi || data.deskripsi || '', name: prev.name || data.name || "",
imageId: prev.imageId || data.imageId || '', deskripsi: prev.deskripsi || data.deskripsi || "",
image2Id: prev.image2Id || data.image2Id || '', imageId: prev.imageId || data.imageId || "",
image2Id: prev.image2Id || data.image2Id || "",
},
})); }));
if (data.image?.link && !previewImage) { if (data.image?.link && !previewImage) setPreviewImage(data.image.link);
setPreviewImage(data.image.link); if (data.image2?.link && !previewImage2) setPreviewImage2(data.image2.link);
} } catch (error) {
if (data.image2?.link && !previewImage2) { console.error("Error loading surat:", error);
setPreviewImage2(data.image2.link); toast.error("Gagal memuat data surat");
}
} }
} catch (error) { };
console.error("Error loading surat:", error);
toast.error("Gagal memuat data surat"); loadSurat();
} // eslint-disable-next-line react-hooks/exhaustive-deps
}; }, [params?.id]);
loadSurat();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params?.id]);
// handler untuk submit // handler untuk submit

View File

@@ -8,17 +8,15 @@ import {
Group, Group,
Paper, Paper,
Stack, Stack,
Text,
TextInput, TextInput,
Title, Title,
Tooltip, Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState, useCallback } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditPelayananTelunjukSakti() { function EditPelayananTelunjukSakti() {
const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa); const stateTelunjukDesa = useProxy(stateLayananDesa.pelayananTelunjukSaktiDesa);
@@ -111,21 +109,19 @@ function EditPelayananTelunjukSakti() {
required required
/> />
{/* Deskripsi pakai editor */} {/* Deskripsi */}
<Box> <TextInput
<Text fz="sm" fw="bold" mb={6}> value={formData.deskripsi}
Deskripsi onChange={(e) => handleChange('deskripsi', e.target.value)}
</Text> label="Judul Link"
<EditEditor placeholder="Masukkan judul link"
value={formData.deskripsi} required
onChange={(htmlContent) => handleChange('deskripsi', htmlContent)} />
/>
</Box>
{/* Link */} {/* Link */}
<TextInput <TextInput
label="Link" label="Link"
placeholder="Masukkan link terkait" placeholder="Masukkan alamat link"
value={formData.link} value={formData.link}
onChange={(e) => handleChange('link', e.target.value)} onChange={(e) => handleChange('link', e.target.value)}
/> />

View File

@@ -82,8 +82,8 @@ function CreatePelayananTelunjukDesa() {
onChange={(val) => { onChange={(val) => {
stateTelunjukDesa.create.form.deskripsi = val.target.value; stateTelunjukDesa.create.form.deskripsi = val.target.value;
}} }}
label="Deskripsi" label="Judul Link"
placeholder="Masukkan deskripsi pelayanan" placeholder="Masukkan judul link"
required required
/> />

View File

@@ -31,6 +31,7 @@ type FormData = {
imageId: string; imageId: string;
rating: number; rating: number;
kategoriId: string[]; kategoriId: string[];
kontak: string;
}; };
function EditPasarDesa() { function EditPasarDesa() {
@@ -47,6 +48,7 @@ function EditPasarDesa() {
imageId: '', imageId: '',
rating: 0, rating: 0,
kategoriId: [], kategoriId: [],
kontak: '',
}); });
// load data awal // load data awal
@@ -67,6 +69,7 @@ function EditPasarDesa() {
imageId: data.imageId || '', imageId: data.imageId || '',
rating: data.rating || 0, rating: data.rating || 0,
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [], kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: data.kontak || '',
}); });
if (data.image?.link) setPreviewImage(data.image.link); if (data.image?.link) setPreviewImage(data.image.link);
} }
@@ -228,6 +231,14 @@ function EditPasarDesa() {
required required
/> />
<TextInput
label="Kontak"
placeholder="Masukkan kontak"
value={formData.kontak}
onChange={(e) => handleChange('kontak', e.target.value)}
required
/>
<MultiSelect <MultiSelect
label="Kategori Produk" label="Kategori Produk"
placeholder="Pilih kategori produk" placeholder="Pilih kategori produk"

View File

@@ -85,6 +85,11 @@ function DetailPasarDesa() {
<Text fz="md" c="dimmed">{data.alamatUsaha || '-'}</Text> <Text fz="md" c="dimmed">{data.alamatUsaha || '-'}</Text>
</Box> </Box>
<Box>
<Text fz="lg" fw="bold">Kontak</Text>
<Text fz="md" c="dimmed">{data.kontak || '-'}</Text>
</Box>
<Box> <Box>
<Text fz="lg" fw="bold">Gambar</Text> <Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? ( {data.image?.link ? (

View File

@@ -41,6 +41,7 @@ export default function CreatePasarDesa() {
imageId: '', imageId: '',
rating: 0, rating: 0,
kategoriId: [], kategoriId: [],
kontak: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
@@ -184,6 +185,15 @@ export default function CreatePasarDesa() {
onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)} onChange={(e) => (statePasar.pasarDesa.create.form.alamatUsaha = e.target.value)}
/> />
{/* Kontak */}
<TextInput
label="Kontak"
type="number"
placeholder="Masukkan kontak"
defaultValue={statePasar.pasarDesa.create.form.kontak}
onChange={(e) => (statePasar.pasarDesa.create.form.kontak = e.target.value)}
/>
{/* Kategori Produk */} {/* Kategori Produk */}
<MultiSelect <MultiSelect
label="Kategori Produk" label="Kategori Produk"

View File

@@ -29,10 +29,11 @@ function EditKontakDaruratKeamanan() {
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Remove the dependency on data in the initial state
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
icon: "" as IconKey | "", icon: "" as IconKey | "",
kategoriId: [] as string[], kategoriId: [] as string[], // Initialize as empty array
}); });
// Load data dari backend // Load data dari backend
@@ -49,7 +50,7 @@ function EditKontakDaruratKeamanan() {
setFormData({ setFormData({
name: data.nama || "", name: data.nama || "",
icon: (data.icon as IconKey) || "", icon: (data.icon as IconKey) || "",
kategoriId: data.kategoriId || [], kategoriId: Array.isArray(data.kategoriId) ? data.kategoriId : [],
}); });
} }
} }
@@ -134,9 +135,9 @@ function EditKontakDaruratKeamanan() {
data={ data={
Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data) Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data)
? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({ ? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({
value: v.id, value: v.id,
label: v.nama, label: v.nama,
})) }))
: [] : []
} }
clearable clearable

View File

@@ -52,17 +52,18 @@ function EditLaporanPublik() {
try { try {
const data = await stateLaporan.edit.load(id); const data = await stateLaporan.edit.load(id);
if (data) { if (data) {
setFormData({ setFormData((prev) => ({
judul: data.judul || '', ...prev,
lokasi: data.lokasi || '', judul: data.judul ?? prev.judul,
tanggalWaktu: data.tanggalWaktu || '', lokasi: data.lokasi ?? prev.lokasi,
status: data.status || '', tanggalWaktu: data.tanggalWaktu ?? prev.tanggalWaktu,
penanganan: data.penanganan?.[0]?.deskripsi || '', status: (data.status as Status) ?? prev.status,
kronologi: data.kronologi || '', penanganan: data.penanganan?.[0]?.deskripsi ?? prev.penanganan,
}); kronologi: data.kronologi ?? prev.kronologi,
}));
} }
} catch (error) { } catch (error) {
console.error('Error loading laporan publik:', error); console.error("Error loading laporan publik:", error);
toast.error("Gagal mengambil data laporan publik"); toast.error("Gagal mengambil data laporan publik");
} }
}; };
@@ -70,6 +71,7 @@ function EditLaporanPublik() {
loadLaporanPublik(); loadLaporanPublik();
}, [params?.id, stateLaporan.edit]); }, [params?.id, stateLaporan.edit]);
const handleChange = (field: string, value: string | Status) => { const handleChange = (field: string, value: string | Status) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };

View File

@@ -10,6 +10,7 @@ import {
Group, Group,
Paper, Paper,
Stack, Stack,
Text,
TextInput, TextInput,
Title, Title,
Tooltip, Tooltip,
@@ -61,9 +62,9 @@ function EditPencegahanKriminalitas() {
const handleChange = const handleChange =
(field: keyof typeof formData) => (field: keyof typeof formData) =>
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({ ...prev, [field]: e.target.value })); setFormData((prev) => ({ ...prev, [field]: e.target.value }));
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(formData.linkVideo); const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
@@ -128,13 +129,17 @@ function EditPencegahanKriminalitas() {
required required
/> />
<TextInput <Box>
label="Deskripsi Singkat" <Text fw="bold" fz="sm" mb={6}>
placeholder="Masukkan deskripsi singkat" Deskripsi
value={formData.deskripsiSingkat} </Text>
onChange={handleChange('deskripsiSingkat')} <EditEditor
required value={formData.deskripsiSingkat}
/> onChange={(val) =>
setFormData((prev) => ({ ...prev, deskripsiSingkat: val }))
}
/>
</Box>
<Box> <Box>
<Title order={6} fw="bold" fz="sm" mb={6}> <Title order={6} fw="bold" fz="sm" mb={6}>

View File

@@ -90,15 +90,17 @@ function CreatePencegahanKriminalitas() {
/> />
{/* Deskripsi Singkat */} {/* Deskripsi Singkat */}
<TextInput <Box>
label="Deskripsi Singkat" <Text fw="bold" fz="sm" mb={6}>
placeholder="Masukkan deskripsi singkat" Deskripsi Singkat
defaultValue={kriminalitasState.create.form.deskripsiSingkat} </Text>
onChange={(e) => { <CreateEditor
kriminalitasState.create.form.deskripsiSingkat = e.currentTarget.value; value={kriminalitasState.create.form.deskripsiSingkat}
}} onChange={(val) => {
required kriminalitasState.create.form.deskripsiSingkat = val;
/> }}
/>
</Box>
{/* Deskripsi Panjang */} {/* Deskripsi Panjang */}
<Box> <Box>

View File

@@ -105,9 +105,11 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
data.map((item) => ( data.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}> <Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.judul} {item.judul}
</Text> </Text>
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Box w={200}>

View File

@@ -127,7 +127,9 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={150}> <Box w={150}>
{item.tarifdanlayanan?.layanan || '-'} <Text truncate="end" lineClamp={1}>
{item.tarifdanlayanan?.layanan || '-'}
</Text>
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>

View File

@@ -140,15 +140,15 @@ function EditInfoWabahPenyakit() {
required required
/> />
<TextInput <Box>
value={formData.deskripsiSingkat} <Text fz="sm" fw="bold">
onChange={(e: ChangeEvent<HTMLInputElement>) => Deskripsi Singkat
updateField('deskripsiSingkat', e.target.value) </Text>
} <EditEditor
label="Deskripsi Singkat" value={formData.deskripsiSingkat}
placeholder="Masukkan deskripsi singkat" onChange={(val) => updateField('deskripsiSingkat', val)}
required />
/> </Box>
<Box> <Box>
<Text fz="sm" fw="bold"> <Text fz="sm" fw="bold">

View File

@@ -84,7 +84,7 @@ function DetailInfoWabahPenyakit() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text> <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data.deskripsiSingkat || '-'}</Text> <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }} />
</Box> </Box>
<Box> <Box>

View File

@@ -100,15 +100,15 @@ function CreateInfoWabahPenyakit() {
required required
/> />
<TextInput <Box>
defaultValue={infoWabahPenyakitState.create.form.deskripsiSingkat} <Text fz="sm" fw="bold">Deskripsi Singkat</Text>
onChange={(val) => { <CreateEditor
infoWabahPenyakitState.create.form.deskripsiSingkat = val.target.value; value={infoWabahPenyakitState.create.form.deskripsiSingkat}
}} onChange={(val) => {
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>} infoWabahPenyakitState.create.form.deskripsiSingkat = val;
placeholder="Masukkan deskripsi singkat" }}
required />
/> </Box>
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text> <Text fz="sm" fw="bold">Deskripsi</Text>

View File

@@ -33,6 +33,7 @@ function EditKontakDarurat() {
name: '', name: '',
deskripsi: '', deskripsi: '',
imageId: '', imageId: '',
whatsapp: '',
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -49,6 +50,7 @@ function EditKontakDarurat() {
name: data.name || '', name: data.name || '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
imageId: data.imageId || '', imageId: data.imageId || '',
whatsapp: data.whatsapp || '',
}); });
if (data?.image?.link) setPreviewImage(data.image.link); if (data?.image?.link) setPreviewImage(data.image.link);
} }
@@ -124,6 +126,14 @@ function EditKontakDarurat() {
required required
/> />
<TextInput
value={formData.whatsapp}
onChange={(e) => setFormData(prev => ({ ...prev, whatsapp: e.target.value }))}
label="Whatsapp"
placeholder="Masukkan whatsapp"
required
/>
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text> <Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor <EditEditor

View File

@@ -72,6 +72,11 @@ function DetailKontakDarurat() {
<Text fz="md" c="dimmed">{data.name || '-'}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box>
<Text fz="lg" fw="bold">Whatsapp</Text>
<Text fz="md" c="dimmed">{data.whatsapp || '-'}</Text>
</Box>
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text <Text

View File

@@ -38,6 +38,7 @@ function CreateKontakDarurat() {
name: '', name: '',
deskripsi: '', deskripsi: '',
imageId: '', imageId: '',
whatsapp: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
@@ -105,6 +106,17 @@ function CreateKontakDarurat() {
required required
/> />
<TextInput
type='number'
defaultValue={kontakDaruratState.create.form.whatsapp}
onChange={(val) => {
kontakDaruratState.create.form.whatsapp = val.target.value;
}}
label={<Text fz="sm" fw="bold">Whatsapp</Text>}
placeholder="Masukkan whatsapp"
required
/>
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text> <Text fz="sm" fw="bold">Deskripsi</Text>
<CreateEditor <CreateEditor

View File

@@ -117,7 +117,6 @@ function EditProgramKesehatan() {
<Stack gap="md"> <Stack gap="md">
{[ {[
{ label: 'Judul', key: 'name', placeholder: 'Masukkan judul' }, { label: 'Judul', key: 'name', placeholder: 'Masukkan judul' },
{ label: 'Deskripsi Singkat', key: 'deskripsiSingkat', placeholder: 'Masukkan deskripsi singkat' },
].map((field) => ( ].map((field) => (
<TextInput <TextInput
key={field.key} key={field.key}
@@ -129,6 +128,16 @@ function EditProgramKesehatan() {
/> />
))} ))}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi Singkat
</Text>
<EditEditor
value={formData.deskripsiSingkat}
onChange={(val) => handleChange('deskripsiSingkat', val)}
/>
</Box>
<Box> <Box>
<Text fz="sm" fw="bold" mb={6}> <Text fz="sm" fw="bold" mb={6}>
Deskripsi Deskripsi

View File

@@ -73,7 +73,7 @@ function DetailProgramKesehatan() {
<Box> <Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text> <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data?.deskripsiSingkat || '-'}</Text> <Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: data?.deskripsiSingkat || '-' }} />
</Box> </Box>
<Box> <Box>

View File

@@ -101,15 +101,17 @@ function CreateProgramKesehatan() {
required required
/> />
<TextInput <Box>
defaultValue={programKesehatanState.create.form.deskripsiSingkat} <Title order={6} mb={6}>
onChange={(val) => { Deskripsi Singkat
programKesehatanState.create.form.deskripsiSingkat = val.target.value; </Title>
}} <CreateEditor
label="Deskripsi Singkat" value={programKesehatanState.create.form.deskripsiSingkat}
placeholder="Masukkan deskripsi singkat" onChange={(val) => {
required programKesehatanState.create.form.deskripsiSingkat = val;
/> }}
/>
</Box>
<Box> <Box>
<Title order={6} mb={6}> <Title order={6} mb={6}>

View File

@@ -7,7 +7,9 @@ type FormCreate = {
alamatUsaha: string; alamatUsaha: string;
imageId: string; imageId: string;
rating: number; rating: number;
kategoriId: string[]; // Array of KategoriProduk IDs kategoriId: string[];
kontak: string;
// Array of KategoriProduk IDs
}; };
export default async function pasarDesaCreate(context: Context) { export default async function pasarDesaCreate(context: Context) {
@@ -28,7 +30,9 @@ export default async function pasarDesaCreate(context: Context) {
alamatUsaha: body.alamatUsaha, alamatUsaha: body.alamatUsaha,
imageId: body.imageId, imageId: body.imageId,
rating: Number(body.rating), rating: Number(body.rating),
kategoriProdukId: body.kategoriId[0], // Use the first category as the main one kategoriProdukId: body.kategoriId[0],
kontak: body.kontak
// Use the first category as the main one
}, },
}); });

View File

@@ -37,6 +37,7 @@ const PasarDesa = new Elysia({
imageId: t.String(), imageId: t.String(),
rating: t.Number(), rating: t.Number(),
kategoriId: t.Array(t.String()), kategoriId: t.Array(t.String()),
kontak: t.String(),
}), }),
} }
) )
@@ -79,6 +80,7 @@ const PasarDesa = new Elysia({
imageId: t.String(), imageId: t.String(),
rating: t.Number(), rating: t.Number(),
kategoriId: t.Array(t.String()), kategoriId: t.Array(t.String()),
kontak: t.String(),
}), }),
} }
); );

View File

@@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyAll.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function kategoriProdukFindManyAll(context: Context) {
// Ambil query search (opsional)
const search2 = (context.query.search as string) || "";
// Buat where clause
const where: any = { isActive: true };
if (search2) {
where.OR = [
{ nama: { contains: search2, mode: "insensitive" } },
{
KategoriToPasar: {
some: {
kategori: {
nama: { contains: search2, mode: "insensitive" },
},
},
},
},
];
}
try {
const data = await prisma.kategoriProduk.findMany({
where,
orderBy: { createdAt: "desc" },
});
return {
success: true,
message: "Berhasil ambil semua kategori produk",
data,
total: data.length,
};
} catch (e) {
console.error("Error di findManyAll:", e);
return {
success: false,
message: "Gagal mengambil data kategori produk",
};
}
}
export default kategoriProdukFindManyAll;

View File

@@ -5,12 +5,14 @@ import kategoriProdukDelete from "./del";
import kategoriProdukCreate from "./create"; import kategoriProdukCreate from "./create";
import kategoriProdukUpdate from "./updt"; import kategoriProdukUpdate from "./updt";
import { t } from "elysia"; import { t } from "elysia";
import kategoriProdukFindManyAll from "./findManyAll";
const KategoriProduk = new Elysia({ const KategoriProduk = new Elysia({
prefix: "/kategoriproduk", prefix: "/kategoriproduk",
tags: ["Ekonomi/Kategori Produk"], tags: ["Ekonomi/Kategori Produk"],
}) })
.get("/find-many", kategoriProdukFindMany) .get("/find-many", kategoriProdukFindMany)
.get("/find-many-all", kategoriProdukFindManyAll)
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await kategoriProdukFindUnique(context); const response = await kategoriProdukFindUnique(context);
return response; return response;

View File

@@ -9,6 +9,7 @@ type FormUpdate = {
imageId: string; imageId: string;
rating: number; rating: number;
kategoriId: string[]; // Array of KategoriProduk IDs kategoriId: string[]; // Array of KategoriProduk IDs
kontak: string;
}; };
export default async function pasarDesaUpdate(context: Context) { export default async function pasarDesaUpdate(context: Context) {
@@ -31,6 +32,7 @@ export default async function pasarDesaUpdate(context: Context) {
alamatUsaha: body.alamatUsaha, alamatUsaha: body.alamatUsaha,
imageId: body.imageId, imageId: body.imageId,
rating: Number(body.rating), rating: Number(body.rating),
kontak: body.kontak
}, },
}); });

View File

@@ -23,6 +23,7 @@ export default async function kontakDaruratKeamananFindMany(context: Context) {
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.kontakDaruratKeamanan.findMany({ prisma.kontakDaruratKeamanan.findMany({
where,
include: { include: {
kontakItems: { kontakItems: {
include: { include: {

View File

@@ -25,6 +25,7 @@ export default async function kontakItemFindMany(context: Context) {
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
where,
}), }),
prisma.kontakItem.count({ where }), prisma.kontakItem.count({ where }),
]); ]);

View File

@@ -7,6 +7,7 @@ type FormCreate = Prisma.KontakDaruratGetPayload<{
name: true; name: true;
deskripsi: true; deskripsi: true;
imageId: true; imageId: true;
whatsapp: true;
}; };
}>; }>;
export default async function kontakDaruratCreate(context: Context) { export default async function kontakDaruratCreate(context: Context) {
@@ -17,6 +18,7 @@ export default async function kontakDaruratCreate(context: Context) {
name: body.name, name: body.name,
deskripsi: body.deskripsi, deskripsi: body.deskripsi,
imageId: body.imageId, imageId: body.imageId,
whatsapp: body.whatsapp,
} }
}) })
return { return {

View File

@@ -14,6 +14,7 @@ const KontakDarurat = new Elysia({
name: t.String(), name: t.String(),
deskripsi: t.String(), deskripsi: t.String(),
imageId: t.String(), imageId: t.String(),
whatsapp: t.String(),
}) })
}) })
.get("/find-many", kontakDaruratFindMany) .get("/find-many", kontakDaruratFindMany)
@@ -33,6 +34,7 @@ const KontakDarurat = new Elysia({
name: t.String(), name: t.String(),
deskripsi: t.String(), deskripsi: t.String(),
imageId: t.String(), imageId: t.String(),
whatsapp: t.String(),
}) })
} }
) )

View File

@@ -10,6 +10,7 @@ type FormUpdate = Prisma.KontakDaruratGetPayload<{
name: true; name: true;
deskripsi: true; deskripsi: true;
imageId: true; imageId: true;
whatsapp: true;
} }
}> }>
export default async function kontakDaruratUpdate(context: Context) { export default async function kontakDaruratUpdate(context: Context) {
@@ -21,6 +22,7 @@ export default async function kontakDaruratUpdate(context: Context) {
name, name,
deskripsi, deskripsi,
imageId, imageId,
whatsapp,
} = body; } = body;
if(!id) { if(!id) {
@@ -75,6 +77,7 @@ export default async function kontakDaruratUpdate(context: Context) {
name, name,
deskripsi, deskripsi,
imageId, imageId,
whatsapp,
} }
}) })

View File

@@ -3,7 +3,7 @@ import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pa
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { Box, Center, Flex, Grid, GridCol, Image, Pagination, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconMapPinFilled, IconSearch, IconShoppingCartFilled, IconStarFilled } from '@tabler/icons-react'; import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from '@tabler/icons-react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -25,7 +25,7 @@ function Page() {
} = state.findMany } = state.findMany
useShallowEffect(() => { useShallowEffect(() => {
pasarDesaState.kategoriProduk.findMany.load() pasarDesaState.kategoriProduk.findManyAll.load()
}, []) }, [])
// Filter data based on selected category // Filter data based on selected category
@@ -105,7 +105,7 @@ function Page() {
return ( return (
<Stack key={k}> <Stack key={k}>
<motion.div <motion.div
onClick={() => router.push('https://www.whatsapp.com/?lang=id')} onClick={() => router.push(`https://wa.me/${v.kontak?.replace(/\D/g, '')}`)}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }} whileTap={{ scale: 0.8 }}
> >
@@ -132,7 +132,7 @@ function Page() {
<Text fz={'sm'} ml={2}>{v.alamatUsaha}</Text> <Text fz={'sm'} ml={2}>{v.alamatUsaha}</Text>
</Flex> </Flex>
</Box> </Box>
<IconShoppingCartFilled size={20} color={colors['blue-button']} /> <IconBrandWhatsapp size={20} color={colors['blue-button']} />
</Flex> </Flex>
</Paper> </Paper>
</motion.div> </motion.div>

View File

@@ -56,7 +56,7 @@ function Page() {
<GridCol span={{ base: 12, md: 3 }}> <GridCol span={{ base: 12, md: 3 }}>
<TextInput <TextInput
radius={"lg"} radius={"lg"}
placeholder='Cari Puskesmas' placeholder='Cari Keamanan Lingkungan'
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}

View File

@@ -34,6 +34,52 @@ function Page() {
); );
} }
if (data.length === 0) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Group px={{ base: 'md', md: 100 }} justify={'space-between'} align='center'>
<Box>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kontak Darurat
</Text>
<Text fz={{ base: "h4", md: "h3" }} >
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung.
</Text>
</Box>
<TextInput
placeholder='Cari kontak darurat, nama, atau nomor...'
leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
</Group>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Flex justify={'center'} gap={'lg'} align={'center'}>
<Avatar radius={"xl"} size={'lg'} bg={colors['BG-trans']}>
<IconPhoneCall size={30} color={colors["blue-button"]} />
</Avatar>
<Box>
<Text ta={'center'} c={colors['blue-button']} py={10} fz={{ base: "md", md: "h4" }} fw={"bold"} >
Nomor Darurat Utama
</Text>
<Text ta={'center'} fw={"bold"} fz={'h2'} c={colors["blue-button"]}>112</Text>
</Box>
</Flex>
</Paper>
</Stack>
</Box>
<Center>
<Text fz={"h1"} c={colors["blue-button"]} fw={"bold"}>Tidak ada kontak darurat yang ditemukan</Text>
</Center>
</Stack>
);
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -76,57 +122,63 @@ function Page() {
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl"> <SimpleGrid cols={{ base: 1, md: 2 }} spacing="xl">
{/* Layanan Darurat */} {/* Layanan Darurat */}
{data.map((item) => ( {data.map((item) => (
<Paper <a
key={item.id} key={item.id}
p="lg" href={`tel:${item.kontakItems[0]?.kontakItem?.nomorTelepon || '112'}`}
radius="md" style={{ textDecoration: 'none' }}
bg={colors['white-trans-1']}
> >
<Group pb="md" align="center"> <Paper
<Avatar radius="xl" size="lg" bg={colors['BG-trans']}>
{item.icon && (
<IconMapper
name={item.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
)}
</Avatar>
<Text fw="bold" fz={{ base: "lg", md: "xl" }} c={colors["blue-button"]}>
{item.nama}
</Text>
</Group>
{/* Kontak Items */} p="lg"
{item.kontakItems?.map((kontak) => ( radius="md"
<Paper bg={colors['white-trans-1']}
key={kontak.id} >
p="lg" <Group pb="md" align="center">
bg={colors['BG-trans']} <Avatar radius="xl" size="lg" bg={colors['BG-trans']}>
radius="md" {item.icon && (
shadow="xs" <IconMapper
mt="sm" name={item.icon as IconKey}
> size={32}
<Group align="center" justify="space-between"> color={colors['blue-button']}
<Group align="center"> />
{kontak.kontakItem?.icon && ( )}
<IconMapper </Avatar>
name={kontak.kontakItem.icon as IconKey} <Text fw="bold" fz={{ base: "lg", md: "xl" }} c={colors["blue-button"]}>
size={24} {item.nama}
color={colors['blue-button']} </Text>
/> </Group>
)}
{/* Kontak Items */}
{item.kontakItems?.map((kontak) => (
<Paper
key={kontak.id}
p="lg"
bg={colors['BG-trans']}
radius="md"
shadow="xs"
mt="sm"
>
<Group align="center" justify="space-between">
<Group align="center">
{kontak.kontakItem?.icon && (
<IconMapper
name={kontak.kontakItem.icon as IconKey}
size={24}
color={colors['blue-button']}
/>
)}
<Text fw="bold" fz={{ base: "sm", md: "md" }} c={colors["blue-button"]}>
{kontak.kontakItem?.nama}
</Text>
</Group>
<Text fw="bold" fz={{ base: "sm", md: "md" }} c={colors["blue-button"]}> <Text fw="bold" fz={{ base: "sm", md: "md" }} c={colors["blue-button"]}>
{kontak.kontakItem?.nama} {kontak.kontakItem?.nomorTelepon}
</Text> </Text>
</Group> </Group>
<Text fw="bold" fz={{ base: "sm", md: "md" }} c={colors["blue-button"]}> </Paper>
{kontak.kontakItem?.nomorTelepon} ))}
</Text> </Paper>
</Group> </a>
</Paper>
))}
</Paper>
))} ))}
</SimpleGrid> </SimpleGrid>
<Center> <Center>

View File

@@ -2,7 +2,7 @@
'use client' 'use client'
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas'; import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Button, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Center, Paper, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowRight } from '@tabler/icons-react'; import { IconArrowRight } from '@tabler/icons-react';
import { useTransitionRouter } from 'next-view-transitions'; import { useTransitionRouter } from 'next-view-transitions';
@@ -63,7 +63,7 @@ function Page() {
<Stack pt={30} gap="lg"> <Stack pt={30} gap="lg">
{data.length > 0 ? ( {data.length > 0 ? (
data.map((item) => ( data.map((item) => (
<ActionIcon key={item.id} variant='transparent' onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)}> <a key={item.id} href={`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`}>
<Paper p="md" bg={colors['blue-button']} radius="md" shadow="sm"> <Paper p="md" bg={colors['blue-button']} radius="md" shadow="sm">
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Text fz="h3" c={colors['white-1']}> <Text fz="h3" c={colors['white-1']}>
@@ -71,7 +71,7 @@ function Page() {
</Text> </Text>
</Stack> </Stack>
</Paper> </Paper>
</ActionIcon> </a>
)) ))
) : ( ) : (
<Text color="dimmed"> <Text color="dimmed">

View File

@@ -20,10 +20,41 @@ function Page() {
} = state; } = state;
useEffect(() => { useEffect(() => {
if (!data && !loading) { load();
load(); }, []);
}
}, [data, loading]); // kalau masih loading
if (loading) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
);
}
// kalau data kosong
if (!data) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box pb={10} px={{ base: 20, md: 100 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Kantor Polisi Terdekat
</Text>
<Text pb={15} fz={'h4'} >
Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung
</Text>
</Box>
<Center py="xl">
<Text fz="lg" fw="bold" c="red">
Data Polsek tidak ada
</Text>
</Center>
</Stack >
);
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>

View File

@@ -8,10 +8,12 @@ import React, { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto'; import BackButton from '../../../desa/layanan/_com/BackButto';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useDebouncedValue } from '@mantine/hooks';
function Page() { function Page() {
const state = useProxy(polsekTerdekatState); const state = useProxy(polsekTerdekatState);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const router = useRouter() const router = useRouter()
const { const {
@@ -23,8 +25,8 @@ function Page() {
} = state.findMany; } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 3, search) load(page, 3, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
if (loading || !data) { if (loading || !data) {
return ( return (

View File

@@ -39,10 +39,9 @@ function Page() {
const nama = data?.name || 'Fasilitas Kesehatan'; const nama = data?.name || 'Fasilitas Kesehatan';
const prosedur = data?.prosedurpendaftaran.content || ''; const prosedur = data?.prosedurpendaftaran.content || '';
console.log("Prosedur:", data?.prosedurpendaftaran);
const alamat = data?.informasiumum?.alamat || '-'; const alamat = data?.informasiumum?.alamat || '-';
const jam = data?.informasiumum?.jamOperasional || '-'; const jam = data?.informasiumum?.jamOperasional || '-';
const layananUnggulan = data?.layananunggulan || ''; const layananUnggulan = data?.layananunggulan?.content || '';
const tenaga = data?.dokterdantenagamedis || null; const tenaga = data?.dokterdantenagamedis || null;
const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || ''; const fasilitasPendukungHtml = data?.fasilitaspendukung?.content || '';
const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null; const tarif = (data?.tarifdanlayanan as TarifDanLayanan) || null;

View File

@@ -53,7 +53,11 @@ function JadwalKegiatanPage() {
{item.informasijadwalkegiatan.name} {item.informasijadwalkegiatan.name}
</Text> </Text>
<Text fw={600} fz="sm" c={colors['blue-button']}> <Text fw={600} fz="sm" c={colors['blue-button']}>
{item.informasijadwalkegiatan.tanggal} {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</Text> </Text>
</Group> </Group>

View File

@@ -0,0 +1,85 @@
'use client'
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function DetailInfoWabahPenyakitUser() {
const state = useProxy(infoWabahPenyakit);
const router = useRouter();
const params = useParams();
useShallowEffect(() => {
state.findUnique.load(params?.id as string);
}, []);
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={400} radius="md" />
</Stack>
);
}
const data = state.findUnique.data;
return (
<Box py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={22} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Wrapper Detail */}
<Paper
withBorder
w="100%"
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="lg">
{/* Judul */}
<Text fz="xl" fw="bold" c={colors['blue-button']} ta="center">
{data.name || 'Kontak Darurat'}
</Text>
{/* Gambar */}
{data.image?.link && (
<Image
src={data.image.link}
alt={data.name}
radius="md"
maw={400}
mx="auto"
loading="lazy"
/>
)}
{/* Deskripsi */}
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap || '-' }}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/>
</Box>
</Stack>
</Paper>
</Box>
);
}
export default DetailInfoWabahPenyakitUser;

View File

@@ -2,10 +2,14 @@
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Badge,
Box, Box,
Button,
Center, Center,
Divider,
Grid, Grid,
GridCol, GridCol,
Group,
Image, Image,
Pagination, Pagination,
Paper, Paper,
@@ -13,27 +17,25 @@ import {
Skeleton, Skeleton,
Stack, Stack,
Text, Text,
TextInput, TextInput
Badge,
HoverCard,
Divider,
Group,
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch, IconInfoCircle } from '@tabler/icons-react'; import { IconInfoCircle, IconSearch } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useTransitionRouter } from 'next-view-transitions';
function Page() { function Page() {
const state = useProxy(infoWabahPenyakit); const state = useProxy(infoWabahPenyakit);
const router = useTransitionRouter();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500)
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 3, search); load(page, 3, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -125,28 +127,9 @@ function Page() {
<Text fz="sm" lh={1.5}> <Text fz="sm" lh={1.5}>
{v.deskripsiSingkat} {v.deskripsiSingkat}
</Text> </Text>
<HoverCard shadow="md" position="bottom" radius="md" width={300}> <Button variant="light" radius="md" size="md" onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${v.id}`)}>
<HoverCard.Target> Selengkapnya
<Text </Button>
fz="sm"
fw={500}
c={colors['blue-button']}
style={{ cursor: 'pointer' }}
>
Lihat detail lengkap
</Text>
</HoverCard.Target>
<HoverCard.Dropdown>
<Text
fz="sm"
lh={1.6}
dangerouslySetInnerHTML={{
__html: v.deskripsiLengkap,
}}
style={{wordBreak: "break-word", whiteSpace: "normal"}}
/>
</HoverCard.Dropdown>
</HoverCard>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
@@ -155,16 +138,16 @@ function Page() {
</Box> </Box>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => load(newPage)}
total={totalPages} total={totalPages}
radius="xl" radius="xl"
size="md" size="md"
mt="lg" mt="lg"
/> />
</Center> </Center>
</Stack> </Stack>
); );

View File

@@ -1,9 +1,9 @@
'use client' 'use client'
import { useState } from 'react'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
import { useProxy } from 'valtio/utils'; import colors from '@/con/colors';
import { useShallowEffect } from '@mantine/hooks';
import { import {
Box, Box,
Button,
Center, Center,
Grid, Grid,
GridCol, GridCol,
@@ -15,23 +15,24 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Tooltip, Tooltip
Badge,
} from '@mantine/core'; } from '@mantine/core';
import { IconSearch, IconPhone } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import { useDebouncedValue } from '@mantine/hooks';
import colors from '@/con/colors';
function Page() { function Page() {
const state = useProxy(kontakDarurat); const state = useProxy(kontakDarurat);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 500)
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 6, search); load(page, 3, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -113,16 +114,16 @@ function Page() {
{v.name} {v.name}
</Text> </Text>
<Text fz="sm" c="dimmed" ta="center" lineClamp={3}> <Text fz="sm" c="dimmed" ta="center" lineClamp={3}>
<span style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }} /> <span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Text> </Text>
<Badge <Button
color="blue"
leftSection={<IconPhone size={14} />}
variant="light" variant="light"
mt="sm" leftSection={<IconBrandWhatsapp size={18} />}
> component="a"
Panggil Sekarang href={`https://wa.me/${v.whatsapp.replace(/\D/g, '')}`}
</Badge> target="_blank"
aria-label="Hubungi WhatsApp"
>WhatsApp</Button>
</Stack> </Stack>
</Paper> </Paper>
))} ))}
@@ -130,22 +131,20 @@ function Page() {
)} )}
</Box> </Box>
{totalPages > 1 && ( <Center mt="xl">
<Center mt="xl"> <Pagination
<Pagination value={page}
value={page} onChange={(newPage) => load(newPage, 3, search)}
onChange={(newPage) => load(newPage, 6, search)} total={totalPages}
total={totalPages} size="lg"
radius="xl" radius="xl"
size="md" styles={{
styles={{ control: {
control: { border: `1px solid ${colors['blue-button']}`,
borderRadius: '999px', },
}, }}
}} />
/> </Center>
</Center>
)}
</Stack> </Stack>
); );
} }

View File

@@ -17,7 +17,7 @@ import {
TextInput, TextInput,
Tooltip Tooltip
} from '@mantine/core' } from '@mantine/core'
import { useShallowEffect } from '@mantine/hooks' import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'
import { IconSearch } from '@tabler/icons-react' import { IconSearch } from '@tabler/icons-react'
import { useState } from 'react' import { useState } from 'react'
import { useProxy } from 'valtio/utils' import { useProxy } from 'valtio/utils'
@@ -26,12 +26,12 @@ import BackButton from '../../desa/layanan/_com/BackButto'
function Page() { function Page() {
const state = useProxy(penangananDarurat) const state = useProxy(penangananDarurat)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 500)
const { data, page, totalPages, loading, load } = state.findMany const { data, page, totalPages, loading, load } = state.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 6, search) load(page, 3, debouncedSearch)
}, [page, search]) }, [page, debouncedSearch])
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -127,7 +127,7 @@ function Page() {
c="dimmed" c="dimmed"
lineClamp={4} lineClamp={4}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Box> </Box>
</Stack> </Stack>
@@ -141,22 +141,21 @@ function Page() {
)} )}
</Box> </Box>
{totalPages > 1 && ( <Center mt="xl">
<Center mt="xl"> <Pagination
<Pagination value={page}
value={page} onChange={(newPage) => load(newPage, 3, search)}
onChange={(newPage) => load(newPage, 6, search)} total={totalPages}
total={totalPages} size="lg"
size="lg" radius="xl"
radius="xl" styles={{
styles={{ control: {
control: { border: `1px solid ${colors['blue-button']}`,
border: `1px solid ${colors['blue-button']}`, },
}, }}
}} />
/> </Center>
</Center>
)}
</Stack> </Stack>
) )
} }

View File

@@ -26,6 +26,16 @@ export default function Page() {
); );
} }
if (data.length === 0) {
return (
<Box py="xl" px={{ base: "md", md: 100 }}>
<Text fz="lg" fw="bold" c={colors["blue-button"]}>
Tidak ada posyandu yang ditemukan
</Text>
</Box>
);
}
return ( return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="xl"> <Stack pos="relative" bg={colors.Bg} py="xl" gap="xl">
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
@@ -111,10 +121,11 @@ export default function Page() {
<IconCalendar size={18} stroke={1.5} /> <IconCalendar size={18} stroke={1.5} />
<Text fz="sm" c="dimmed"> <Text fz="sm" c="dimmed">
Jadwal:{" "} Jadwal:{" "}
<span style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} /> <span style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.jadwalPelayanan }} />
</Text> </Text>
</Flex> </Flex>
<Spoiler <Spoiler
key={`spoiler-${v.id}`}
maxHeight={70} maxHeight={70}
showLabel="Lihat selengkapnya" showLabel="Lihat selengkapnya"
hideLabel="Sembunyikan" hideLabel="Sembunyikan"
@@ -124,7 +135,7 @@ export default function Page() {
fz="sm" fz="sm"
lh={1.5} lh={1.5}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
</Spoiler> </Spoiler>
</Stack> </Stack>

View File

@@ -30,7 +30,7 @@ import BackButton from "../../desa/layanan/_com/BackButto";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan"; import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
import { useState } from "react"; import { useState } from "react";
import { useShallowEffect } from "@mantine/hooks"; import { useDebouncedValue, useShallowEffect } from "@mantine/hooks";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
const manfaatProgram = [ const manfaatProgram = [
@@ -58,11 +58,12 @@ export default function Page() {
const state = useProxy(programKesehatan); const state = useProxy(programKesehatan);
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const { data, page, totalPages, loading, load } = state.findMany; const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 3, search); load(page, 3, debouncedSearch);
}, [page, search]); }, [page, debouncedSearch]);
if (loading || !data) { if (loading || !data) {
return ( return (
@@ -125,14 +126,18 @@ export default function Page() {
className="hover-scale" className="hover-scale"
> >
<Stack gap="md"> <Stack gap="md">
<Image <Box h={180} w="100%">
src={v.image?.link} <Image
alt={v.name} src={v.image?.link}
radius="xl" alt={v.name}
height={180} radius="xl"
fit="cover" w="100%"
loading="lazy" h="100%"
/> fit="cover"
loading="lazy"
/>
</Box>
<Box px="lg" pb="lg"> <Box px="lg" pb="lg">
<Text <Text
fw="bold" fw="bold"
@@ -147,7 +152,7 @@ export default function Page() {
c="dimmed" c="dimmed"
lineClamp={3} lineClamp={3}
dangerouslySetInnerHTML={{ __html: v.deskripsi }} dangerouslySetInnerHTML={{ __html: v.deskripsi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{ wordBreak: "break-word", whiteSpace: "normal" }}
/> />
<Group justify="space-between" mt="md"> <Group justify="space-between" mt="md">
<Group gap="xs"> <Group gap="xs">
@@ -155,13 +160,13 @@ export default function Page() {
<Text size="sm"> <Text size="sm">
{v.createdAt {v.createdAt
? new Date(v.createdAt).toLocaleDateString( ? new Date(v.createdAt).toLocaleDateString(
"id-ID", "id-ID",
{ {
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
} }
) )
: "Tanggal tidak tersedia"} : "Tanggal tidak tersedia"}
</Text> </Text>
</Group> </Group>