Fix Konsisten teks di tampilan mobile dan desktop

Fix QC Kak Inno tgl 10 Des
Fix QC Kak Ayu tgl 10 Des
This commit is contained in:
2025-12-11 17:58:03 +08:00
parent 242ea86f77
commit a00481152c
43 changed files with 1725 additions and 1093 deletions

View File

@@ -1,7 +1,7 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { Box, Flex, Group, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -30,196 +30,265 @@ function Page() {
// Hasil akhir
const sisaAnggaran = totalPendapatan - totalBelanja - totalPembiayaan;
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(value);
};
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
{/* Page Title */}
<Title
ta="center"
c={colors["blue-button"]}
fw="bold"
order={1}
fz={{ base: 28, md: 36 }}
>
Pendapatan Asli Desa
</Text>
</Title>
<Box px={{ base: "md", md: 100 }}>
<Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }}>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Box
p="md"
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
height: '100%'
}}
>
<Stack gap="md">
<Title order={3} fz={{ base: 18, md: 20 }} c={colors['blue-button']}>
Pendapatan
</Title>
<Stack gap="sm">
{latestApb?.pendapatan?.map((item) => (
<Box key={item.id}>
<Flex gap={1}>
<Text
fz="md"
fz={{ base: 13, md: 14 }}
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
lh={1.4}
c="black"
style={{ wordBreak: 'break-word' }}
>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(item.value)}
{item.name} {formatCurrency(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text style={{
wordBreak: 'break-word',
whiteSpace: 'normal'
}} fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPendapatan)}
</Flex>
</Box>
))}
</Stack>
<Box
pt="sm"
mt="auto"
style={{
borderTop: `2px solid ${colors['blue-button']}`
}}
>
<Flex direction="column" gap={4}>
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
Total Pendapatan
</Text>
</GridCol>
</Grid>
<Text
fz={{ base: 18, md: 22 }}
fw={700}
c={colors['blue-button']}
lh={1.4}
>
{formatCurrency(totalPendapatan)}
</Text>
</Flex>
</Box>
</Stack>
</Box>
{/* Belanja Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
<Box
p="md"
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
height: '100%'
}}
>
<Stack gap="md">
<Title order={3} fz={{ base: 18, md: 20 }} c="orange">
Belanja
</Title>
<Stack gap="sm">
{latestApb?.belanja?.map((item) => (
<Box key={item.id}>
<Group gap={1}>
<Text
fz={{ base: 13, md: 14 }}
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
lh={1.4}
c="black"
style={{ wordBreak: 'break-word' }}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
{item.name} {formatCurrency(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="orange">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalBelanja)}
</Group>
</Box>
))}
</Stack>
<Box
pt="sm"
mt="auto"
style={{
borderTop: '2px solid orange'
}}
>
<Flex direction="column" gap={4}>
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
Total Belanja
</Text>
</GridCol>
</Grid>
<Text
fz={{ base: 18, md: 22 }}
fw={700}
c="orange"
lh={1.4}
>
{formatCurrency(totalBelanja)}
</Text>
</Flex>
</Box>
</Stack>
</Box>
{/* Pembiayaan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }} style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Text
fz="md"
<Box
p="md"
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
height: '100%'
}}
>
<Stack gap="md">
<Title order={3} fz={{ base: 18, md: 20 }} c="green">
Pembiayaan
</Title>
<Stack gap="sm">
{latestApb?.pembiayaan?.map((item) => (
<Box key={item.id}>
<Group gap={1}>
<Text
fz={{ base: 13, md: 14 }}
fw={500}
style={{
wordBreak: 'break-word',
whiteSpace: 'normal',
textAlign: 'right',
}}
lh={1.4}
c="black"
style={{ wordBreak: 'break-word' }}
>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}
{item.name} {formatCurrency(item.value)}
</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="green">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPembiayaan)}
</Group>
</Box>
))}
</Stack>
<Box
pt="sm"
mt="auto"
style={{
borderTop: '2px solid green'
}}
>
<Flex direction="column" gap={4}>
<Text fz={{ base: 14, md: 16 }} fw={600} lh={1.4}>
Total Pembiayaan
</Text>
</GridCol>
</Grid>
<Text
fz={{ base: 18, md: 22 }}
fw={700}
c="green"
lh={1.4}
>
{formatCurrency(totalPembiayaan)}
</Text>
</Flex>
</Box>
</Stack>
</Box>
</SimpleGrid>
</Paper>
{/* 🔽 Tambahan Ringkasan Anggaran */}
<Paper bg={colors['white-1']} p="xl" shadow="sm" withBorder>
<Title order={3} mb="md">Ringkasan Anggaran</Title>
{/* Ringkasan Anggaran */}
<Paper bg={colors['white-1']} p={{ base: 'md', md: 'xl' }} shadow="sm" withBorder>
<Title order={3} mb="md" fz={{ base: 18, md: 20 }}>
Ringkasan Anggaran
</Title>
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Keterangan</Table.Th>
<Table.Th ta={"right"}>Jumlah</Table.Th>
<Table.Th>
<Text fz={{ base: 13, md: 14 }} fw={600}>Keterangan</Text>
</Table.Th>
<Table.Th ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600}>Jumlah</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
<Table.Td>Total Pendapatan</Table.Td>
<Table.Td align="right">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPendapatan)}
<Table.Td>
<Text fz={{ base: 13, md: 14 }} lh={1.4}>Total Pendapatan</Text>
</Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4}>
{formatCurrency(totalPendapatan)}
</Text>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Total Belanja</Table.Td>
<Table.Td align="right" c="orange">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalBelanja)}
<Table.Td>
<Text fz={{ base: 13, md: 14 }} lh={1.4} c="orange">Total Belanja</Text>
</Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="orange">
{formatCurrency(totalBelanja)}
</Text>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>Total Pembiayaan</Table.Td>
<Table.Td align="right" c="green">
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(totalPembiayaan)}
<Table.Td>
<Text fz={{ base: 13, md: 14 }} lh={1.4} c="green">Total Pembiayaan</Text>
</Table.Td>
<Table.Td ta="right">
<Text fz={{ base: 13, md: 14 }} fw={600} lh={1.4} c="green">
{formatCurrency(totalPembiayaan)}
</Text>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td><b>Sisa Anggaran</b></Table.Td>
<Table.Td align="right" c={sisaAnggaran >= 0 ? "blue" : "red"}>
<b>
{new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(sisaAnggaran)}
</b>
<Table.Tr style={{ backgroundColor: '#f8f9fa' }}>
<Table.Td>
<Text fz={{ base: 14, md: 15 }} fw={700} lh={1.4}>Sisa Anggaran</Text>
</Table.Td>
<Table.Td ta="right">
<Text
fz={{ base: 14, md: 15 }}
fw={700}
c={sisaAnggaran >= 0 ? colors['blue-button'] : "red"}
lh={1.4}
>
{formatCurrency(sisaAnggaran)}
</Text>
</Table.Td>
</Table.Tr>
</Table.Tbody>

