Fix QC Kak Inno 21 Oktober, QC Kak Ayu 21 Oktober, QC Keano, && QC Pak Jun 21 Oktober

This commit is contained in:
2025-10-22 17:00:12 +08:00
parent 827c1c191a
commit 0ff0d5234a
15 changed files with 333 additions and 189 deletions

View File

@@ -55,9 +55,9 @@ function EditProgramKemiskinan() {
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
stateProgram.findUnique const loadData = async () => {
.load(id) try {
.then(() => { await stateProgram.findUnique.load(id);
const data = stateProgram.findUnique.data; const data = stateProgram.findUnique.data;
if (data) { if (data) {
setFormData({ setFormData({
@@ -70,12 +70,16 @@ function EditProgramKemiskinan() {
}, },
}); });
} }
}) } catch (err) {
.catch((err) => {
console.error('Error load data:', err); console.error('Error load data:', err);
toast.error('Gagal mengambil data program'); toast.error('Gagal mengambil data program');
}); }
}, [id, stateProgram.findUnique]); };
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); // ✅ hanya trigger saat id berubah
// generic handler untuk field top-level // generic handler untuk field top-level
const handleChange = useCallback( const handleChange = useCallback(

View File

@@ -62,11 +62,23 @@ function Page() {
Informasi dan Pelayanan Administrasi Digital Informasi dan Pelayanan Administrasi Digital
</Text> </Text>
</Box> </Box>
<Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy"/> <Image src={state.findUnique.data?.image?.link || ''} alt='' w={"100%"} loading="lazy" />
</Container> </Container>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.content || '' }} /> <Text
py={20}
fz={{ base: "sm", md: "lg" }}
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
ta="justify"
style={{
wordBreak: "break-word",
whiteSpace: "normal",
}}
dangerouslySetInnerHTML={{
__html: state.findUnique.data?.content || "",
}}
/>
</Stack> </Stack>
</Box> </Box>
</Stack> </Stack>

View File

@@ -186,12 +186,11 @@ function Page() {
stateCreate.create.form.kategoriId = ''; stateCreate.create.form.kategoriId = '';
} }
}} }}
searchable // searchable
clearable clearable
nothingFoundMessage="Tidak ditemukan" nothingFoundMessage="Tidak ditemukan"
required required
/> />
<Button bg={colors['blue-button']} onClick={handleSubmit}> <Button bg={colors['blue-button']} onClick={handleSubmit}>
Simpan Simpan
</Button> </Button>

View File

@@ -240,15 +240,8 @@ function Page() {
</Table> </Table>
</Stack> </Stack>
</Card> </Card>
</Stack>
</Grid.Col>
</Grid>
</Box>
<Box px={{ base: 'md', md: 100 }}> <Card radius="xl" p="lg" withBorder>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 8 }}>
<Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Fasilitas Pendukung</Title> <Title order={3}>Fasilitas Pendukung</Title>
<Divider /> <Divider />
@@ -264,8 +257,7 @@ function Page() {
)} )}
</Stack> </Stack>
</Card> </Card>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Card radius="xl" p="lg" withBorder> <Card radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">
<Title order={3}>Layanan & Tarif</Title> <Title order={3}>Layanan & Tarif</Title>
@@ -303,10 +295,11 @@ function Page() {
)} )}
</Stack> </Stack>
</Card> </Card>
</Stack>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Box> </Box>
<Box px={{ base: 'md', md: 100 }} pb="xl"> <Box px={{ base: 'md', md: 100 }} pb="xl">
<Paper radius="xl" p="lg" withBorder> <Paper radius="xl" p="lg" withBorder>
<Stack gap="md"> <Stack gap="md">

View File

