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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user