View File

@@ -1,7 +1,7 @@
'use client'
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
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, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from '@tabler/icons-react';
import { motion } from 'motion/react';
@@ -14,7 +14,7 @@ function Page() {
const router = useRouter()
const state = useProxy(pasarDesaState.pasarDesa)
const [search, setSearch] = useState('');
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const {
data,
@@ -28,7 +28,6 @@ function Page() {
pasarDesaState.kategoriProduk.findManyAll.load()
}, [])
// Filter data based on selected category
const filteredData = selectedCategory
? data?.filter(item =>
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
@@ -39,7 +38,6 @@ function Page() {
load(page, 4, debouncedSearch, selectedCategory || undefined)
}, [page, debouncedSearch, selectedCategory])
if (loading || !data) {
return (
<Stack py={10}>
@@ -49,21 +47,22 @@ function Page() {
}
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 }}>
<BackButton />
</Box>
<Box>
<Grid align='center' px={{ base: 'md', md: 100 }}>
<Grid align="center" px={{ base: 'md', md: 100 }}>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title order={1} c={colors["blue-button"]} fw="bold">
Pasar Desa
</Text>
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Produk'
radius="lg"
placeholder="Cari Produk"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
@@ -71,22 +70,15 @@ function Page() {
/>
</GridCol>
</Grid>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan
</Text>
<Text px={{ base: 'md', md: 100 }} ta={"justify"} fz="md" >
dan memperkenalkan produk mereka.
<Text px={{ base: 'md', md: 100 }} pt={20} ta="justify" fz={{ base: 'sm', md: 'md' }}>
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<SimpleGrid
pb={30}
cols={{
base: 1,
md: 2
}}
>
<Stack gap="lg">
<SimpleGrid pb={30} cols={{ base: 1, md: 2 }}>
<Box>
<Select
placeholder="Pilih Kategori"
@@ -103,50 +95,58 @@ function Page() {
/>
</Box>
</SimpleGrid>
<SimpleGrid cols={{ base: 1, md: 4 }}>
{filteredData?.map((v, k) => {
return (
<Stack key={k}>
<motion.div
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
<Paper p={'lg'}>
<Image
radius={'lg'}
src={v.image?.link || '/placeholder-product.jpg'}
alt={v.nama}
h={200}
w='100%'
style={{ objectFit: 'cover' }}
loading="lazy"
/>
<Text py={10} fw={'bold'} fz={'lg'}>{v.nama}</Text>
<Text fz={'md'}>Rp {v.harga.toLocaleString('id-ID')}</Text>
<Flex py={10} gap={'md'}>
<IconStarFilled size={20} color='#EBCB09' />
<Text fz={'sm'} ml={2}>{v.rating}</Text>
</Flex>
<Flex justify={'space-between'} align={'center'}>
<Box>
<Flex gap={'md'} align={'center'}>
<IconMapPinFilled size={20} color='red' />
<Text fz={'sm'} ml={2}>{v.alamatUsaha}</Text>
</Flex>
</Box>
<IconBrandWhatsapp size={20} color={colors['blue-button']} />
</Flex>
</Paper>
</motion.div>
</Stack>
)
})}
{filteredData?.map((v, k) => (
<Stack key={k}>
<motion.div
onClick={() => router.push(`/darmasaba/ekonomi/pasar-desa/${v.id}`)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
>
<Paper p="lg">
<Image
radius="lg"
src={v.image?.link || '/placeholder-product.jpg'}
alt={v.nama}
h={200}
w="100%"
style={{ objectFit: 'cover' }}
loading="lazy"
/>
<Text py="sm" fw="bold" fz={{ base: 'md', md: 'lg' }}>
{v.nama}
</Text>
<Text fz={{ base: 'sm', md: 'md' }}>
Rp {v.harga.toLocaleString('id-ID')}
</Text>
<Flex py="sm" gap="md">
<IconStarFilled size={20} color="#EBCB09" />
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
{v.rating}
</Text>
</Flex>
<Flex justify="space-between" align="center">
<Box>
<Flex gap="md" align="center">
<IconMapPinFilled size={20} color="red" />
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
{v.alamatUsaha}
</Text>
</Flex>
</Box>
<IconBrandWhatsapp size={20} color={colors['blue-button']} />
</Flex>
</Paper>
</motion.div>
</Stack>
))}
</SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
@@ -157,4 +157,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -1,7 +1,7 @@
'use client'
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
@@ -32,10 +32,9 @@ interface ProgramKemiskinanData {
function Page() {
const [search, setSearch] = useState('')
const [debouncedSearch] = useDebouncedValue(search, 1000); // 500ms delay
const [debouncedSearch] = useDebouncedValue(search, 1000);
const state = useProxy(programKemiskinanState)
// 🔧 Get valid statistics data with proper type checking
const statistikData = state.findMany.data
.filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } => {
return !!item?.statistik &&
@@ -43,11 +42,11 @@ function Page() {
item.statistik.jumlah !== undefined;
})
.map(item => ({
tahun: Number(item.statistik.tahun) || 0, // Ensure tahun is a number
jumlah: Number(item.statistik.jumlah) || 0, // Ensure jumlah is a number
tahun: Number(item.statistik.tahun) || 0,
jumlah: Number(item.statistik.jumlah) || 0,
}))
.sort((a, b) => a.tahun - b.tahun)
.filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah)); // Remove any invalid entries
.filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah));
const {
data,
@@ -74,12 +73,18 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Box px={{ base: 'md', md: 100 }}>
<Grid align='center'>
<GridCol span={{ base: 12, md: 9 }}>
<Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
<Title
order={1}
c={colors["blue-button"]}
fw={"bold"}
fz={{ base: '28px', md: '32px' }}
lh={{ base: '1.2', md: '1.25' }}
>
Program Kemiskinan
</Text>
</Title>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
@@ -92,7 +97,15 @@ function Page() {
/>
</GridCol>
</Grid>
<Text fz={'h4'}>Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat</Text>
<Text
fz={{ base: '14px', md: '16px' }}
lh={{ base: '1.5', md: '1.6' }}
c="black"
ta={{ base: 'left', md: 'left' }}
pt={20}
>
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
@@ -106,8 +119,22 @@ function Page() {
{state.findMany.data.map(v => {
return (
<Paper p={'xl'} key={v.id}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text>
<Text fz={'lg'} c={'black'} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
<Title
order={3}
fw={'bold'}
c={colors['blue-button']}
fz={{ base: '18px', md: '20px' }}
lh={{ base: '1.3', md: '1.35' }}
>
{v.nama}
</Title>
<Text
fz={{ base: '14px', md: '16px' }}
lh={{ base: '1.5', md: '1.6' }}
c={'black'}
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Paper>
)
})}
@@ -124,7 +151,16 @@ function Page() {
/>
</Center>
<Paper p={'xl'}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
<Title
order={3}
fw={'bold'}
c={colors['blue-button']}
fz={{ base: '18px', md: '20px' }}
lh={{ base: '1.3', md: '1.35' }}
mb="md"
>
Statistik Kemiskinan Masyarakat
</Title>
<Box style={{ width: '100%', height: 'auto' }}>
{statistikData.length > 0 ? (
<Box w="100%" style={{ overflowX: 'auto' }}>
@@ -162,7 +198,11 @@ function Page() {
</Box>
) : (
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
<Text c="dimmed">
<Text
fz={{ base: '12px', md: '14px' }}
c="dimmed"
lh={{ base: '1.4', md: '1.5' }}
>
{state.findMany.loading
? 'Memuat data statistik...'
: 'Belum ada data statistik yang tersedia atau data tidak valid'}
@@ -177,4 +217,4 @@ function Page() {
);
}
export default Page;
export default Page;

View File

@@ -14,6 +14,9 @@ import {
Loader,
Paper,
Stack,
Tabs,
TabsList,
TabsTab,
Text,
TextInput,
Title,
@@ -33,6 +36,8 @@ import { OrganizationChart } from 'primereact/organizationchart'
import { useEffect, useRef, useState } from 'react'
import { useProxy } from 'valtio/utils'
import BackButton from '../../desa/layanan/_com/BackButto'
import { useMediaQuery } from '@mantine/hooks'
import { useTransitionRouter } from 'next-view-transitions'
export default function Page() {
return (
@@ -51,11 +56,11 @@ export default function Page() {
order={1}
ta="center"
c={colors['blue-button']}
fz={{ base: 28, md: 36, lg: 44 }}
fz={{ base: 28, md: 36 }}
>
Struktur Organisasi & SK Pengurus BumDes
</Title>
<Text ta="center" c="black" maw={800}>
<Text ta="center" c="black" maw={800} fz={{ base: 14, md: 16 }} lh={1.6}>
Gambaran visual peran dan pengurus yang ditugaskan. Gunakan kontrol
di bawah untuk mencari, memperbesar, atau melihat lebih jelas.
</Text>
@@ -70,13 +75,14 @@ export default function Page() {
}
function StrukturOrganisasiBumDes() {
const router = useTransitionRouter()
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
const chartContainerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(1)
const [isFullscreen, setFullscreen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const debouncedSearch = useRef(
debounce((value: string) => setSearchQuery(value), 400)
debounce((value: string) => setSearchQuery(value), 1000)
).current
useEffect(() => {
@@ -92,8 +98,10 @@ function StrukturOrganisasiBumDes() {
<Center py={48}>
<Stack align="center" gap="sm">
<Loader size="lg" />
<Text fw={600}>Memuat struktur organisasi</Text>
<Text c="dimmed" size="sm">
<Text fw={600} fz={{ base: 15, md: 16 }} lh={1.4}>
Memuat struktur organisasi
</Text>
<Text c="dimmed" fz={{ base: 12, md: 14 }} lh={1.4}>
Mengambil data pengurus dan posisi. Mohon tunggu sebentar.
</Text>
</Stack>
@@ -119,10 +127,10 @@ function StrukturOrganisasiBumDes() {
<Center>
<IconUsers size={56} />
</Center>
<Title order={3} mt="md">
<Title order={3} mt="md" ta="center">
Data pengurus belum tersedia
</Title>
<Text c="dimmed" mt="xs">
<Text c="dimmed" mt="xs" fz={{ base: 12, md: 14 }} lh={1.4}>
Belum ada data pengurus yang tercatat untuk BumDes.
</Text>
<Group justify="center" mt="lg">
@@ -218,155 +226,300 @@ function StrukturOrganisasiBumDes() {
return (
<Stack align="center" mt="xl">
{/* 🧭 Kontrol atas */}
<Paper shadow="xs" p="md" radius="md" bg={colors['blue-button']}>
<Group gap="sm" wrap="wrap" justify="center">
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
<Paper
shadow="xs"
w={{
base: '100%', // Mobile: 100%
sm: '40%', // Tablet: 95%
md: '39%', // Desktop: 70%
lg: '38%', // Desktop L: 60%
xl: '37%', // 4K: 50%
'2xl': '36%', // Ultra-wide: 45%
}}
p="md"
radius="md"
style={{
background: colors['blue-button'], // ⬅️ penting
maxWidth: '100%', // ⬅️ penting
overflowX: 'auto' // ⬅️ untuk mencegah overflow
}}
>
<Stack gap="sm">
<Group justify='center'>
<TextInput
placeholder="Cari nama atau jabatan..."
leftSection={<IconSearch size={16} />}
onChange={(e) => debouncedSearch(e.target.value)}
styles={{
input: {
minWidth: 250,
},
}}
/>
</Group>
<Tabs
defaultValue="zoom-out"
variant="outline"
radius="md"
styles={{
input: {
minWidth: 250,
panel: { display: 'none' },
tab: {
color: colors['blue-button'],
backgroundColor: colors['blue-button-2'],
border: 'none',
fontWeight: 600,
fontSize: '0.875rem',
padding: '6px 12px',
minHeight: 'auto',
flexShrink: 0,
},
}}
/>
<Group gap="xs">
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
>
Zoom Out
</Button>
<Box
bg={colors['blue-button-2']}
c={colors['blue-button']}
px={16}
py={8}
style={{ width: '100%' }} // 👈 penting
>
<TabsList
style={{
fontSize: 14,
fontWeight: 700,
borderRadius: '8px',
minWidth: 70,
textAlign: 'center',
display: 'flex',
overflowX: 'auto',
overflowY: 'hidden',
gap: '4px',
paddingBottom: '4px',
flexWrap: 'nowrap',
WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'thin',
msOverflowStyle: '-ms-autohiding-scrollbar',
maxWidth: '100%',
scrollBehavior: 'smooth', // 👈 smooth scroll
}}
>
{Math.round(scale * 100)}%
</Box>
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={handleZoomIn}
leftSection={<IconZoomIn size={16} />}
>
Zoom In
</Button>
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={resetZoom}
>
Reset
</Button>
<Button
variant="light"
bg={colors['blue-button-2']}
c={colors['blue-button']}
size="sm"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
>
Fullscreen
</Button>
</Group>
</Group>
<TabsTab
value="zoom-out"
onClick={handleZoomOut}
leftSection={<IconZoomOut size={16} />}
style={{ flexShrink: 0 }}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom Out</Text>
</TabsTab>
<Box
bg={colors['blue-button-2']}
c={colors['blue-button']}
px={12}
py={6}
style={{
fontWeight: 700,
borderRadius: '6px',
minWidth: 60,
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
whiteSpace: 'nowrap',
}}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} c={colors['blue-button']}>
{Math.round(scale * 100)}%
</Text>
</Box>
<TabsTab
value="zoom-in"
onClick={handleZoomIn}
leftSection={<IconZoomIn size={16} />}
style={{ flexShrink: 0 }}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Zoom In</Text>
</TabsTab>
<TabsTab
value="reset"
onClick={resetZoom}
style={{ flexShrink: 0 }}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">Reset</Text>
</TabsTab>
<TabsTab
value="fullscreen"
onClick={toggleFullscreen}
leftSection={
isFullscreen ? (
<IconArrowsMinimize size={16} />
) : (
<IconArrowsMaximize size={16} />
)
}
style={{ flexShrink: 0 }}
>
<Text fz={{ base: 12, sm: 13 }} lh={1} ta="center">
{isFullscreen ? 'Exit' : 'Fullscreen'}
</Text>
</TabsTab>
</TabsList>
</Tabs>
</Stack>
</Paper>
{/* 📊 Chart Container */}
<Center style={{ width: '100%' }}>
<Box
ref={chartContainerRef}
style={{
overflowX: 'auto',
overflowY: 'auto',
width: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
transform: `scale(${scale})`,
transformOrigin: 'center top',
}}
>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Center>
</Stack>
)
}
function NodeCard({ node }: any) {
const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const description = node?.data?.description || ''
return (
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: 240,
padding: 20,
background:
'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
transition: 'all 0.3s ease',
}}
>
<Stack align="center" gap={10}>
<Box
style={{
width: 90,
height: 90,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
}}
>
<Image src={imageSrc} alt={name} fit="cover" loading="lazy" />
</Box>
<Text fw={700} size="sm" ta="center" c={colors['blue-button']}>
{name}
</Text>
<Text size="xs" c="dimmed" ta="center">
{title}
</Text>
<Text size="xs" c="dimmed" ta="center" lineClamp={3}>
{description || 'Belum ada deskripsi.'}
</Text>
{/* 🧩 Chart Container */}
<Center style={{ width: '100%' }}>
<Box
ref={chartContainerRef}
style={{
borderRadius: '12px',
overflowX: 'auto',
overflowY: 'auto',
width: '100%',
maxWidth: '100%',
padding: '32px 16px',
transition: 'transform 0.2s ease',
}}
>
<Box style={{
transform: `scale(${scale})`,
transformOrigin: 'center top',
display: 'inline-block', // 👈 agar tidak memenuhi lebar parent
minWidth: 'min-content', // 👈 penting agar chart tidak dipaksa muat di width 100%
}}>
<OrganizationChart
value={chartData}
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
className="p-organizationchart p-organizationchart-horizontal"
/>
</Box>
</Box>
</Center>
</Stack>
</Card>
)}
</Transition>
)
}
)
}
function NodeCard({ node, router }: any) {
const imageSrc = node?.data?.image || '/img/default.png'
const name = node?.data?.name || 'Tanpa Nama'
const title = node?.data?.title || 'Tanpa Jabatan'
const hasId = Boolean(node?.data?.id)
const isMobile = useMediaQuery("(max-width: 768px)");
return (
<Transition mounted transition="pop" duration={300}>
{(styles) => (
<Card
shadow="md"
radius="xl"
withBorder
style={{
...styles,
width: '100%',
maxWidth: isMobile ? 200 : 240, // lebih kecil di mobile
minHeight: isMobile ? 240 : 280,
padding: isMobile ? 16 : 20,
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
borderColor: 'rgba(28, 110, 164, 0.3)',
borderWidth: 2,
transition: 'all 0.3s ease',
cursor: hasId ? 'pointer' : 'default',
}}
onMouseEnter={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(-4px)'
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
}
}}
onMouseLeave={(e) => {
if (hasId) {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = ''
}
}}
>
<Stack align="center" gap={12}>
{/* Photo */}
<Box
style={{
width: 96,
height: 96,
borderRadius: '50%',
overflow: 'hidden',
border: '3px solid rgba(28, 110, 164, 0.4)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
background: 'white',
}}
>
<Image
src={imageSrc}
alt={name}
width={96}
height={96}
fit="cover"
loading="lazy"
style={{
objectFit: 'cover',
}}
/>
</Box>
{/* Name */}
<Text
fw={700}
ta="center"
c={colors['blue-button']}
lineClamp={2}
fz={{ base: 13, md: 15 }}
lh={1.2}
style={{
minHeight: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{name}
</Text>
{/* Title/Position */}
<Text
c="dimmed"
ta="center"
fw={500}
lineClamp={2}
fz={{ base: 12, md: 13 }}
lh={1.3}
style={{
minHeight: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
wordBreak: 'break-word',
}}
>
{title}
</Text>
{/* Detail Button */}
{hasId && (
<Button
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
size="xs"
fullWidth
mt={8}
radius="md"
onClick={() =>
router.push(`/darmasaba/ppid/struktur-ppid/${node.data.id}`)
}
style={{
height: 32,
fontWeight: 600,
}}
>
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
</Button>
)}
</Stack>
</Card>
)}
</Transition>
)
}