@@ -4,14 +4,16 @@ import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
Box, Box,
Button,
Divider, Divider,
Group, Group,
Modal,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Text Text
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useDisclosure, useShallowEffect } from '@mantine/hooks';
import { IconMail, IconPhone, IconUser } from '@tabler/icons-react'; import { IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -21,6 +23,7 @@ import CreatePendaftaran from '../create/page';
function Page() { function Page() {
const params = useParams(); const params = useParams();
const state = useProxy(jadwalkegiatanState); const state = useProxy(jadwalkegiatanState);
const [opened, { open, close }] = useDisclosure(false);
useShallowEffect(() => { useShallowEffect(() => {
state.findUnique.load(params?.id as string); state.findUnique.load(params?.id as string);
@@ -66,28 +69,38 @@ function Page() {
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Deskripsi Kegiatan</Text> <Text fz="lg" fw="bold">Deskripsi Kegiatan</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsijadwalkegiatan.deskripsi }} />
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Layanan yang Tersedia</Text> <Text fz="lg" fw="bold">Layanan yang Tersedia</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.layananjadwalkegiatan.content }} />
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Syarat & Ketentuan</Text> <Text fz="lg" fw="bold">Syarat & Ketentuan</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.syaratketentuanjadwalkegiatan.content }} />
</Stack> </Stack>
<Stack gap="sm"> <Stack gap="sm">
<Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text> <Text fz="lg" fw="bold">Dokumen yang Perlu Dibawa</Text>
<Divider /> <Divider />
<Text ta="justify" fz="md" style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} /> <Text ta="justify" fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Stack> </Stack>
<CreatePendaftaran /> <Stack gap="sm">
<Text fz="lg" fw="bold">Pendaftaran Kegiatan</Text>
<Divider />
<Group>
<Button onClick={open}>Buat Pendaftaran</Button>
</Group>
</Stack>
<Modal opened={opened} onClose={close}>
<CreatePendaftaran />
</Modal>
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm"> <Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
<Stack gap="xs"> <Stack gap="xs">

View File

@@ -49,12 +49,10 @@ function Page() {
<Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}> <Paper p={20} bg={colors['white-trans-1']} shadow="md" radius="md" style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack gap="md"> <Stack gap="md">
<Box> <Box>
<Tooltip label={tujuan.data?.judul} position="top" withArrow> <Tooltip label={<Text fz={"sm"} c={"white"} dangerouslySetInnerHTML={{ __html: tujuan.data?.judul || '' }} /> } position="top" withArrow>
<Stack gap={4} align="center"> <Stack gap={4} align="center">
<IconLeaf size={28} color={colors['blue-button']} /> <IconLeaf size={28} color={colors['blue-button']} />
<Text fz="h3" fw="bold" c={colors['blue-button']} ta="center"> <Text dangerouslySetInnerHTML={{ __html: tujuan.data?.judul || '' }} fz="h3" fw="bold" c={colors['blue-button']} ta="center" />
{tujuan.data?.judul}
</Text>
</Stack> </Stack>
</Tooltip> </Tooltip>
</Box> </Box>

View File

@@ -74,7 +74,7 @@ function Page() {
<Title fz={55} fw={900} c={colors['blue-button']}> <Title fz={55} fw={900} c={colors['blue-button']}>
Wujudkan Mimpi Pendidikanmu di Desa Darmasaba Wujudkan Mimpi Pendidikanmu di Desa Darmasaba
</Title> </Title>
<Text fz="lg" mt="md" c="dimmed"> <Text fz="lg" mt="md" fw={"bold"}>
Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba. Program beasiswa untuk mendukung pendidikan berkualitas bagi generasi muda Desa Darmasaba.
</Text> </Text>
<Group mt="xl"> <Group mt="xl">

View File

@@ -15,7 +15,7 @@ import {
Timeline, Timeline,
Title Title
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react'; import { IconArrowLeft, IconChecklist, IconInfoCircle, IconQuote, IconSchool, IconTimeline, IconUserPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -69,10 +69,11 @@ export default function BeasiswaPage() {
{/* Hero Section */} {/* Hero Section */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Stack gap="md" maw={600}> <Stack gap="md" maw={600}>
<Title order={2} c="blue.9"> <Group>
Program Beasiswa Pendidikan Desa Darmasaba <IconSchool size={30} color={colors["blue-button"]} />
</Title> <Title order={2}>Program Beasiswa Pendidikan Desa Darmasaba</Title>
<Text c="dimmed"> </Group>
<Text>
Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba Program ini bertujuan untuk mendukung pendidikan generasi muda di Desa Darmasaba
agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan. agar dapat melanjutkan studi ke jenjang lebih tinggi dengan dukungan finansial dan pendampingan.
</Text> </Text>
@@ -81,21 +82,35 @@ export default function BeasiswaPage() {
{/* Tentang Program */} {/* Tentang Program */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Tentang Program <IconInfoCircle size={24} color={colors["blue-button"]} />
</Title> <Title order={3}>Tentang Program</Title>
</Group>
<Text> <Text>
Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses Program Beasiswa Desa Darmasaba adalah inisiatif pemerintah desa untuk meningkatkan akses
pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan pendidikan bagi siswa berprestasi dan kurang mampu. Melalui program ini, desa memberikan bantuan
biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih. biaya sekolah, bimbingan akademik, serta pelatihan soft skill bagi peserta terpilih.
</Text> </Text>
{/* Tambahkan info tahun berjalan di sini */}
<Paper mt="md" p="md" radius="lg" shadow="xs" bg="#f8fbff" withBorder>
<Text fw={500} c={colors["blue-button"]}>
📅 Periode Beasiswa Tahun 2025
</Text>
<Text fz="sm" c="dimmed">
Pendaftaran beasiswa dibuka mulai <strong>1 Januari 2025</strong> dan ditutup pada
<strong>31 Mei 2025</strong>.
Pengumuman hasil seleksi akan diumumkan pada pertengahan Juni 2025 melalui website resmi Desa Darmasaba.
</Text>
</Paper>
</Container> </Container>
{/* Syarat dan Ketentuan */} {/* Syarat dan Ketentuan */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Syarat Pendaftaran <IconChecklist size={24} color={colors["blue-button"]} />
</Title> <Title order={3}>Syarat Pendaftaran</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="lg">
<Paper shadow="sm" p="md" radius="lg" withBorder> <Paper shadow="sm" p="md" radius="lg" withBorder>
@@ -123,42 +138,61 @@ export default function BeasiswaPage() {
{/* Proses Seleksi */} {/* Proses Seleksi */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Proses Seleksi <IconTimeline size={24} color={colors["blue-button"]} />
</Title> <Title order={3}>Proses Seleksi</Title>
</Group>
<Timeline active={4} bulletSize={24} lineWidth={2}> <Timeline active={4} bulletSize={24} lineWidth={2}>
<Timeline.Item title="Pendaftaran Online"> <Timeline.Item title="Pendaftaran Online">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung. Calon peserta mengisi formulir pendaftaran dan mengunggah dokumen pendukung.
</Text> </Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 1 Februari 31 Mei 2025
</Text>
</Timeline.Item> </Timeline.Item>
<Timeline.Item title="Seleksi Administrasi"> <Timeline.Item title="Seleksi Administrasi">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Panitia memverifikasi kelengkapan dan validitas berkas. Panitia memverifikasi kelengkapan dan validitas berkas.
</Text> </Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 57 hari kerja setelah penutupan pendaftaran
</Text>
</Timeline.Item> </Timeline.Item>
<Timeline.Item title="Wawancara dan Penilaian"> <Timeline.Item title="Wawancara dan Penilaian">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi. Peserta yang lolos administrasi akan diundang untuk wawancara langsung dengan tim seleksi.
</Text> </Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 710 hari kerja setelah pengumuman seleksi administrasi
</Text>
</Timeline.Item> </Timeline.Item>
<Timeline.Item title="Pengumuman Penerima"> <Timeline.Item title="Pengumuman Penerima">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba. Daftar penerima beasiswa diumumkan melalui website resmi Desa Darmasaba.
</Text> </Text>
<Text size="sm" fw={500} c={colors["blue-button"]} mt={4}>
Estimasi waktu: 5 hari kerja setelah tahap wawancara selesai
</Text>
</Timeline.Item> </Timeline.Item>
</Timeline> </Timeline>
<Text c="dimmed" size="sm" mt="lg" ta="center">
🗓 Total estimasi keseluruhan proses: sekitar 34 minggu setelah penutupan pendaftaran
</Text>
</Container> </Container>
{/* Testimoni */} {/* Testimoni */}
<Container size="lg" py="xl"> <Container size="lg" py="xl">
<Title order={3} mb="sm"> <Group mb="sm">
Cerita Sukses Penerima Beasiswa <IconQuote size={24} color={colors["blue-button"]} />
</Title> <Title order={3}>Cerita Sukses Penerima Beasiswa</Title>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg"> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<Paper shadow="md" p="lg" radius="lg"> <Paper shadow="md" p="lg" radius="lg">
@@ -183,7 +217,10 @@ export default function BeasiswaPage() {
{/* CTA Akhir */} {/* CTA Akhir */}
<Container size="lg" py="xl" ta="center"> <Container size="lg" py="xl" ta="center">
<Title order={3}>Siap Bergabung dengan Program Ini?</Title> <Group justify="center" mb="sm">
<IconUserPlus size={28} color={colors["blue-button"]} />
<Title order={3}>Siap Bergabung dengan Program Ini?</Title>
</Group>
<Text c="dimmed" mb="md"> <Text c="dimmed" mb="md">
Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba. Segera daftar dan wujudkan mimpimu bersama Desa Darmasaba.
</Text> </Text>

View File

@@ -43,7 +43,7 @@ function Page() {
Pendidikan Non Formal Pendidikan Non Formal
</Title> </Title>
<Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto"> <Text ta="center" fz="lg" lh={1.6} c="black" maw={800} mx="auto">
Bentuk pendidikan di luar sekolah yang terstruktur, bertujuan memberikan keterampilan, pengetahuan, dan pengembangan karakter masyarakat dari berbagai usia serta latar belakang. Pendidikan non formal merupakan bentuk pendidikan di luar sekolah yang terstruktur, bertujuan untuk memberikan keterampilan, pengetahuan, serta pengembangan karakter masyarakat dari berbagai usia dan latar belakang.
</Text> </Text>
</Box> </Box>
<SimpleGrid <SimpleGrid

View File

@@ -16,16 +16,11 @@ import {
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
import { DateInput } from '@mantine/dates'; import { DateInput } from '@mantine/dates';
import { import { IconArrowRight, IconBook2, IconUser } from '@tabler/icons-react';
IconArrowRight,
IconBook2,
IconUser
} from '@tabler/icons-react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
export interface ModalPeminjamanProps { export interface ModalPeminjamanProps {
opened: boolean; opened: boolean;
onClose: () => void; onClose: () => void;
@@ -45,11 +40,12 @@ export default function ModalPeminjaman({
}: ModalPeminjamanProps) { }: ModalPeminjamanProps) {
const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku); const snap = useSnapshot(perpustakaanDigitalState.peminjamanBuku);
// reset form setiap modal dibuka const BATAS_HARI_PINJAM = 4;
// Reset form setiap modal dibuka
useEffect(() => { useEffect(() => {
if (opened && buku) { if (opened && buku) {
perpustakaanDigitalState.peminjamanBuku.create.form = { perpustakaanDigitalState.peminjamanBuku.create.form = {
...perpustakaanDigitalState.peminjamanBuku.create.form,
bukuId: buku.id, bukuId: buku.id,
nama: '', nama: '',
noTelp: '', noTelp: '',
@@ -99,7 +95,14 @@ export default function ModalPeminjaman({
</Badge> </Badge>
)} )}
<Text fz="sm" c="dimmed" lineClamp={3} dangerouslySetInnerHTML={{ __html: buku.deskripsi || 'Tidak ada deskripsi' }} /> <Text
fz="sm"
c="dimmed"
lineClamp={3}
dangerouslySetInnerHTML={{
__html: buku.deskripsi || 'Tidak ada deskripsi',
}}
/>
</Stack> </Stack>
</Group> </Group>
@@ -112,7 +115,8 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />} leftSection={<IconUser size={16} />}
value={snap.create.form.nama} value={snap.create.form.nama}
onChange={(e) => onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.nama = e.currentTarget.value) (perpustakaanDigitalState.peminjamanBuku.create.form.nama =
e.currentTarget.value)
} }
required required
/> />
@@ -123,7 +127,8 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />} leftSection={<IconUser size={16} />}
value={snap.create.form.noTelp} value={snap.create.form.noTelp}
onChange={(e) => onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.noTelp = e.currentTarget.value) (perpustakaanDigitalState.peminjamanBuku.create.form.noTelp =
e.currentTarget.value)
} }
required required
/> />
@@ -134,11 +139,13 @@ export default function ModalPeminjaman({
leftSection={<IconUser size={16} />} leftSection={<IconUser size={16} />}
value={snap.create.form.alamat} value={snap.create.form.alamat}
onChange={(e) => onChange={(e) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.alamat = e.currentTarget.value) (perpustakaanDigitalState.peminjamanBuku.create.form.alamat =
e.currentTarget.value)
} }
required required
/> />
{/* === OTOMATIS SET BATAS DAN TANGGAL KEMBALI === */}
<DateInput <DateInput
label="Tanggal Pinjam" label="Tanggal Pinjam"
placeholder="Pilih tanggal pinjam" placeholder="Pilih tanggal pinjam"
@@ -148,64 +155,83 @@ export default function ModalPeminjaman({
: null : null
} }
onChange={(date) => { onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = if (date) {
date ? new Date(date).toISOString() : ''; const tanggalPinjam = new Date(date);
// simpan tanggal pinjam
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam =
tanggalPinjam.toISOString();
// hitung batas +4 hari
const batasKembali = new Date(tanggalPinjam);
batasKembali.setDate(batasKembali.getDate() + BATAS_HARI_PINJAM);
// set batas & tanggal kembali otomatis
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali =
batasKembali.toISOString();
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
batasKembali.toISOString();
toast.info(
`Batas pengembalian otomatis diset ke ${batasKembali.toLocaleDateString('id-ID')} (+${BATAS_HARI_PINJAM} hari).`
);
} else {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalPinjam = '';
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = '';
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali = '';
}
}} }}
required required
/> />
<Box> <Box>
<Text>Catatan</Text> <Text fw={500}>Catatan</Text>
<CreateEditor <CreateEditor
value={snap.create.form.catatan} value={snap.create.form.catatan}
onChange={(e) => onChange={(val) =>
(perpustakaanDigitalState.peminjamanBuku.create.form.catatan = e) (perpustakaanDigitalState.peminjamanBuku.create.form.catatan =
val)
} }
/> />
</Box> </Box>
<DateInput
label="Tanggal Kembali"
placeholder="Pilih tanggal kembali"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
onChange={(date) => {
perpustakaanDigitalState.peminjamanBuku.create.form.tanggalKembali =
date ? new Date(date).toISOString() : '';
}}
required
/>
<DateInput <DateInput
label="Batas Pengembalian" label="Batas Pengembalian"
placeholder="Pilih tanggal kembali" placeholder="Otomatis diatur +4 hari dari tanggal pinjam"
value={ value={
snap.create.form.batasKembali snap.create.form.batasKembali
? new Date(snap.create.form.batasKembali) ? new Date(snap.create.form.batasKembali)
: null : null
} }
onChange={(date) => { disabled
perpustakaanDigitalState.peminjamanBuku.create.form.batasKembali = readOnly
date ? new Date(date).toISOString() : '';
}}
required
/> />
<DateInput
label="Tanggal Kembali"
placeholder="Otomatis sama dengan batas pengembalian"
value={
snap.create.form.tanggalKembali
? new Date(snap.create.form.tanggalKembali)
: null
}
disabled
readOnly
/>
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
loading={snap.create.loading} loading={snap.create.loading}
disabled={ disabled={
!snap.create.form.nama || !snap.create.form.nama || !snap.create.form.tanggalPinjam
!snap.create.form.tanggalPinjam ||
!snap.create.form.batasKembali ||
!snap.create.form.tanggalKembali
} }
rightSection={<IconArrowRight size={16} />} rightSection={<IconArrowRight size={16} />}
radius="xl" radius="xl"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
> >
Pinjam Buku Pinjam Buku
</Button> </Button>

View File

@@ -3,8 +3,8 @@
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan"; import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
import colors from "@/con/colors"; import colors from "@/con/colors";
import { BarChart, PieChart } from '@mantine/charts'; 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 { Box, Button, Center, Container, Flex, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import { useDisclosure, useMediaQuery, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useState } from "react"; import { useState } from "react";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
@@ -18,14 +18,13 @@ interface ChartDataItem {
function Kepuasan() { function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden); const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany; const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]); const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]); const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]); const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]); const [barChartData, setBarChartData] = useState<Array<{ month: string; Responden: number }>>([]);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false)
const isMobile = useMediaQuery("(max-width: 768px)");
const resetForm = () => { const resetForm = () => {
state.create.form = { state.create.form = {
@@ -122,18 +121,18 @@ function Kepuasan() {
// Convert map to array and sort by date // Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries()) const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => { .map(([key, Responden]) => {
const [year, month] = key.split('-'); const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1) const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' }); .toLocaleString('id-ID', { month: 'long' });
return { return {
month: `${monthName} ${year}`, month: `${monthName} ${year}`,
count, Responden,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10) sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
}; };
}) })
.sort((a, b) => a.sortKey - b.sortKey) .sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count })); .map(({ month, Responden }) => ({ month, Responden }));
setBarChartData(barData); setBarChartData(barData);
} }
@@ -141,12 +140,12 @@ function Kepuasan() {
if ((loading && !data) || !data) { if ((loading && !data) || !data) {
return ( return (
<Stack py={10} px="sm"> <Stack py={10} px="xl">
<Skeleton height={200} mb="md" /> <Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="md"> <SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={200} /> <Skeleton height={300} />
<Skeleton height={200} /> <Skeleton height={300} />
<Skeleton height={200} /> <Skeleton height={300} />
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
); );
@@ -157,10 +156,16 @@ function Kepuasan() {
<Stack p="sm"> <Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}> <Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Center> <Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text> <Text fw={"bold"} fz={{ base: "1.8rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</Center> </Center>
<Text ta={"center"} fz={{ base: "1rem", md: "1.3rem" }}>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}> <Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button> <Button
radius={"lg"}
onClick={open}
variant="gradient"
gradient={{ from: "#26667F", to: "#124170" }}
>Ajukan Responden</Button>
</Center> </Center>
</Container> </Container>
<Box px={"xl"}> <Box px={"xl"}>
@@ -177,10 +182,10 @@ function Kepuasan() {
</Box> </Box>
</Flex> </Flex>
<BarChart <BarChart
h={300} h={window.innerWidth < 480 ? 200 : 300}
data={barChartData} data={barChartData}
dataKey="month" dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]} series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y" tickLine="y"
xAxisLabel="Bulan" xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden" yAxisLabel="Jumlah Responden"
@@ -191,12 +196,9 @@ function Kepuasan() {
</Paper> </Paper>
<Box py={"xl"}> <Box py={"xl"}>
<SimpleGrid <SimpleGrid
cols={{ cols={{ base: 1, sm: 2, lg: 3 }}
base: 1, spacing="md"
md: 1, verticalSpacing="md"
lg: 1,
xl: 3
}}
> >
{/* Chart Jenis Kelamin */} {/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
@@ -215,7 +217,7 @@ function Kepuasan() {
withLabels withLabels
withTooltip withTooltip
labelsType="percent" labelsType="percent"
size={200} size={250} // Fixed size in pixels
data={donutDataJenisKelamin} data={donutDataJenisKelamin}
/> />
</Center> </Center>
@@ -254,7 +256,7 @@ function Kepuasan() {
labelsPosition="outside" labelsPosition="outside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={200} size={250}
data={donutDataRating} data={donutDataRating}
/> />
</Center> </Center>
@@ -297,7 +299,7 @@ function Kepuasan() {
labelsPosition="outside" labelsPosition="outside"
labelsType="percent" labelsType="percent"
withLabelsLine withLabelsLine
size={190} size={250}
data={donutDataKelompokUmur} data={donutDataKelompokUmur}
/> />
</Center> </Center>
@@ -330,7 +332,7 @@ function Kepuasan() {
<TextInput <TextInput
label="Nama" label="Nama"
type='text' type='text'
placeholder="masukkan nama" placeholder="Masukkan nama"
defaultValue={state.create.form.name} defaultValue={state.create.form.name}
onChange={(val) => { onChange={(val) => {
state.create.form.name = val.currentTarget.value; state.create.form.name = val.currentTarget.value;
@@ -413,41 +415,57 @@ function Kepuasan() {
); );
} }
return ( return (
<Stack p="sm"> <Stack p={"sm"}>
<Container w={{ base: "100%", md: "80%" }} p={isMobile ? "md" : "xl"}> <Container size="lg" px="md">
<Stack gap="xs"> <Center>
<Text ta="center" fz={{ base: "2rem", md: "3rem" }}>Indeks Kepuasan Masyarakat</Text> <Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Group justify="center"> </Center>
<Button radius="lg" bg={colors["blue-button"]} onClick={open}> <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>
Ajukan Responden <Center mt={10}>
</Button> <Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
</Group> </Center>
</Stack>
</Container> </Container>
<Box px={isMobile ? "sm" : "xl"}> <Box px={"xl"}>
<Paper p="lg" bg={colors.Bg}> <Paper p={"lg"} bg={colors.Bg}>
<Paper p={isMobile ? "sm" : "lg"}> <Paper p={"lg"}>
<Stack gap="xs"> <Stack gap={"xs"}>
<Flex direction={isMobile ? "column" : "row"} justify="space-between" align={isMobile ? "start" : "center"}> <Flex
<Text fw="bold" mb={isMobile ? "sm" : 0}>Pelayanan Terhadap Publik Desa Darmasaba</Text> direction={{ base: "column", sm: "row" }}
<Box> justify="space-between"
<Text fz="sm" fw="bold" c={colors["blue-button"]}>Total Responden</Text> align={{ base: "flex-start", sm: "center" }}
<Text ta="end" fz="h1" fw="bold" c={colors["blue-button"]}> >
{state.findMany.total.toLocaleString("id-ID")} <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')}
</Text> </Text>
</Box> </Box>
</Flex> </Flex>
<BarChart <BarChart
h={isMobile ? 200 : 300} h={300}
data={barChartData} data={barChartData}
dataKey="month" dataKey="month"
series={[{ name: "count", color: colors["blue-button"] }]} series={[{ name: 'Responden', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip withTooltip
tooltipAnimationDuration={200}
/> />
</Stack> </Stack>
</Paper> </Paper>
<Box py="xl"> <Box py={"xl"}>
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="lg"> <SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
{/* Chart Jenis Kelamin */} {/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md"> <Paper bg={colors['white-1']} p="md" radius="md">
<Stack> <Stack>
@@ -457,17 +475,28 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
<Paper p="md" radius="md"> <Paper p="md" radius="md" withBorder>
<Stack> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Center> <Box style={{ position: 'relative', width: '100%' }}>
<PieChart <Center>
size={isMobile ? 150 : 200} <PieChart
withLabels withLabels
data={donutDataJenisKelamin} withTooltip
withTooltip labelsType="percent"
/> size={200}
</Center> data={donutDataJenisKelamin}
</Stack> />
</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> </Paper>
)} )}
</Stack> </Stack>
@@ -482,18 +511,35 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
<Paper p="md" radius="md"> <Paper p="md" radius="md" withBorder>
<Stack> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Center> <Box style={{ position: 'relative', width: '100%' }}>
<PieChart <Center>
size={isMobile ? 150 : 200} <PieChart
withLabels withTooltip
labelsPosition="outside" tooltipAnimationDuration={200}
withLabelsLine withLabels
data={donutDataRating} labelsPosition="outside"
/> labelsType="percent"
</Center> withLabelsLine
</Stack> 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> </Paper>
)} )}
</Stack> </Stack>
@@ -508,18 +554,35 @@ function Kepuasan() {
Belum ada data untuk ditampilkan dalam grafik Belum ada data untuk ditampilkan dalam grafik
</Text> </Text>
) : ( ) : (
<Paper p="md" radius="md"> <Paper p="md" radius="md" withBorder>
<Stack> <Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Center> <Box style={{ position: 'relative', width: '100%' }}>
<PieChart <Center>
size={isMobile ? 150 : 200} <PieChart
withLabels withTooltip
labelsPosition="outside" tooltipAnimationDuration={200}
withLabelsLine withLabels
data={donutDataKelompokUmur} labelsPosition="outside"
/> labelsType="percent"
</Center> withLabelsLine
</Stack> 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> </Paper>
)} )}
</Stack> </Stack>
@@ -542,7 +605,7 @@ function Kepuasan() {
}} }}
/> />
<TextInput <TextInput
label="Tanggal" label="Tanggal Pengisian"
type="date" type="date"
placeholder="masukkan tanggal" placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal} defaultValue={state.create.form.tanggal}

View File

@@ -88,7 +88,6 @@ function Page() {
<Text <Text
fz={{ base: 'md', md: 'lg' }} fz={{ base: 'md', md: 'lg' }}
lh={1.7} lh={1.7}
ta="center"
dangerouslySetInnerHTML={{ __html: item.misi }} dangerouslySetInnerHTML={{ __html: item.misi }}
style={{wordBreak: "break-word", whiteSpace: "normal"}} style={{wordBreak: "break-word", whiteSpace: "normal"}}
/> />

View File

@@ -41,7 +41,7 @@ export function Navbar() {
stateNav.mobileOpen = false; stateNav.mobileOpen = false;
}} }}
> >
<Tooltip label="Go to homepage" position="bottom" withArrow> <Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
<Image <Image
src="/darmasaba-icon.png" src="/darmasaba-icon.png"
alt="Village Logo" alt="Village Logo"

View File

@@ -26,7 +26,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
<Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}> <Stack gap={0} visibleFrom="sm" bg={colors["white-trans-1"]}>
<Container pos="relative" w={{ base: '100%', md: '80%' }} fluid> <Container pos="relative" w={{ base: '100%', md: '80%' }} fluid>
<Flex align="center" justify="space-between" wrap={{ base: "wrap", md: "nowrap" }}> <Flex align="center" justify="space-between" wrap={{ base: "wrap", md: "nowrap" }}>
<Tooltip label="Go to Homepage" position="bottom" withArrow> <Tooltip label="Kembali ke Beranda" position="bottom" withArrow>
<ActionIcon <ActionIcon
radius="xl" radius="xl"
variant="transparent" variant="transparent"
@@ -55,7 +55,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
/> />
))} ))}
<Tooltip label="Search content" position="bottom" withArrow> <Tooltip label="Cari Konten" position="bottom" withArrow>
<ActionIcon <ActionIcon
variant="transparent" variant="transparent"
c={isSearch ? 'gray' : colors["blue-button"]} c={isSearch ? 'gray' : colors["blue-button"]}
@@ -71,7 +71,7 @@ export function NavbarMainMenu({ listNavbar }: { listNavbar: MenuItem[] }) {
{/* hanya tampil kalau role = admin */} {/* hanya tampil kalau role = admin */}
{stateAuth.role === "admin" && ( {stateAuth.role === "admin" && (
<Tooltip label="My Profile" position="bottom" withArrow> <Tooltip label="Profil Saya" position="bottom" withArrow>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
next.push("/admin/landing-page/profile/program-inovasi") next.push("/admin/landing-page/profile/program-inovasi")

View File

@@ -605,7 +605,7 @@ function Kepuasan() {
}} }}
/> />
<TextInput <TextInput
label="Tanggal" label="Tanggal Pengisian"
type="date" type="date"
placeholder="masukkan tanggal" placeholder="masukkan tanggal"
defaultValue={state.create.form.tanggal} defaultValue={state.create.form.tanggal}