Fix QC Kak Ayu 15 Des
Fix QC Kak Inno 15 Des Fix UI User Font Size, Font Weight, Line Height Fix UI Admin Font Size, Font Weight, Line Height & UI Mobile
This commit is contained in:
@@ -123,7 +123,7 @@ export default function EditKolaborasiInovasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: "sm", md: "lg" }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors["blue-button"]} size={24} />
|
<IconArrowBack color={colors["blue-button"]} size={24} />
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function DetailSDGSDesa() {
|
|||||||
const data = sdgsState.findUnique.data;
|
const data = sdgsState.findUnique.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -54,7 +54,7 @@ function DetailSDGSDesa() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '60%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function CreateSDGsDesa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ function EditAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -215,7 +215,7 @@ function EditAPBDes() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
w={{ base: '100%', md: '100%' }}
|
w={{ base: '100%', md: '50%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function DetailAPBDes() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -77,7 +77,7 @@ function DetailAPBDes() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: '100%', md: '100%' }}
|
w={{ base: '100%', md: '70%' }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ function CreateAPBDes() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -56,71 +56,85 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'md', md: 'lg' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'md', md: 'lg' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
{/* Desktop Table */}
|
||||||
<Group justify="space-between" mb="md">
|
<Box visibleFrom="md">
|
||||||
<Title order={4}>Daftar APBDes</Title>
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
<Button
|
<Group justify="space-between" mb="md">
|
||||||
leftSection={<IconPlus size={18} />}
|
<Title order={2} size="lg" lh={1.2}>
|
||||||
color="blue"
|
Daftar APBDes
|
||||||
variant="light"
|
</Title>
|
||||||
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
<Button
|
||||||
>
|
leftSection={<IconPlus size={18} />}
|
||||||
Tambah Baru
|
color="blue"
|
||||||
</Button>
|
variant="light"
|
||||||
</Group>
|
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover miw={0}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>APBDes</TableTh>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
<TableTh style={{ width: '25%' }}>Tahun</TableTh>
|
APBDes
|
||||||
<TableTh style={{ width: '25%' }}>Dokumen</TableTh>
|
</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Aksi</TableTh>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
</TableTr>
|
Tahun
|
||||||
</TableThead>
|
</TableTh>
|
||||||
<TableTbody>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
{filteredData.length > 0 ? (
|
Dokumen
|
||||||
filteredData.map((item) => (
|
</TableTh>
|
||||||
<TableTr key={item.id}>
|
<TableTh fz="md" fw={600} ta="left" w="25%">
|
||||||
<TableTd style={{ width: '25%' }}>
|
Aksi
|
||||||
<Text fw={500} lineClamp={1}>
|
</TableTh>
|
||||||
APBDes {item.tahun}
|
</TableTr>
|
||||||
</Text>
|
</TableThead>
|
||||||
</TableTd>
|
<TableTbody>
|
||||||
<TableTd style={{ width: '25%' }}>
|
{filteredData.length > 0 ? (
|
||||||
<Text fw={500}>{item.tahun || '-'}</Text>
|
filteredData.map((item) => (
|
||||||
</TableTd>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd>
|
||||||
{item.file?.link ? (
|
<Text fz="md" fw={500} lh={1.5} lineClamp={1}>
|
||||||
<Button
|
APBDes {item.tahun}
|
||||||
component="a"
|
|
||||||
href={item.file.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
variant="light"
|
|
||||||
leftSection={<IconFile size={16} />}
|
|
||||||
size="xs"
|
|
||||||
radius="sm"
|
|
||||||
>
|
|
||||||
Lihat Dokumen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Text c="dimmed" fz="sm">
|
|
||||||
Tidak ada dokumen
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</TableTd>
|
||||||
</TableTd>
|
<TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<Text fz="md" fw={500} lh={1.5}>
|
||||||
<Box w={100}>
|
{item.tahun || '-'}
|
||||||
|
</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
|
{item.file?.link ? (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={item.file.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFile size={16} />}
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
fz="sm"
|
||||||
|
>
|
||||||
|
Lihat Dokumen
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada dokumen
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -128,29 +142,126 @@ function ListAPBDes({ search }: { search: string }) {
|
|||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={14} />}
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||||
fullWidth
|
fz="sm"
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableTr>
|
||||||
|
<TableTd colSpan={4}>
|
||||||
|
<Center py="lg">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
|
Tidak ada data APBDes yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
)}
|
||||||
) : (
|
</TableTbody>
|
||||||
<TableTr>
|
</Table>
|
||||||
<TableTd colSpan={5}>
|
</Box>
|
||||||
<Center py={20}>
|
</Paper>
|
||||||
<Text color="dimmed">Tidak ada data APBDes yang cocok</Text>
|
</Box>
|
||||||
</Center>
|
|
||||||
</TableTd>
|
|
||||||
</TableTr>
|
|
||||||
)}
|
|
||||||
</TableTbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Center mt="md">
|
{/* Mobile Cards */}
|
||||||
|
<Box hiddenFrom="md">
|
||||||
|
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={2} size="lg" lh={1.2}>
|
||||||
|
Daftar APBDes
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={18} />}
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/apbdes/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="md">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper
|
||||||
|
key={item.id}
|
||||||
|
withBorder
|
||||||
|
bg={colors['white-1']}
|
||||||
|
p="md"
|
||||||
|
shadow="sm"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
APBDes {item.tahun}
|
||||||
|
</Text>
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||||
|
Tahun
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" fw={500} lh={1.4}>
|
||||||
|
{item.tahun || '-'}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group justify="space-between" wrap="nowrap">
|
||||||
|
<Text fz="sm" c="dimmed" lh={1.4}>
|
||||||
|
Dokumen
|
||||||
|
</Text>
|
||||||
|
{item.file?.link ? (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={item.file.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFile size={14} />}
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
fz="xs"
|
||||||
|
lh={1.4}
|
||||||
|
>
|
||||||
|
Lihat
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.4}>
|
||||||
|
Tidak ada
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/apbdes/${item.id}`)}
|
||||||
|
mt="sm"
|
||||||
|
fz="xs"
|
||||||
|
lh={1.4}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Paper withBorder bg={colors['white-1']} p="md" radius="md">
|
||||||
|
<Center py="lg">
|
||||||
|
<Text c="dimmed" fz="xs" lh={1.4}>
|
||||||
|
Tidak ada data APBDes yang cocok
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Center mt="xl">
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<Box visibleFrom='md'>
|
<Box visibleFrom='md' pb={10}>
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -102,7 +102,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box hiddenFrom='md'>
|
<Box hiddenFrom='md' pb={10}>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
type="auto"
|
type="auto"
|
||||||
offsetScrollbars={false}
|
offsetScrollbars={false}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
|
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -63,6 +64,10 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
|
||||||
borderRadius: "1rem",
|
borderRadius: "1rem",
|
||||||
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
gap: "0.5rem",
|
||||||
|
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, i) => (
|
{tabs.map((tab, i) => (
|
||||||
@@ -74,6 +79,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.9rem",
|
fontSize: "0.9rem",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
|
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function EditKategoriPrestasi() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CreateKategoriPrestasi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py="md">
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
@@ -65,28 +65,33 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
{/* DESKTOP: Table */}
|
||||||
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm" withBorder>
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="xl">
|
||||||
<Title order={4} c="dark">List Kategori Prestasi</Title>
|
<Title order={2} size="lg" lh={1.2}>List Kategori Prestasi</Title>
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}>
|
<Button
|
||||||
Tambah Baru
|
leftSection={<IconPlus size={18} />}
|
||||||
</Button>
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => router.push('/admin/landing-page/prestasi-desa/kategori-prestasi-desa/create')}
|
||||||
|
>
|
||||||
|
Tambah Baru
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<Box visibleFrom="md">
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table verticalSpacing="sm" highlightOnHover>
|
<Table verticalSpacing="sm" highlightOnHover>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>Nama Kategori</TableTh>
|
<TableTh><Text fz="sm" fw={600} c="dark">Nama Kategori</Text></TableTh>
|
||||||
<TableTh style={{ width: '120px' }} ta={'center'}>Edit</TableTh>
|
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Edit</Text></TableTh>
|
||||||
<TableTh ta={'center'} style={{ width: '120px' }}>Delete</TableTh>
|
<TableTh w={120} ta="center"><Text fz="sm" fw={600} c="dark">Delete</Text></TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length === 0 ? (
|
{filteredData.length === 0 ? (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={2} style={{ textAlign: 'center' }}>
|
<TableTd colSpan={3} ta="center">
|
||||||
<Text py="md" c="dimmed">
|
<Text py="md" c="dimmed" fz="sm" lh={1.5}>
|
||||||
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||||
</Text>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
@@ -95,67 +100,129 @@ function ListKategoriPrestasi({ search }: { search: string }) {
|
|||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Box w={200}>
|
<Text truncate="end" fz="md" lh={1.5} c="dark">
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
<TableTd ta="center" w={120}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||||
>
|
>
|
||||||
<IconEdit size={18} />
|
<IconEdit size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ textAlign: 'center', width: '120px' }}>
|
<TableTd ta="center" w={120}>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedId(item.id);
|
setSelectedId(item.id);
|
||||||
setModalHapus(true);
|
setModalHapus(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconTrash size={18} />
|
<IconTrash size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Center mt="xl">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
withEdges
|
||||||
|
size="sm"
|
||||||
|
styles={{
|
||||||
|
control: {
|
||||||
|
'&[data-active]': {
|
||||||
|
background: `${colors['blue-button']} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{/* MOBILE: Card */}
|
||||||
<Center mt="lg">
|
<Box hiddenFrom="md">
|
||||||
<Pagination
|
<Stack gap="md">
|
||||||
value={page}
|
{filteredData.length === 0 ? (
|
||||||
onChange={(newPage) => load(newPage)}
|
<Paper p="lg" ta="center">
|
||||||
total={totalPages}
|
<Text c="dimmed" fz="sm" lh={1.5}>
|
||||||
withEdges
|
{search ? 'Tidak ada hasil yang cocok' : 'Belum ada data kategori'}
|
||||||
size="sm"
|
</Text>
|
||||||
styles={{
|
</Paper>
|
||||||
control: {
|
) : (
|
||||||
'&[data-active]': {
|
filteredData.map((item) => (
|
||||||
background: `${colors['blue-button']} !important`,
|
<Paper key={item.id} p="md" withBorder bg={colors['white-1']}>
|
||||||
},
|
<Stack gap="xs">
|
||||||
},
|
<Text fz="sm" lh={1.5} fw={600} c="dark">{item.name}</Text>
|
||||||
}}
|
<Group justify="flex-end" gap="xs">
|
||||||
/>
|
<Button
|
||||||
</Center>
|
variant="light"
|
||||||
)}
|
color="green"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/kategori-prestasi-desa/${item.id}`)}
|
||||||
|
>
|
||||||
|
<IconEdit size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
setModalHapus(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={14} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Center py="lg">
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
withEdges
|
||||||
|
size="xs"
|
||||||
|
styles={{
|
||||||
|
control: {
|
||||||
|
'&[data-active]': {
|
||||||
|
background: `${colors['blue-button']} !important`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Modal Konfirmasi Hapus */}
|
||||||
|
<ModalKonfirmasiHapus
|
||||||
|
opened={modalHapus}
|
||||||
|
onClose={() => setModalHapus(false)}
|
||||||
|
onConfirm={handleHapus}
|
||||||
|
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
{/* Modal Konfirmasi Hapus */}
|
</Box>
|
||||||
<ModalKonfirmasiHapus
|
|
||||||
opened={modalHapus}
|
|
||||||
onClose={() => setModalHapus(false)}
|
|
||||||
onConfirm={handleHapus}
|
|
||||||
text='Apakah anda yakin ingin menghapus kategori prestasi ini?'
|
|
||||||
/>
|
|
||||||
</Box >
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export default function EditPrestasiDesa() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function DetailPrestasiDesa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box px={{ base: 0, md: 'xs' }} py="xs">
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
@@ -53,7 +53,7 @@ function DetailPrestasiDesa() {
|
|||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
withBorder
|
withBorder
|
||||||
w={{ base: "100%", md: "60%" }}
|
w={{ base: "100%", md: "70%" }}
|
||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
p="lg"
|
p="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function CreatePrestasiDesa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -28,6 +28,7 @@ function ListPrestasiDesa() {
|
|||||||
function ListPrestasi({ search }: { search: string }) {
|
function ListPrestasi({ search }: { search: string }) {
|
||||||
const listState = useProxy(prestasiState.prestasiDesa)
|
const listState = useProxy(prestasiState.prestasiDesa)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -39,60 +40,65 @@ function ListPrestasi({ search }: { search: string }) {
|
|||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
load(page, 10, search);
|
load(page, 10, search);
|
||||||
}, [page, search]);
|
}, []);
|
||||||
|
|
||||||
const filteredData = data || []
|
const filteredData = data || []
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={{ base: 'sm', md: 'md' }}>
|
||||||
<Skeleton height={600} radius="md" />
|
<Skeleton height={600} radius="md" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={10}>
|
<Box py={{ base: 'sm', md: 'md' }}>
|
||||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
|
||||||
<Title order={4}>Daftar Prestasi Desa</Title>
|
<Title order={2} size={isMobile ? 'md' : 'lg'} lh={1.2}>
|
||||||
|
Daftar Prestasi Desa
|
||||||
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
color="blue"
|
color="blue"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
onClick={() => router.push('/admin/landing-page/prestasi-desa/list-prestasi-desa/create')}
|
||||||
|
size={isMobile ? 'xs' : 'sm'}
|
||||||
>
|
>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
|
||||||
<Table highlightOnHover>
|
{/* Desktop Table */}
|
||||||
|
<Box visibleFrom="md">
|
||||||
|
<Table highlightOnHover miw={0}>
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh style={{ width: '25%' }}>Nama Prestasi</TableTh>
|
<TableTh w="25%">Nama Prestasi</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Deskripsi</TableTh>
|
<TableTh w="25%">Deskripsi</TableTh>
|
||||||
<TableTh style={{ width: '25%' }}>Kategori</TableTh>
|
<TableTh w="25%">Kategori</TableTh>
|
||||||
<TableTh style={{ width: '25%', textAlign: 'center' }}>Aksi</TableTh>
|
<TableTh w="25%" ta="center">Aksi</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
</TableThead>
|
</TableThead>
|
||||||
<TableTbody>
|
<TableTbody>
|
||||||
{filteredData.length > 0 ? (
|
{filteredData.length > 0 ? (
|
||||||
filteredData.map((item) => (
|
filteredData.map((item) => (
|
||||||
<TableTr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd w="25%">
|
||||||
<Box w={100}>
|
<Text truncate="end" fz="md" lh={1.5}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.name}</Text>
|
{item.name}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd w="25%">
|
||||||
<Text lineClamp={1} fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
<Text lineClamp={1} fz="md" c="dimmed" lh={1.5} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%' }}>
|
<TableTd w="25%">
|
||||||
<Box w={150}>
|
<Text truncate="end" fz="md" lh={1.5}>
|
||||||
<Text truncate="end" fz={"sm"}>{item.kategori?.name || 'Tidak ada kategori'}</Text>
|
{item.kategori?.name || 'Tidak ada kategori'}
|
||||||
</Box>
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '25%', textAlign: 'center' }}>
|
<TableTd w="25%" ta="center">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -108,23 +114,63 @@ function ListPrestasi({ search }: { search: string }) {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTd colSpan={4} style={{ textAlign: 'center' }}>
|
<TableTd colSpan={4} ta="center">
|
||||||
<Text c="dimmed" py="md">Tidak ada data prestasi</Text>
|
<Text c="dimmed" py="md" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data prestasi
|
||||||
|
</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
)}
|
)}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<Stack hiddenFrom="md" gap="xs">
|
||||||
|
{filteredData.length > 0 ? (
|
||||||
|
filteredData.map((item) => (
|
||||||
|
<Paper key={item.id} withBorder p="sm" radius="sm">
|
||||||
|
<Stack gap={4}>
|
||||||
|
<Text fz="sm" fw={600} lh={1.4}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.5} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
|
||||||
|
<Text fz="xs" c="dimmed" lh={1.4}>
|
||||||
|
Kategori: {item.kategori?.name || 'Tidak ada kategori'}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" mt="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
leftSection={<IconDeviceImacCog size={14} />}
|
||||||
|
onClick={() => router.push(`/admin/landing-page/prestasi-desa/list-prestasi-desa/${item.id}`)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Center py="md">
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||||
|
Tidak ada data prestasi
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Center mt="lg">
|
<Center mt={{ base: 'md', md: 'lg' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={load}
|
onChange={load}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
withEdges
|
withEdges
|
||||||
size="sm"
|
size={isMobile ? 'xs' : 'sm'}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
keepMounted={false}
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{/* ✅ Scroll horizontal wrapper */}
|
{/* ✅ Scroll horizontal wrapper */}
|
||||||
<Box visibleFrom='md'>
|
<Box visibleFrom='md' pb={10}>
|
||||||
<ScrollArea type="auto" offsetScrollbars>
|
<ScrollArea type="auto" offsetScrollbars>
|
||||||
<TabsList
|
<TabsList
|
||||||
p="sm"
|
p="sm"
|
||||||
@@ -108,7 +108,7 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box hiddenFrom='md'>
|
<Box hiddenFrom='md' pb={10}>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
type="auto"
|
type="auto"
|
||||||
offsetScrollbars={false}
|
offsetScrollbars={false}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const [opened, { toggle }] = useDisclosure();
|
const [opened, { toggle, close }] = useDisclosure(); // ✅ Tambahkan 'close'
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
@@ -45,21 +45,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/auth/me', {
|
||||||
credentials: 'include' // ✅ ADD credentials
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
// ✅ Check if user is NOT active → redirect to waiting room
|
|
||||||
if (!data.user.isActive) {
|
if (!data.user.isActive) {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
router.replace('/waiting-room');
|
router.replace('/waiting-room');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Fetch menuIds
|
|
||||||
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
const menuRes = await fetch(`/api/admin/user-menu-access?userId=${data.user.id}`, {
|
||||||
credentials: 'include' // ✅ ADD credentials
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const menuData = await menuRes.json();
|
const menuData = await menuRes.json();
|
||||||
|
|
||||||
@@ -67,7 +65,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
? [...menuData.menuIds]
|
? [...menuData.menuIds]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ✅ Set user dengan menuIds yang fresh
|
|
||||||
authStore.setUser({
|
authStore.setUser({
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
@@ -76,7 +73,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
isActive: data.user.isActive
|
isActive: data.user.isActive
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ IMPROVED: Redirect ONLY if di root /admin
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
if (currentPath === '/admin') {
|
if (currentPath === '/admin') {
|
||||||
@@ -84,7 +80,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
console.log('🔄 Redirecting from /admin to:', expectedPath);
|
||||||
router.replace(expectedPath);
|
router.replace(expectedPath);
|
||||||
}
|
}
|
||||||
// ✅ Jangan redirect jika user sudah di path yang valid
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
authStore.setUser(null);
|
authStore.setUser(null);
|
||||||
@@ -100,17 +95,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUser();
|
fetchUser();
|
||||||
}, [router]); // ✅ Only depend on router
|
}, [router]);
|
||||||
|
|
||||||
const getRedirectPath = (roleId: number): string => {
|
const getRedirectPath = (roleId: number): string => {
|
||||||
switch (roleId) {
|
switch (roleId) {
|
||||||
case 0: // DEVELOPER
|
case 0:
|
||||||
case 1: // SUPERADMIN
|
case 1:
|
||||||
case 2: // ADMIN_DESA
|
case 2:
|
||||||
return '/admin/landing-page/profil/program-inovasi';
|
return '/admin/landing-page/profil/program-inovasi';
|
||||||
case 3: // ADMIN_KESEHATAN
|
case 3:
|
||||||
return '/admin/kesehatan/posyandu';
|
return '/admin/kesehatan/posyandu';
|
||||||
case 4: // ADMIN_PENDIDIKAN
|
case 4:
|
||||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
default:
|
default:
|
||||||
return '/admin';
|
return '/admin';
|
||||||
@@ -139,7 +134,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const response = await fetch('/api/auth/logout', {
|
const response = await fetch('/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include' // ✅ ADD credentials
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -163,6 +158,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Handler untuk menutup mobile menu saat navigasi
|
||||||
|
const handleNavClick = (path: string) => {
|
||||||
|
router.push(path);
|
||||||
|
close(); // Tutup mobile menu
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
@@ -177,7 +178,6 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
padding="md"
|
padding="md"
|
||||||
>
|
>
|
||||||
{/* ... rest of your JSX (Header, Navbar, Main) sama seperti sebelumnya ... */}
|
|
||||||
<AppShellHeader
|
<AppShellHeader
|
||||||
style={{
|
style={{
|
||||||
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
background: "linear-gradient(90deg, #ffffff, #f9fbff)",
|
||||||
@@ -230,16 +230,48 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
|
|
||||||
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
<AppShellNavbar component={ScrollArea} style={{ background: "#ffffff", borderRight: `1px solid ${colors["blue-button"]}20` }} p={{ base: 'xs', sm: 'sm' }}>
|
||||||
{/* ... Navbar content sama seperti sebelumnya ... */}
|
|
||||||
<AppShell.Section p="sm">
|
<AppShell.Section p="sm">
|
||||||
{currentNav.map((v, k) => {
|
{currentNav.map((v, k) => {
|
||||||
const isParentActive = segments.includes(_.lowerCase(v.name));
|
const isParentActive = segments.includes(_.lowerCase(v.name));
|
||||||
return (
|
return (
|
||||||
<NavLink key={k} defaultOpened={isParentActive} c={isParentActive ? colors["blue-button"] : "gray"} label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>} style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }} styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }} variant="light" active={isParentActive}>
|
<NavLink
|
||||||
|
key={k}
|
||||||
|
defaultOpened={isParentActive}
|
||||||
|
c={isParentActive ? colors["blue-button"] : "gray"}
|
||||||
|
label={<Text fw={isParentActive ? 600 : 400} fz="sm">{v.name}</Text>}
|
||||||
|
style={{ borderRadius: rem(10), marginBottom: rem(4), transition: "background 150ms ease" }}
|
||||||
|
styles={{ root: { '&:hover': { backgroundColor: 'rgba(25, 113, 194, 0.05)' } } }}
|
||||||
|
variant="light"
|
||||||
|
active={isParentActive}
|
||||||
|
>
|
||||||
{v.children.map((child, key) => {
|
{v.children.map((child, key) => {
|
||||||
const isChildActive = segments.includes(_.lowerCase(child.name));
|
const isChildActive = segments.includes(_.lowerCase(child.name));
|
||||||
return (
|
return (
|
||||||
<NavLink key={key} href={child.path} c={isChildActive ? colors["blue-button"] : "gray"} label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>} styles={{ root: { borderRadius: rem(8), marginBottom: rem(2), transition: 'background 150ms ease', padding: '6px 12px', '&:hover': { backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)' }, ...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' }) } }} active={isChildActive} component={Link} />
|
<NavLink
|
||||||
|
key={key}
|
||||||
|
// ✅ PERBAIKAN: Gunakan onClick untuk handle navigasi dan close menu
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNavClick(child.path);
|
||||||
|
}}
|
||||||
|
href={child.path}
|
||||||
|
c={isChildActive ? colors["blue-button"] : "gray"}
|
||||||
|
label={<Text fw={isChildActive ? 600 : 400} fz="sm">{child.name}</Text>}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: rem(8),
|
||||||
|
marginBottom: rem(2),
|
||||||
|
transition: 'background 150ms ease',
|
||||||
|
padding: '6px 12px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: isChildActive ? 'rgba(25, 113, 194, 0.15)' : 'rgba(25, 113, 194, 0.05)'
|
||||||
|
},
|
||||||
|
...(isChildActive && { backgroundColor: 'rgba(25, 113, 194, 0.1)' })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
active={isChildActive}
|
||||||
|
component={Link}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default async function grafikJumlahPendudukMiskinFindMany(
|
|||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limit,
|
take: limit,
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { year: "asc" },
|
||||||
}),
|
}),
|
||||||
prisma.grafikJumlahPendudukMiskin.count({
|
prisma.grafikJumlahPendudukMiskin.count({
|
||||||
where,
|
where,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton } from '@mantine/core';
|
import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton, Title } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
import { BarChart } from '@mantine/charts';
|
import { BarChart } from '@mantine/charts';
|
||||||
@@ -32,23 +32,47 @@ function Page() {
|
|||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} >
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Title
|
||||||
|
order={1}
|
||||||
|
ta="center"
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
fw="bold"
|
||||||
|
lh={1.2}
|
||||||
|
style={{ lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
Demografi Pekerjaan
|
Demografi Pekerjaan
|
||||||
|
</Title>
|
||||||
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.5}
|
||||||
|
c="black"
|
||||||
|
style={{ lineHeight: 1.5 }}
|
||||||
|
>
|
||||||
|
Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta={'center'} fz={'h4'}>Desa Darmasaba memiliki komposisi penduduk yang beragam dalam sektor pekerjaan</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
<Paper p={'xl'}>
|
<Paper p={'xl'}>
|
||||||
<Box style={{overflowX: 'scroll'}}>
|
<Box style={{ overflowX: 'auto' }} w={"100%"}>
|
||||||
<Text pb={5} fw={'bold'} fz={'h4'}>Statistik Demografi Pekerjaan Di Desa Darmasaba</Text>
|
<Text
|
||||||
|
pb={5}
|
||||||
|
fw={'bold'}
|
||||||
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
|
lh={1.2}
|
||||||
|
c="black"
|
||||||
|
style={{ lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
|
Statistik Demografi Pekerjaan Di Desa Darmasaba
|
||||||
|
</Text>
|
||||||
<BarChart
|
<BarChart
|
||||||
type='stacked'
|
type='stacked'
|
||||||
p={10}
|
p={10}
|
||||||
mb={50}
|
mb={50}
|
||||||
h={400}
|
h={400}
|
||||||
w={Math.max(data.length * 120, 800)} // auto lebar sesuai jumlah data
|
w={Math.max(data.length * 120, 800)}
|
||||||
data={data.map((item) => ({
|
data={data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
Pekerjaan: item.pekerjaan,
|
Pekerjaan: item.pekerjaan,
|
||||||
@@ -62,28 +86,45 @@ function Page() {
|
|||||||
]}
|
]}
|
||||||
tickLine="y"
|
tickLine="y"
|
||||||
xAxisProps={{
|
xAxisProps={{
|
||||||
angle: -45, // Rotate labels by -45 degrees
|
angle: -45,
|
||||||
textAnchor: 'end', // Anchor text to the end for better alignment
|
textAnchor: 'end',
|
||||||
height: 100, // Increase height for rotated labels
|
height: 100,
|
||||||
interval: 0, // Show all labels
|
interval: 0,
|
||||||
style: {
|
style: {
|
||||||
fontSize: '12px', // Adjust font size if needed
|
fontSize: '12px',
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap',
|
||||||
|
lineHeight: 1.4,
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{base: 7, md: 5}} align={'center'}>
|
<Flex gap={{ base: 7, md: 5 }} align={'center'}>
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Laki-Laki</Text>
|
<Text
|
||||||
|
fw={'bold'}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.2}
|
||||||
|
c="black"
|
||||||
|
style={{ lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
|
Laki-Laki
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#5082EE" size={30} />
|
<ColorSwatch color="#5082EE" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{base: 7, md: 5}} align={'center'}>
|
<Flex gap={{ base: 7, md: 5 }} align={'center'}>
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Perempuan</Text>
|
<Text
|
||||||
|
fw={'bold'}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.2}
|
||||||
|
c="black"
|
||||||
|
style={{ lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
|
Perempuan
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#6EDF9C" size={30} />
|
<ColorSwatch color="#6EDF9C" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
|
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { BarChart } from '@mantine/charts';
|
import { BarChart } from '@mantine/charts';
|
||||||
import { Box, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Paper, Skeleton, Stack, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -17,13 +17,10 @@ function Page() {
|
|||||||
const state = useProxy(jumlahPendudukMiskin)
|
const state = useProxy(jumlahPendudukMiskin)
|
||||||
const [chartData, setChartData] = useState<JPMGrafik[]>([])
|
const [chartData, setChartData] = useState<JPMGrafik[]>([])
|
||||||
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
state.findMany.load()
|
state.findMany.load()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.findMany.data) {
|
if (state.findMany.data) {
|
||||||
setChartData(state.findMany.data.map((item) => ({
|
setChartData(state.findMany.data.map((item) => ({
|
||||||
@@ -48,20 +45,30 @@ function Page() {
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} >
|
<Box px={{ base: 'md', md: 100 }} >
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Title
|
||||||
|
order={1}
|
||||||
|
ta={"center"}
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
fw={"bold"}
|
||||||
|
lh={1.1}
|
||||||
|
>
|
||||||
Jumlah Penduduk Miskin
|
Jumlah Penduduk Miskin
|
||||||
</Text>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
<Paper p={'xl'}>
|
<Paper p={'xl'}>
|
||||||
<Text fz={'h3'}>Jumlah Data Penduduk Miskin</Text>
|
<Title order={3} fw={'normal'} lh={1.1}>
|
||||||
<Text fw={"bold"} fz={'h1'}>
|
Jumlah Data Penduduk Miskin
|
||||||
|
</Title>
|
||||||
|
<Title order={2} fw={"bold"} lh={1.1}>
|
||||||
{state.findMany.data?.reduce((sum, item) => sum + (Number(item.totalPoorPopulation) || 0), 0).toLocaleString()} Orang
|
{state.findMany.data?.reduce((sum, item) => sum + (Number(item.totalPoorPopulation) || 0), 0).toLocaleString()} Orang
|
||||||
</Text>
|
</Title>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper p={'xl'}>
|
<Paper p={'xl'}>
|
||||||
<Text pb={10} fw={'bold'} fz={'h4'}>Jumlah Penduduk Miskin Per Tahun</Text>
|
<Title order={3} pb={10} fw={'bold'} lh={1.1}>
|
||||||
|
Jumlah Penduduk Miskin Per Tahun
|
||||||
|
</Title>
|
||||||
<BarChart
|
<BarChart
|
||||||
h={300}
|
h={300}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
|
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { PieChart } from '@mantine/charts';
|
import { PieChart } from '@mantine/charts';
|
||||||
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -56,7 +56,7 @@ function Page() {
|
|||||||
}
|
}
|
||||||
}, [stateGrafikNganggurPendidikan.findMany.data])
|
}, [stateGrafikNganggurPendidikan.findMany.data])
|
||||||
|
|
||||||
if (!stateGrafikNganggur.findMany.data) {
|
if (!stateGrafikNganggur.findMany.data || !stateGrafikNganggurPendidikan.findMany.data) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
@@ -64,114 +64,151 @@ function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stateGrafikNganggur.findMany.data) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Skeleton h={500} />
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22" style={{ overflow: 'auto' }}>
|
||||||
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 50, lg: 100 }} >
|
<Box px={{ base: 'md', md: 50, lg: 100 }}>
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Title
|
||||||
|
order={1}
|
||||||
|
ta="center"
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
fw="bold"
|
||||||
|
style={{ lineHeight: 1.15 }}
|
||||||
|
>
|
||||||
Jumlah Penduduk Usia Kerja Yang Menganggur
|
Jumlah Penduduk Usia Kerja Yang Menganggur
|
||||||
</Text>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 50, lg: 100 }}>
|
<Box px={{ base: "md", md: 50, lg: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
<Paper p={'lg'}>
|
<Paper p={'lg'}>
|
||||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
|
<Title
|
||||||
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
order={2}
|
||||||
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
|
fw="bold"
|
||||||
<PieChart
|
style={{ lineHeight: 1.2 }}
|
||||||
w="100%"
|
>
|
||||||
h={250} // lebih kecil biar aman di mobile
|
Pengangguran Berdasarkan Usia
|
||||||
withLabelsLine
|
</Title>
|
||||||
labelsPosition="outside"
|
{mounted && donutGrafikNganggurData.length > 0 ? (
|
||||||
labelsType="percent"
|
<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
|
||||||
withLabels
|
<Box w="100%" maw={{ base: '100%', md: 400 }} mx="auto">
|
||||||
data={donutGrafikNganggurData}
|
<PieChart
|
||||||
withTooltip
|
w="100%"
|
||||||
tooltipDataSource="segment"
|
h={250}
|
||||||
/>
|
withLabelsLine
|
||||||
|
labelsPosition="outside"
|
||||||
|
labelsType="percent"
|
||||||
|
withLabels
|
||||||
|
data={donutGrafikNganggurData}
|
||||||
|
withTooltip
|
||||||
|
tooltipDataSource="segment"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>) : <Skeleton h={500} />}
|
) : (
|
||||||
|
<Skeleton h={500} />
|
||||||
|
)}
|
||||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
|
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>18-25</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
18-25
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#4b6Ef5" size={30} />
|
<ColorSwatch color="#4b6Ef5" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>26-35</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
26-35
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#14b885" size={30} />
|
<ColorSwatch color="#14b885" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>36-45</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
36-45
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#E6A03B" size={30} />
|
<ColorSwatch color="#E6A03B" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>46+</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
46+
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#DB524D" size={30} />
|
<ColorSwatch color="#DB524D" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper p={'lg'}>
|
<Paper p={'lg'}>
|
||||||
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Pendidikan</Text>
|
<Title
|
||||||
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (<Center>
|
order={2}
|
||||||
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
fw="bold"
|
||||||
<PieChart
|
style={{ lineHeight: 1.2 }}
|
||||||
w="100%"
|
>
|
||||||
h="min(250px, 50vh)" // lebih kecil biar aman di mobile
|
Pengangguran Berdasarkan Pendidikan
|
||||||
withLabelsLine
|
</Title>
|
||||||
labelsPosition="outside"
|
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (
|
||||||
labelsType="percent"
|
<Center>
|
||||||
withLabels
|
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
|
||||||
data={donutGrafikNganggurDataPendidikan}
|
<PieChart
|
||||||
withTooltip
|
w="100%"
|
||||||
tooltipDataSource="segment"
|
h="min(250px, 50vh)"
|
||||||
/>
|
withLabelsLine
|
||||||
</Box>
|
labelsPosition="outside"
|
||||||
</Center>) : <Skeleton h={500} />}
|
labelsType="percent"
|
||||||
|
withLabels
|
||||||
|
data={donutGrafikNganggurDataPendidikan}
|
||||||
|
withTooltip
|
||||||
|
tooltipDataSource="segment"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Skeleton h={500} />
|
||||||
|
)}
|
||||||
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
|
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SD</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
SD
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#4b6Ef5" size={30} />
|
<ColorSwatch color="#4b6Ef5" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMP</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
SMP
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#14b885" size={30} />
|
<ColorSwatch color="#14b885" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMA/SMK</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
SMA/SMK
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#E6A03B" size={30} />
|
<ColorSwatch color="#E6A03B" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>D3</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
D3
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#DB524D" size={30} />
|
<ColorSwatch color="#DB524D" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>S1</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} style={{ lineHeight: 1.45 }}>
|
||||||
|
S1
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#1018A8FF" size={30} />
|
<ColorSwatch color="#1018A8FF" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ function Page() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
if (state.findMany.data) {
|
if (state.findMany.data) {
|
||||||
// Set chart data
|
|
||||||
setChartData(state.findMany.data.map((item) => ({
|
setChartData(state.findMany.data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
bulan: item.month,
|
bulan: item.month,
|
||||||
@@ -44,7 +43,6 @@ function Page() {
|
|||||||
takberpendidikan: Number(item.uneducatedUnemployment),
|
takberpendidikan: Number(item.uneducatedUnemployment),
|
||||||
})));
|
})));
|
||||||
|
|
||||||
// Calculate yearly totals
|
|
||||||
const currentYearData = state.findMany.data.filter(item => item.year === currentYear);
|
const currentYearData = state.findMany.data.filter(item => item.year === currentYear);
|
||||||
if (currentYearData.length > 0) {
|
if (currentYearData.length > 0) {
|
||||||
const yearlyTotal = {
|
const yearlyTotal = {
|
||||||
@@ -72,30 +70,37 @@ function Page() {
|
|||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} >
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Title
|
||||||
|
order={1}
|
||||||
|
ta={"center"}
|
||||||
|
c={colors["blue-button"]}
|
||||||
|
fw={"bold"}
|
||||||
|
lh={1.2}
|
||||||
|
>
|
||||||
Jumlah Pengangguran
|
Jumlah Pengangguran
|
||||||
</Text>
|
</Title>
|
||||||
<Group py={20} align='center' justify='space-between'>
|
<Group py={20} align='center' justify='space-between'>
|
||||||
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
|
<Title order={2} fw={"bold"} lh={1.2}>
|
||||||
|
DATA PENGANGGURAN DESA
|
||||||
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify='center'>
|
||||||
<SimpleGrid
|
<SimpleGrid cols={1} pb={20}>
|
||||||
cols={1}
|
|
||||||
pb={20}
|
|
||||||
>
|
|
||||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||||
{/* Total Unemployment Card */}
|
{/* Total Unemployment Card */}
|
||||||
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
||||||
<Flex direction="column" gap="md">
|
<Flex direction="column" gap="md">
|
||||||
<IconUserOff size={35} color={colors['blue-button']} />
|
<IconUserOff size={35} color={colors['blue-button']} />
|
||||||
<Text fz="h4" fw={600}>Total Pengangguran</Text>
|
<Title order={3} fw={600} lh={1.2}>
|
||||||
<Text fz="h2" fw={700} c={colors['blue-button']}>
|
Total Pengangguran
|
||||||
|
</Title>
|
||||||
|
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c={colors['blue-button']} lh={1.2}>
|
||||||
{yearlyData?.total.toLocaleString() || 0} Orang
|
{yearlyData?.total.toLocaleString() || 0} Orang
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
|
||||||
Total data tahun {currentYear}
|
Total data tahun {currentYear}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -105,11 +110,13 @@ function Page() {
|
|||||||
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
||||||
<Flex direction="column" gap="md">
|
<Flex direction="column" gap="md">
|
||||||
<IconSchool size={35} color="#5082EE" />
|
<IconSchool size={35} color="#5082EE" />
|
||||||
<Text fz="h4" fw={600}>Pengangguran Terdidik</Text>
|
<Title order={3} fw={600} lh={1.2}>
|
||||||
<Text fz="h2" fw={700} c="#5082EE">
|
Pengangguran Terdidik
|
||||||
|
</Title>
|
||||||
|
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c="#5082EE" lh={1.2}>
|
||||||
{yearlyData?.educated.toLocaleString() || 0} Orang
|
{yearlyData?.educated.toLocaleString() || 0} Orang
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
|
||||||
{yearlyData ?
|
{yearlyData ?
|
||||||
<>
|
<>
|
||||||
{((yearlyData.educated / yearlyData.total) * 100).toFixed(1)}%
|
{((yearlyData.educated / yearlyData.total) * 100).toFixed(1)}%
|
||||||
@@ -123,11 +130,13 @@ function Page() {
|
|||||||
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
|
||||||
<Flex direction="column" gap="md">
|
<Flex direction="column" gap="md">
|
||||||
<IconSchoolOff size={35} color="#DA524C" />
|
<IconSchoolOff size={35} color="#DA524C" />
|
||||||
<Text fz="h4" fw={600}>Pengangguran Tidak Terdidik</Text>
|
<Title order={3} fw={600} lh={1.2}>
|
||||||
<Text fz="h2" fw={700} c="#DA524C">
|
Pengangguran Tidak Terdidik
|
||||||
|
</Title>
|
||||||
|
<Text fz={{ base: 'lg', md: 'xl' }} fw={700} c="#DA524C" lh={1.2}>
|
||||||
{yearlyData?.uneducated.toLocaleString() || 0} Orang
|
{yearlyData?.uneducated.toLocaleString() || 0} Orang
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="sm" c="dimmed">
|
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.4}>
|
||||||
{yearlyData ?
|
{yearlyData ?
|
||||||
<>
|
<>
|
||||||
{((yearlyData.uneducated / yearlyData.total) * 100).toFixed(1)}%
|
{((yearlyData.uneducated / yearlyData.total) * 100).toFixed(1)}%
|
||||||
@@ -142,13 +151,17 @@ function Page() {
|
|||||||
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
|
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Berpendidikan</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
Pengangguran Berpendidikan
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#5082EE" size={30} />
|
<ColorSwatch color="#5082EE" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
|
||||||
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Tak Berpendidikan</Text>
|
<Text fw={'bold'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
Pengangguran Tak Berpendidikan
|
||||||
|
</Text>
|
||||||
<ColorSwatch color="#DA524C" size={30} />
|
<ColorSwatch color="#DA524C" size={30} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -156,15 +169,24 @@ function Page() {
|
|||||||
{!mounted || chartData.length === 0 ? (
|
{!mounted || chartData.length === 0 ? (
|
||||||
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
|
<Title order={3} pb={10} lh={1.2}>
|
||||||
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
|
Data Pengangguran Terdidik dan Tidak Terdidik
|
||||||
|
</Title>
|
||||||
|
<Text c='dimmed' fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
|
||||||
|
Belum ada data untuk ditampilkan dalam grafik
|
||||||
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
|
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
|
||||||
<Paper bg={colors['white-1']} p={'md'}>
|
<Paper bg={colors['white-1']} p={'md'}>
|
||||||
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
|
<Title order={3} pb={10} lh={1.2}>
|
||||||
<Box w={{ base: '100%', md: '70%' }}>
|
Data Pengangguran Terdidik dan Tidak Terdidik
|
||||||
|
</Title>
|
||||||
|
<Box
|
||||||
|
w={{ base: '100%', md: '70%' }}
|
||||||
|
style={{ overflowX: "auto" }}
|
||||||
|
>
|
||||||
<BarChart
|
<BarChart
|
||||||
h={450}
|
h={450}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
@@ -178,32 +200,55 @@ function Page() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Paper>
|
</Paper>
|
||||||
<Paper p={'lg'}>
|
<Paper p={'lg'}>
|
||||||
<Text fw={'bold'} fz={'h4'}>Detail Data Pengangguran</Text>
|
<Title order={2} fw={'bold'} fz={{ base: 'md', md: 'lg' }} lh={1.2}>
|
||||||
<Table striped highlightOnHover>
|
Detail Data Pengangguran
|
||||||
<TableThead>
|
</Title>
|
||||||
<TableTr>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<TableTh ta={'center'}>Bulan</TableTh>
|
<Table striped highlightOnHover>
|
||||||
<TableTh ta={'center'}>Total</TableTh>
|
<TableThead>
|
||||||
<TableTh ta={'center'}>Terdidik</TableTh>
|
<TableTr>
|
||||||
<TableTh ta={'center'}>Tidak Terdidik</TableTh>
|
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
<TableTh ta={'center'}>Perubahan</TableTh>
|
Bulan
|
||||||
</TableTr>
|
</TableTh>
|
||||||
</TableThead>
|
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
<TableTbody>
|
Total
|
||||||
{state.findMany.data?.map((item, index) => (
|
</TableTh>
|
||||||
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
|
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
<TableTd ta={'center'}>{item.month}</TableTd>
|
Terdidik
|
||||||
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
|
</TableTh>
|
||||||
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
|
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
|
Tidak Terdidik
|
||||||
<TableTd ta={'center'}>{item.percentageChange}%</TableTd>
|
</TableTh>
|
||||||
|
<TableTh ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
Perubahan
|
||||||
|
</TableTh>
|
||||||
</TableTr>
|
</TableTr>
|
||||||
))}
|
</TableThead>
|
||||||
</TableTbody>
|
<TableTbody>
|
||||||
</Table>
|
{state.findMany.data?.map((item, index) => (
|
||||||
|
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
|
||||||
|
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
{item.month}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
{item.totalUnemployment}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
{item.educatedUnemployment}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
{item.uneducatedUnemployment}
|
||||||
|
</TableTd>
|
||||||
|
<TableTd ta={'center'} fz={{ base: 'sm', md: 'md' }} lh={1.45}>
|
||||||
|
{item.percentageChange}%
|
||||||
|
</TableTd>
|
||||||
|
</TableTr>
|
||||||
|
))}
|
||||||
|
</TableTbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Button, Center, Group, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
|
import { IconArrowBack, IconBrandWhatsapp, IconBriefcase, IconCurrencyDollar, IconMapPin, IconPhone } from '@tabler/icons-react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
@@ -33,18 +33,25 @@ function DetailLowonganKerjaUser() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatRupiah = (value: number) =>
|
||||||
|
new Intl.NumberFormat("id-ID", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "IDR",
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
|
<Stack bg={colors.Bg} py="xl" px={{ base: 'md', md: 100 }} align="center">
|
||||||
<Box w={{ base: '100%', md: '70%' }}>
|
<Box w={{ base: '100%', md: '70%' }}>
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconArrowBack size={20} />}
|
leftSection={<IconArrowBack size={20} />}
|
||||||
mb="md"
|
mb="md"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
radius="lg"
|
radius="lg"
|
||||||
@@ -54,11 +61,17 @@ function DetailLowonganKerjaUser() {
|
|||||||
bg={colors['white-1']}
|
bg={colors['white-1']}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
{/* Judul */}
|
{/* Judul Posisi - H1 */}
|
||||||
<Text fz={{ base: '1.6rem', md: '2rem' }} fw={700} c={colors['blue-button']}>
|
<Title
|
||||||
|
order={1}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
style={{ lineHeight: 1.15 }}
|
||||||
|
>
|
||||||
{data.posisi}
|
{data.posisi}
|
||||||
</Text>
|
</Title>
|
||||||
<Text c="dimmed" fz="sm">
|
|
||||||
|
{/* Tanggal Posting - Caption */}
|
||||||
|
<Text c="dimmed" fz={{ base: 12, md: 'sm' }} lh={1.4}>
|
||||||
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
|
Diposting: {new Date(data.createdAt).toLocaleDateString('id-ID', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@@ -70,44 +83,72 @@ function DetailLowonganKerjaUser() {
|
|||||||
<Stack gap="sm" mt="md">
|
<Stack gap="sm" mt="md">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconBriefcase size={20} color={colors['blue-button']} />
|
<IconBriefcase size={20} color={colors['blue-button']} />
|
||||||
<Text fz="md" fw={600}>{data.namaPerusahaan}</Text>
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
fw={600}
|
||||||
|
lh={1.5}
|
||||||
|
>
|
||||||
|
{data.namaPerusahaan}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconMapPin size={20} color={colors['blue-button']} />
|
<IconMapPin size={20} color={colors['blue-button']} />
|
||||||
<Text fz="md">{data.lokasi}</Text>
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.5}
|
||||||
|
>
|
||||||
|
{data.lokasi}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconPhone size={20} color={colors['blue-button']} />
|
<IconPhone size={20} color={colors['blue-button']} />
|
||||||
<Text fz="md">{data.notelp}</Text>
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.5}
|
||||||
|
>
|
||||||
|
{data.notelp}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconCurrencyDollar size={20} color={colors['blue-button']} />
|
<IconCurrencyDollar size={20} color={colors['blue-button']} />
|
||||||
<Text fz="md">{data.gaji || '-'}</Text>
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.5}
|
||||||
|
>
|
||||||
|
{formatRupiah(Number(data.gaji)) || '-'}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconBriefcase size={20} color={colors['blue-button']} />
|
<IconBriefcase size={20} color={colors['blue-button']} />
|
||||||
<Text fz="md">{data.tipePekerjaan}</Text>
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.5}
|
||||||
|
>
|
||||||
|
{data.tipePekerjaan}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
{/* Deskripsi Pekerjaan - H2 */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={600} fz="lg" mb={4}>
|
<Title order={2} mb={8} style={{ lineHeight: 1.2 }}>
|
||||||
Deskripsi Pekerjaan
|
Deskripsi Pekerjaan
|
||||||
</Text>
|
</Title>
|
||||||
<Text
|
<Text
|
||||||
fz="sm"
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
lh={1.6}
|
lh={1.6}
|
||||||
style={{ wordBreak: 'break-word' }}
|
style={{ wordBreak: 'break-word' }}
|
||||||
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Kualifikasi - H2 */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={600} fz="lg" mb={4}>
|
<Title order={2} mb={8} style={{ lineHeight: 1.2 }}>
|
||||||
Kualifikasi
|
Kualifikasi
|
||||||
</Text>
|
</Title>
|
||||||
<Text
|
<Text
|
||||||
fz="sm"
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
lh={1.6}
|
lh={1.6}
|
||||||
style={{ wordBreak: 'break-word' }}
|
style={{ wordBreak: 'break-word' }}
|
||||||
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}
|
dangerouslySetInnerHTML={{ __html: data.kualifikasi || '-' }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
|
import { Box, Button, Center, Flex, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { IconBriefcase, IconClock, IconMapPin, IconSearch } from '@tabler/icons-react';
|
import { IconBriefcase, IconClock, IconMapPin, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -53,17 +53,19 @@ function Page() {
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} pb={80}>
|
<Box px={{ base: 'md', md: 100 }} pb={80}>
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Title order={1} ta="center" c={colors["blue-button"]} fw="bold" lh={1.15}>
|
||||||
Lowongan Kerja Lokal
|
Lowongan Kerja Lokal
|
||||||
</Text>
|
</Title>
|
||||||
<Group justify='center'>
|
<Group justify='center'>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius={'xl'}
|
radius={'xl'}
|
||||||
w={{ base: 500, md: 700 }}
|
w={{ base: '100%', md: 700 }}
|
||||||
placeholder='Cari Pekerjaan'
|
placeholder='Cari Pekerjaan'
|
||||||
leftSection={<IconSearch size={20} />}
|
leftSection={<IconSearch size={20} />}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={1.5}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -80,30 +82,42 @@ function Page() {
|
|||||||
<Paper key={k} p={'xl'}>
|
<Paper key={k} p={'xl'}>
|
||||||
<Stack gap={'md'}>
|
<Stack gap={'md'}>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={'xl'} align={'center'}>
|
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
|
||||||
<IconBriefcase color={colors['blue-button']} size={50} />
|
<IconBriefcase color={colors['blue-button']} size={40} />
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>{v.posisi}</Text>
|
<Text fw={'bold'} fz={{ base: 'lg', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
|
||||||
<Text fz={'h4'}>{v.namaPerusahaan}</Text>
|
{v.posisi}
|
||||||
|
</Text>
|
||||||
|
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
|
||||||
|
{v.namaPerusahaan}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={'xl'} align={'center'}>
|
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
|
||||||
<IconMapPin color={colors['blue-button']} size={50} />
|
<IconMapPin color={colors['blue-button']} size={40} />
|
||||||
<Text fz={'h4'}>{v.lokasi}</Text>
|
<Text fz={{ base: 'md', md: 'h4' }} lh={1.5}>
|
||||||
|
{v.lokasi}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex gap={'xl'} align={'center'}>
|
<Flex gap={{ base: 'md', md: 'xl' }} align={'center'}>
|
||||||
<IconClock color={colors['blue-button']} size={50} />
|
<IconClock color={colors['blue-button']} size={40} />
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>Full Time</Text>
|
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }} c={colors['blue-button']} lh={1.3}>
|
||||||
<Text fz={'h4'}>{formatCurrency(v.gaji)}</Text>
|
Full Time
|
||||||
|
</Text>
|
||||||
|
<Text fz={{ base: 'sm', md: 'h4' }} lh={1.5}>
|
||||||
|
{formatCurrency(v.gaji)}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)}>Detail</Button>
|
<Button onClick={() => router.push(`/darmasaba/ekonomi/lowongan-kerja-lokal/${v.id}`)} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider } from '@mantine/core';
|
import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Badge, Divider, Title } from '@mantine/core';
|
||||||
import { IconArrowBack, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
|
import { IconArrowBack, IconBrandWhatsapp, IconMapPin, IconPhone, IconStar } from '@tabler/icons-react';
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -31,14 +31,16 @@ function DetailProdukPasarUser() {
|
|||||||
<Box py={20}>
|
<Box py={20}>
|
||||||
{/* Tombol kembali */}
|
{/* Tombol kembali */}
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
leftSection={<IconArrowBack size={20} color={colors['blue-button']} />}
|
||||||
mb={15}
|
mb={15}
|
||||||
>
|
>
|
||||||
Kembali ke daftar produk
|
<Text fz={{ base: 'md', md: 'lg' }} lh={1.5}>
|
||||||
</Button>
|
Kembali ke daftar produk
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paper
|
<Paper
|
||||||
@@ -65,26 +67,31 @@ function DetailProdukPasarUser() {
|
|||||||
<Box
|
<Box
|
||||||
h={300}
|
h={300}
|
||||||
bg="gray.1"
|
bg="gray.1"
|
||||||
display="flex"
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 'var(--mantine-radius-md)' }}
|
||||||
style={{ alignItems: 'center', justifyContent: 'center', borderRadius: 'md' }}
|
|
||||||
>
|
>
|
||||||
<Text c="dimmed">Tidak ada gambar</Text>
|
<Text c="dimmed" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
|
Tidak ada gambar
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Detail Produk */}
|
{/* Detail Produk */}
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
|
<Title order={2} lh={1.1} c={colors['blue-button']}>
|
||||||
{data.nama || 'Produk Tanpa Nama'}
|
{data.nama || 'Produk Tanpa Nama'}
|
||||||
</Text>
|
</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Badge color="green" size="lg" radius="md">
|
<Badge color="green" size="lg" radius="md">
|
||||||
Rp {data.harga?.toLocaleString('id-ID')}
|
<Text c={"white"} fz={{ base: 'sm', md: 'md' }} fw={600} lh={1.4}>
|
||||||
|
Rp {data.harga?.toLocaleString('id-ID')}
|
||||||
|
</Text>
|
||||||
</Badge>
|
</Badge>
|
||||||
{data.rating && (
|
{data.rating && (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<IconStar size={18} color="#FFD43B" />
|
<IconStar size={18} color="#FFD43B" />
|
||||||
<Text fz="md" fw={500}>{data.rating}</Text>
|
<Text fz={{ base: 'sm', md: 'md' }} fw={500} lh={1.5}>
|
||||||
|
{data.rating}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
@@ -95,16 +102,20 @@ function DetailProdukPasarUser() {
|
|||||||
{/* Info Tambahan */}
|
{/* Info Tambahan */}
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw={600}>Kategori</Text>
|
<Title order={3} lh={1.15}>
|
||||||
|
Kategori
|
||||||
|
</Title>
|
||||||
<Group gap="xs" mt={4}>
|
<Group gap="xs" mt={4}>
|
||||||
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
|
{data.KategoriToPasar && data.KategoriToPasar.length > 0 ? (
|
||||||
data.KategoriToPasar.map((kategori) => (
|
data.KategoriToPasar.map((kategori) => (
|
||||||
<Badge key={kategori.id} color="blue" variant="light">
|
<Badge key={kategori.id} color="blue" variant="light" fz={{ base: 'xs', md: 'sm' }} lh={1.4}>
|
||||||
{kategori.kategori.nama}
|
{kategori.kategori.nama}
|
||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Text fz="sm" c="dimmed">Tidak ada kategori</Text>
|
<Text fz={{ base: 'xs', md: 'sm' }} c="dimmed" lh={1.5}>
|
||||||
|
Tidak ada kategori
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -112,14 +123,18 @@ function DetailProdukPasarUser() {
|
|||||||
{data.alamatUsaha && (
|
{data.alamatUsaha && (
|
||||||
<Group gap={6}>
|
<Group gap={6}>
|
||||||
<IconMapPin size={18} color={colors['blue-button']} />
|
<IconMapPin size={18} color={colors['blue-button']} />
|
||||||
<Text fz="md">{data.alamatUsaha}</Text>
|
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
|
{data.alamatUsaha}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.kontak && (
|
{data.kontak && (
|
||||||
<Group gap={6}>
|
<Group gap={6}>
|
||||||
<IconPhone size={18} color={colors['blue-button']} />
|
<IconPhone size={18} color={colors['blue-button']} />
|
||||||
<Text fz="md">{data.kontak}</Text>
|
<Text fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
|
{data.kontak}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -128,8 +143,10 @@ function DetailProdukPasarUser() {
|
|||||||
|
|
||||||
{/* Deskripsi */}
|
{/* Deskripsi */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw={600}>Deskripsi Produk</Text>
|
<Title order={3} lh={1.15}>
|
||||||
<Text fz="md" c="dimmed" mt={4}>
|
Deskripsi Produk
|
||||||
|
</Title>
|
||||||
|
<Text fz={{ base: 'sm', md: 'md' }} c="dimmed" mt={4} lh={1.5}>
|
||||||
Tidak ada deskripsi.
|
Tidak ada deskripsi.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -144,8 +161,11 @@ function DetailProdukPasarUser() {
|
|||||||
component="a"
|
component="a"
|
||||||
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
|
href={`https://wa.me/${data.kontak.replace(/[^0-9]/g, '')}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
leftSection={<IconBrandWhatsapp/>}
|
||||||
>
|
>
|
||||||
Hubungi Penjual via WhatsApp
|
<Text c={"white"} fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
|
Hubungi Penjual via WhatsApp
|
||||||
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IconBrandWhatsapp, IconMapPinFilled, IconSearch, IconStarFilled } from
|
|||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
@@ -30,8 +30,8 @@ function Page() {
|
|||||||
|
|
||||||
const filteredData = selectedCategory
|
const filteredData = selectedCategory
|
||||||
? data?.filter(item =>
|
? data?.filter(item =>
|
||||||
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
|
item.KategoriToPasar?.some(kategori => kategori.kategoriId === selectedCategory)
|
||||||
)
|
)
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
@@ -55,7 +55,7 @@ function Page() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Grid align="center" px={{ base: 'md', md: 100 }}>
|
<Grid align="center" px={{ base: 'md', md: 100 }}>
|
||||||
<GridCol span={{ base: 12, md: 9 }}>
|
<GridCol span={{ base: 12, md: 9 }}>
|
||||||
<Title order={1} c={colors["blue-button"]} fw="bold">
|
<Title order={1} c={colors["blue-button"]} fw="bold" lh={1.15}>
|
||||||
Pasar Desa
|
Pasar Desa
|
||||||
</Title>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
@@ -71,7 +71,14 @@ function Page() {
|
|||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Text px={{ base: 'md', md: 100 }} pt={20} ta="justify" fz={{ base: 'sm', md: 'md' }}>
|
<Text
|
||||||
|
px={{ base: 'md', md: 100 }}
|
||||||
|
pt={20}
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.55 }}
|
||||||
|
c="black"
|
||||||
|
>
|
||||||
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
|
Pasar Desa Online adalah media promosi untuk membantu warga memasarkan dan memperkenalkan produk mereka.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -92,6 +99,9 @@ function Page() {
|
|||||||
searchable
|
searchable
|
||||||
nothingFoundMessage="Tidak ada kategori ditemukan"
|
nothingFoundMessage="Tidak ada kategori ditemukan"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.55 }}
|
||||||
|
c="black"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -114,15 +124,29 @@ function Page() {
|
|||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover' }}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<Text py="sm" fw="bold" fz={{ base: 'md', md: 'lg' }}>
|
<Text
|
||||||
|
py="sm"
|
||||||
|
fw="bold"
|
||||||
|
fz={{ base: 'md', md: 'lg' }}
|
||||||
|
lh={{ base: 1.3, md: 1.25 }}
|
||||||
|
c="black"
|
||||||
|
>
|
||||||
{v.nama}
|
{v.nama}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz={{ base: 'sm', md: 'md' }}>
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.55 }}
|
||||||
|
c="black"
|
||||||
|
>
|
||||||
Rp {v.harga.toLocaleString('id-ID')}
|
Rp {v.harga.toLocaleString('id-ID')}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex py="sm" gap="md">
|
<Flex py="sm" gap="md" align="center">
|
||||||
<IconStarFilled size={20} color="#EBCB09" />
|
<IconStarFilled size={20} color="#EBCB09" />
|
||||||
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
lh={{ base: 1.4, md: 1.45 }}
|
||||||
|
c="black"
|
||||||
|
>
|
||||||
{v.rating}
|
{v.rating}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -130,7 +154,11 @@ function Page() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Flex gap="md" align="center">
|
<Flex gap="md" align="center">
|
||||||
<IconMapPinFilled size={20} color="red" />
|
<IconMapPinFilled size={20} color="red" />
|
||||||
<Text fz={{ base: 'xs', md: 'sm' }} ml={2}>
|
<Text
|
||||||
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
|
lh={{ base: 1.4, md: 1.45 }}
|
||||||
|
c="black"
|
||||||
|
>
|
||||||
{v.alamatUsaha}
|
{v.alamatUsaha}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -69,48 +69,47 @@ function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos="relative" bg={colors.Bg} py={{ base: 'xl', md: 'xl' }} gap={'22'}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Grid align='center'>
|
<Grid align="center">
|
||||||
<GridCol span={{ base: 12, md: 9 }}>
|
<GridCol span={{ base: 12, md: 9 }}>
|
||||||
<Title
|
<Title
|
||||||
order={1}
|
order={1}
|
||||||
c={colors["blue-button"]}
|
c={colors["blue-button"]}
|
||||||
fw={"bold"}
|
fw="bold"
|
||||||
fz={{ base: '28px', md: '32px' }}
|
lh={{ base: 1.2, md: 1.2 }}
|
||||||
lh={{ base: '1.2', md: '1.25' }}
|
|
||||||
>
|
>
|
||||||
Program Kemiskinan
|
Program Kemiskinan
|
||||||
</Title>
|
</Title>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ base: 12, md: 3 }}>
|
<GridCol span={{ base: 12, md: 3 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
radius={"lg"}
|
radius="lg"
|
||||||
placeholder='Cari Program'
|
placeholder="Cari Program"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftSection={<IconSearch size={20} />}
|
leftSection={<IconSearch size={20} />}
|
||||||
w={{ base: "50%", md: "100%" }}
|
w="100%"
|
||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Text
|
<Text
|
||||||
fz={{ base: '14px', md: '16px' }}
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
lh={{ base: '1.5', md: '1.6' }}
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
c="black"
|
c="black"
|
||||||
ta={{ base: 'left', md: 'left' }}
|
ta="left"
|
||||||
pt={20}
|
pt={{ base: 'sm', md: 20 }}
|
||||||
>
|
>
|
||||||
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
|
Berbagai program bantuan untuk mengurangi kemiskinan dan meningkatkan kesejahteraan masyarakat
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap={'lg'} justify="center">
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
pb={10}
|
pb={{ base: 'md', md: 10 }}
|
||||||
cols={{
|
cols={{
|
||||||
base: 1,
|
base: 1,
|
||||||
md: 2
|
md: 2
|
||||||
@@ -118,20 +117,19 @@ function Page() {
|
|||||||
>
|
>
|
||||||
{state.findMany.data.map(v => {
|
{state.findMany.data.map(v => {
|
||||||
return (
|
return (
|
||||||
<Paper p={'xl'} key={v.id}>
|
<Paper p={{ base: 'lg', md: 'xl' }} key={v.id}>
|
||||||
<Title
|
<Title
|
||||||
order={3}
|
order={3}
|
||||||
fw={'bold'}
|
fw="bold"
|
||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
fz={{ base: '18px', md: '20px' }}
|
lh={{ base: 1.2, md: 1.2 }}
|
||||||
lh={{ base: '1.3', md: '1.35' }}
|
|
||||||
>
|
>
|
||||||
{v.nama}
|
{v.nama}
|
||||||
</Title>
|
</Title>
|
||||||
<Text
|
<Text
|
||||||
fz={{ base: '14px', md: '16px' }}
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
lh={{ base: '1.5', md: '1.6' }}
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
c={'black'}
|
c="black"
|
||||||
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
style={{ wordBreak: "break-word", whiteSpace: "normal" }}
|
||||||
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +137,7 @@ function Page() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Center my={10}>
|
<Center my={{ base: 'md', md: 10 }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(newPage) => {
|
onChange={(newPage) => {
|
||||||
@@ -147,16 +145,15 @@ function Page() {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}}
|
}}
|
||||||
total={totalPages}
|
total={totalPages}
|
||||||
my={"md"}
|
my="md"
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
<Paper p={'xl'}>
|
<Paper p={{ base: 'lg', md: 'xl' }}>
|
||||||
<Title
|
<Title
|
||||||
order={3}
|
order={3}
|
||||||
fw={'bold'}
|
fw="bold"
|
||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
fz={{ base: '18px', md: '20px' }}
|
lh={{ base: 1.2, md: 1.2 }}
|
||||||
lh={{ base: '1.3', md: '1.35' }}
|
|
||||||
mb="md"
|
mb="md"
|
||||||
>
|
>
|
||||||
Statistik Kemiskinan Masyarakat
|
Statistik Kemiskinan Masyarakat
|
||||||
@@ -166,7 +163,7 @@ function Page() {
|
|||||||
<Box w="100%" style={{ overflowX: 'auto' }}>
|
<Box w="100%" style={{ overflowX: 'auto' }}>
|
||||||
<Center>
|
<Center>
|
||||||
<RechartsLineChart
|
<RechartsLineChart
|
||||||
width={Math.min(800, window.innerWidth - 100)}
|
width={Math.min(800, typeof window !== 'undefined' ? window.innerWidth - 100 : 800)}
|
||||||
height={400}
|
height={400}
|
||||||
data={statistikData}
|
data={statistikData}
|
||||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
@@ -175,10 +172,12 @@ function Page() {
|
|||||||
<XAxis
|
<XAxis
|
||||||
dataKey="tahun"
|
dataKey="tahun"
|
||||||
label={{ value: 'Tahun', position: 'insideBottomRight', offset: -5 }}
|
label={{ value: 'Tahun', position: 'insideBottomRight', offset: -5 }}
|
||||||
|
tick={{ fontSize: 12 }}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
label={{ value: 'Jumlah', angle: -90, position: 'insideLeft' }}
|
label={{ value: 'Jumlah', angle: -90, position: 'insideLeft' }}
|
||||||
domain={[0, 'auto']}
|
domain={[0, 'auto']}
|
||||||
|
tick={{ fontSize: 12 }}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value) => [`${value} orang`, 'Jumlah']}
|
formatter={(value) => [`${value} orang`, 'Jumlah']}
|
||||||
@@ -199,9 +198,9 @@ function Page() {
|
|||||||
) : (
|
) : (
|
||||||
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
|
<Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
|
||||||
<Text
|
<Text
|
||||||
fz={{ base: '12px', md: '14px' }}
|
fz={{ base: 'xs', md: 'sm' }}
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
lh={{ base: '1.4', md: '1.5' }}
|
lh={{ base: 1.4, md: 1.4 }}
|
||||||
>
|
>
|
||||||
{state.findMany.loading
|
{state.findMany.loading
|
||||||
? 'Memuat data statistik...'
|
? 'Memuat data statistik...'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Stack, Box, Text, Paper, Skeleton, Center } from '@mantine/core';
|
import { Stack, Box, Text, Paper, Skeleton, Center, Title } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
import { BarChart } from '@mantine/charts';
|
import { BarChart } from '@mantine/charts';
|
||||||
@@ -28,16 +28,15 @@ function Page() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this check before the return statement
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap={22}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<Text fz={{ base: 'h1', md: '2.5rem' }} c={colors['blue-button']} fw="bold">
|
<Title order={1} c={colors['blue-button']} fw="bold">
|
||||||
Sektor Unggulan Desa Darmasaba
|
Sektor Unggulan Desa Darmasaba
|
||||||
</Text>
|
</Title>
|
||||||
<Text c="dimmed" mt="md">
|
<Text c="dimmed" mt="md" fz={{ base: 'sm', md: 'md' }} lh={1.5}>
|
||||||
Data sektor unggulan belum tersedia
|
Data sektor unggulan belum tersedia
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -53,32 +52,49 @@ function Page() {
|
|||||||
Ton: item.value,
|
Ton: item.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const chartWidth = Math.max(600, chartData.length * 150); // contoh: 150px per bar
|
const chartWidth = Math.max(600, chartData.length * 150);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap="22">
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: 'md', md: 100 }} >
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Title ta="center" order={1} c={colors['blue-button']} fw="bold">
|
||||||
Sektor Unggulan Desa Darmasaba
|
Sektor Unggulan Desa Darmasaba
|
||||||
|
</Title>
|
||||||
|
<Text
|
||||||
|
ta="center"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
|
mt="sm"
|
||||||
|
>
|
||||||
|
Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta={'center'} fz={'h4'}> Desa Darmasaba dikenal sebagai desa dengan potensi unggulan di sektor pertanian dan peternakan</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap="lg" justify="center">
|
||||||
{data.map((v, k) => {
|
{data.map((v, k) => {
|
||||||
return (
|
return (
|
||||||
<Paper p={'xl'} key={k}>
|
<Paper p="xl" key={k}>
|
||||||
<Text fw={'bold'} fz={'h4'}>{v.name}</Text>
|
<Title order={3} fw="bold">
|
||||||
<Text fz={'h4'} ta={'justify'} style={{ wordBreak: "break-word", whiteSpace: "normal" }} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
|
{v.name}
|
||||||
|
</Title>
|
||||||
|
<Text
|
||||||
|
ta="justify"
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh={{ base: 1.5, md: 1.6 }}
|
||||||
|
style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: v.description || '' }}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
<Box style={{ width: '100%', overflowX: 'auto' }}>
|
||||||
<Paper p="xl">
|
<Paper p="xl">
|
||||||
<Text pb={10} fw="bold" fz="h4">Statistik Sektor Unggulan Darmasaba</Text>
|
<Title order={3} fw="bold" pb="md">
|
||||||
|
Statistik Sektor Unggulan Darmasaba
|
||||||
|
</Title>
|
||||||
<Box style={{ width: '100%', overflowX: 'auto', maxWidth: `${chartWidth}px` }}>
|
<Box style={{ width: '100%', overflowX: 'auto', maxWidth: `${chartWidth}px` }}>
|
||||||
<Center>
|
<Center>
|
||||||
<BarChart
|
<BarChart
|
||||||
@@ -98,7 +114,7 @@ function Page() {
|
|||||||
yAxisLabel="Ton"
|
yAxisLabel="Ton"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
fontSize: '12px', // ukuran font lebih kecil di mobile
|
fontSize: '12px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
'use client';
|
||||||
|
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { IconArrowBack } from '@tabler/icons-react';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
|
function DetailPegawaiBumdes() {
|
||||||
|
const statePegawai = useProxy(stateStrukturBumDes.pegawai);
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
stateStrukturBumDes.posisiOrganisasi.findMany.load();
|
||||||
|
statePegawai.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!statePegawai.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py="lg">
|
||||||
|
<Skeleton height={500} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = statePegawai.findUnique.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||||
|
{/* Back button */}
|
||||||
|
<Group mb="lg" px={{ base: 'md', md: 100 }}>
|
||||||
|
<Box
|
||||||
|
onClick={() => router.back()}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowBack size={22} color={colors['blue-button']} />
|
||||||
|
<Text fz={{ base: 'sm', md: 'md' }} lh="1.4" fw={500} c={colors['blue-button']}>
|
||||||
|
Kembali
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '70%' }}
|
||||||
|
mx="auto"
|
||||||
|
p="xl"
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
bg="white"
|
||||||
|
style={{ border: '1px solid #eaeaea' }}
|
||||||
|
>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
{/* Foto Profil */}
|
||||||
|
<Image
|
||||||
|
src={data.image?.link || '/placeholder-profile.png'}
|
||||||
|
alt={data.namaLengkap || 'Foto Profil'}
|
||||||
|
w={160}
|
||||||
|
h={160}
|
||||||
|
radius={100}
|
||||||
|
fit="cover"
|
||||||
|
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Nama & Jabatan */}
|
||||||
|
<Stack align="center" gap={2}>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fw={700}
|
||||||
|
fz={{ base: 'xl', md: '28px' }}
|
||||||
|
lh="1.2"
|
||||||
|
ta="center"
|
||||||
|
>
|
||||||
|
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh="1.4"
|
||||||
|
c="dimmed"
|
||||||
|
ta="center"
|
||||||
|
>
|
||||||
|
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider my="lg" />
|
||||||
|
|
||||||
|
{/* Informasi Detail */}
|
||||||
|
<Stack gap="md">
|
||||||
|
<InfoRow label="Email" value={data.email} />
|
||||||
|
<InfoRow label="Telepon" value={data.telepon} />
|
||||||
|
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||||
|
<InfoRow
|
||||||
|
label="Tanggal Masuk"
|
||||||
|
value={
|
||||||
|
data.tanggalMasuk
|
||||||
|
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
label="Status"
|
||||||
|
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||||
|
valueColor={data.isActive ? 'green' : 'red'}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Komponen Baris Informasi */
|
||||||
|
function InfoRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
valueColor,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: string | null;
|
||||||
|
valueColor?: string;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
fw={600}
|
||||||
|
lh="1.3"
|
||||||
|
c="dark"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
fz={{ base: 'sm', md: 'md' }}
|
||||||
|
lh="1.5"
|
||||||
|
c={valueColor || 'dimmed'}
|
||||||
|
style={{
|
||||||
|
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailPegawaiBumdes;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
|
import stateStrukturBumDes from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import {
|
import {
|
||||||
@@ -32,12 +31,13 @@ import {
|
|||||||
IconZoomOut,
|
IconZoomOut,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
|
import { useTransitionRouter } from 'next-view-transitions'
|
||||||
import { OrganizationChart } from 'primereact/organizationchart'
|
import { OrganizationChart } from 'primereact/organizationchart'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto'
|
import BackButton from '../../desa/layanan/_com/BackButto'
|
||||||
|
import '../../ppid/struktur-ppid/struktur.css'
|
||||||
import { useMediaQuery } from '@mantine/hooks'
|
import { useMediaQuery } from '@mantine/hooks'
|
||||||
import { useTransitionRouter } from 'next-view-transitions'
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
@@ -49,14 +49,16 @@ export default function Page() {
|
|||||||
paddingBottom: 48,
|
paddingBottom: 48,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
|
|
||||||
<Stack align="center" gap="xl" mt="xl">
|
<Stack align="center" gap="xl" mt="xl">
|
||||||
<Title
|
<Title
|
||||||
order={1}
|
order={1}
|
||||||
ta="center"
|
ta="center"
|
||||||
c={colors['blue-button']}
|
c={colors['blue-button']}
|
||||||
fz={{ base: 28, md: 36 }}
|
fz={{ base: 28, md: 36, lg: 44 }}
|
||||||
|
lh={{ base: 1.05, md: 1.03 }}
|
||||||
>
|
>
|
||||||
Struktur Organisasi & SK Pengurus BumDes
|
Struktur Organisasi & SK Pengurus BumDes
|
||||||
</Title>
|
</Title>
|
||||||
@@ -75,14 +77,18 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StrukturOrganisasiBumDes() {
|
function StrukturOrganisasiBumDes() {
|
||||||
const router = useTransitionRouter()
|
|
||||||
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
const stateOrganisasi: any = useProxy(stateStrukturBumDes.pegawai)
|
||||||
|
const router = useTransitionRouter()
|
||||||
const chartContainerRef = useRef<HTMLDivElement>(null)
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const [scale, setScale] = useState(1)
|
const [scale, setScale] = useState(1)
|
||||||
const [isFullscreen, setFullscreen] = useState(false)
|
const [isFullscreen, setFullscreen] = useState(false)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
|
// debounce pencarian
|
||||||
const debouncedSearch = useRef(
|
const debouncedSearch = useRef(
|
||||||
debounce((value: string) => setSearchQuery(value), 1000)
|
debounce((value: string) => {
|
||||||
|
setSearchQuery(value)
|
||||||
|
}, 1000)
|
||||||
).current
|
).current
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -90,8 +96,7 @@ function StrukturOrganisasiBumDes() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
!stateOrganisasi.findMany.data &&
|
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
|
||||||
stateOrganisasi.findMany.loading !== false
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -149,7 +154,7 @@ function StrukturOrganisasiBumDes() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📊 susun struktur organisasi
|
// 🧩 buat struktur organisasi
|
||||||
const posisiMap = new Map<string, any>()
|
const posisiMap = new Map<string, any>()
|
||||||
const aktifPegawai = data.filter((p: any) => p.isActive)
|
const aktifPegawai = data.filter((p: any) => p.isActive)
|
||||||
|
|
||||||
@@ -183,7 +188,6 @@ function StrukturOrganisasiBumDes() {
|
|||||||
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
||||||
title: node.nama || 'Tanpa Jabatan',
|
title: node.nama || 'Tanpa Jabatan',
|
||||||
image: pegawai?.image?.link || '/img/default.png',
|
image: pegawai?.image?.link || '/img/default.png',
|
||||||
description: node.deskripsi || '',
|
|
||||||
},
|
},
|
||||||
children: node.children?.map(toOrgChartFormat) || [],
|
children: node.children?.map(toOrgChartFormat) || [],
|
||||||
}
|
}
|
||||||
@@ -208,7 +212,7 @@ function StrukturOrganisasiBumDes() {
|
|||||||
chartData = filterNodes(chartData)
|
chartData = filterNodes(chartData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 fullscreen dan zoom control
|
// 🎬 fullscreen & zoom control
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
chartContainerRef.current?.requestFullscreen()
|
chartContainerRef.current?.requestFullscreen()
|
||||||
@@ -225,7 +229,7 @@ function StrukturOrganisasiBumDes() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack align="center" mt="xl">
|
<Stack align="center" mt="xl">
|
||||||
{/* 🧭 Kontrol atas */}
|
{/* 🔍 Controls */}
|
||||||
<Paper
|
<Paper
|
||||||
shadow="xs"
|
shadow="xs"
|
||||||
w={{
|
w={{
|
||||||
@@ -244,6 +248,7 @@ function StrukturOrganisasiBumDes() {
|
|||||||
overflowX: 'auto' // ⬅️ untuk mencegah overflow
|
overflowX: 'auto' // ⬅️ untuk mencegah overflow
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify='center'>
|
<Group justify='center'>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -360,165 +365,163 @@ function StrukturOrganisasiBumDes() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* 🧩 Chart Container */}
|
{/* 🧩 Chart Container */}
|
||||||
<Center style={{ width: '100%' }}>
|
<Center style={{ width: '100%' }}>
|
||||||
<Box
|
<Box
|
||||||
ref={chartContainerRef}
|
ref={chartContainerRef}
|
||||||
|
style={{
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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={{
|
style={{
|
||||||
overflowX: 'auto',
|
objectFit: 'cover',
|
||||||
overflowY: 'auto',
|
}}
|
||||||
width: '100%',
|
/>
|
||||||
maxWidth: '100%',
|
</Box>
|
||||||
padding: '32px 16px',
|
|
||||||
transition: 'transform 0.2s ease',
|
{/* 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/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/${node.data.id}`)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
height: 32,
|
||||||
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box style={{
|
<Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||||
|
</Button>
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</Stack>
|
||||||
)
|
</Card>
|
||||||
}
|
)}
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import { Box } from '@mantine/core';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import LayoutTabs from './_lib/layoutTabs';
|
import LayoutTabs from './_lib/layoutTabs';
|
||||||
|
|
||||||
function Layout({ children }: { children: React.ReactNode }) {
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
|
const isDetailPage = segments.length === 5; // [darmasaba, desa, berita, kategori, id]
|
||||||
|
|
||||||
|
if (isDetailPage) {
|
||||||
|
// Tampilkan tanpa tab menu
|
||||||
|
return (
|
||||||
|
<Box bg={colors.Bg}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<LayoutTabs>
|
<LayoutTabs>
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ function NodeCard({ node, router }: any) {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
<Text c={"white"} fz={{ base: 12, md: 13 }} lh={1} ta="center">Lihat Detail</Text>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -10,10 +10,19 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
|
Progress,
|
||||||
|
Group,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { authStore } from '@/store/authStore'; // ✅ integrasi authStore
|
import { authStore } from '@/store/authStore';
|
||||||
|
|
||||||
|
// ⚙️ Configuration
|
||||||
|
const CONFIG = {
|
||||||
|
POLL_INTERVAL: 3000, // 3 detik
|
||||||
|
MAX_RETRIES: 2, // 2x retry
|
||||||
|
TIMEOUT_DURATION: 5 * 60 * 1000, // 5 menit (300 detik)
|
||||||
|
};
|
||||||
|
|
||||||
async function fetchUser() {
|
async function fetchUser() {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/auth/me', {
|
||||||
@@ -26,21 +35,48 @@ async function fetchUser() {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WaitingRoom() {
|
export default function WaitingRoom() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
const [retryCount, setRetryCount] = useState(0);
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
const MAX_RETRIES = 2;
|
|
||||||
|
|
||||||
|
// ⏱️ Countdown timer
|
||||||
|
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
|
||||||
|
const [hasTimedOut, setHasTimedOut] = useState(false);
|
||||||
|
|
||||||
|
// ⏱️ Countdown effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRedirecting || hasTimedOut) return;
|
||||||
|
|
||||||
|
const countdownInterval = setInterval(() => {
|
||||||
|
setTimeLeft((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
setHasTimedOut(true);
|
||||||
|
setError('Waktu tunggu habis. Silakan hubungi administrator atau coba login ulang nanti.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(countdownInterval);
|
||||||
|
}, [isRedirecting, hasTimedOut]);
|
||||||
|
|
||||||
|
// 🔄 Polling effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
let interval: ReturnType<typeof setInterval>;
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
if (isRedirecting || !isMounted) return;
|
if (isRedirecting || !isMounted || hasTimedOut) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchUser();
|
const data = await fetchUser();
|
||||||
@@ -59,12 +95,11 @@ export default function WaitingRoom() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// In the poll function
|
// ✅ Check if approved
|
||||||
if (currentUser?.isActive === true) {
|
if (currentUser?.isActive === true) {
|
||||||
setIsRedirecting(true);
|
setIsRedirecting(true);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
||||||
// Update authStore with the current user data
|
|
||||||
authStore.setUser({
|
authStore.setUser({
|
||||||
id: currentUser.id,
|
id: currentUser.id,
|
||||||
name: currentUser.name || 'User',
|
name: currentUser.name || 'User',
|
||||||
@@ -78,7 +113,7 @@ export default function WaitingRoom() {
|
|||||||
localStorage.removeItem('auth_nomor');
|
localStorage.removeItem('auth_nomor');
|
||||||
localStorage.removeItem('auth_username');
|
localStorage.removeItem('auth_username');
|
||||||
|
|
||||||
// Force a session refresh
|
// Force session refresh
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/refresh-session', {
|
const res = await fetch('/api/auth/refresh-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -99,26 +134,26 @@ export default function WaitingRoom() {
|
|||||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
window.location.href = redirectPath; // Use window.location to force full page reload
|
window.location.href = redirectPath;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing session:', error);
|
console.error('Error refreshing session:', error);
|
||||||
router.refresh(); // Fallback to client-side refresh
|
router.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
if (err.message.includes('401')) {
|
if (err.message.includes('401')) {
|
||||||
if (retryCount < MAX_RETRIES) {
|
if (retryCount < CONFIG.MAX_RETRIES) {
|
||||||
setRetryCount((prev) => prev + 1);
|
setRetryCount((prev) => prev + 1);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isMounted) interval = setInterval(poll, 3000);
|
if (isMounted) interval = setInterval(poll, CONFIG.POLL_INTERVAL);
|
||||||
}, 800);
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
setError('Sesi tidak valid. Silakan login ulang.');
|
setError('Sesi tidak valid. Silakan login ulang.');
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
authStore.setUser(null); // ✅ clear sesi
|
authStore.setUser(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Error polling:', err);
|
console.error('Error polling:', err);
|
||||||
@@ -126,26 +161,53 @@ export default function WaitingRoom() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interval = setInterval(poll, 3000);
|
interval = setInterval(poll, CONFIG.POLL_INTERVAL);
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [router, isRedirecting, retryCount]);
|
}, [router, isRedirecting, retryCount, hasTimedOut]);
|
||||||
|
|
||||||
// ✅ UI Error
|
// 🚨 Handle logout
|
||||||
if (error) {
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout error:', err);
|
||||||
|
} finally {
|
||||||
|
authStore.setUser(null);
|
||||||
|
localStorage.clear();
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ UI Error / Timeout
|
||||||
|
if (error || hasTimedOut) {
|
||||||
return (
|
return (
|
||||||
<Center h="100vh">
|
<Center h="100vh" bg={colors.Bg}>
|
||||||
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={400}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<Title order={3} c="red">
|
<Title order={3} c="red" ta="center">
|
||||||
Sesi Tidak Valid
|
{hasTimedOut ? '⏱️ Waktu Habis' : '❌ Sesi Tidak Valid'}
|
||||||
</Title>
|
</Title>
|
||||||
<Text>{error}</Text>
|
<Text ta="center" size="sm">
|
||||||
<Button onClick={() => router.push('/login')}>
|
{error || 'Waktu tunggu persetujuan telah habis.'}
|
||||||
Login Ulang
|
</Text>
|
||||||
</Button>
|
<Text ta="center" size="xs" c="dimmed">
|
||||||
|
Silakan hubungi Superadmin atau coba login ulang nanti.
|
||||||
|
</Text>
|
||||||
|
<Group gap="sm" w="100%">
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
Kembali ke Login
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -171,24 +233,56 @@ export default function WaitingRoom() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ UI Default (MENUNGGU) — INI YANG KAMU HILANGKAN!
|
// ⏳ UI Default (MENUNGGU)
|
||||||
|
const progressValue = ((CONFIG.TIMEOUT_DURATION / 1000 - timeLeft) / (CONFIG.TIMEOUT_DURATION / 1000)) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Center h="100vh" bg={colors.Bg}>
|
<Center h="100vh" bg={colors.Bg}>
|
||||||
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
<Paper p="xl" radius="md" bg={colors['white-trans-1']} w={{ base: '90%', sm: 400 }}>
|
||||||
<Stack align="center" gap="lg">
|
<Stack align="center" gap="lg">
|
||||||
<Title order={2} c={colors['blue-button']} ta="center">
|
<Title order={2} c={colors['blue-button']} ta="center">
|
||||||
Menunggu Persetujuan
|
⏳ Menunggu Persetujuan
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text ta="center" c="dimmed">
|
<Text ta="center" c="dimmed">
|
||||||
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||||
</Text>
|
</Text>
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
|
||||||
|
<Text ta="center" size="sm" fw={500}>
|
||||||
Nomor: {user?.nomor || '...'}
|
Nomor: {user?.nomor || '...'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* ⏱️ Countdown Timer */}
|
||||||
|
<Stack w="100%" gap="xs">
|
||||||
|
<Group justify="space-between" w="100%">
|
||||||
|
<Text size="sm" c="dimmed">Sisa waktu:</Text>
|
||||||
|
<Text size="sm" fw={600} c={timeLeft < 60 ? 'red' : colors['blue-button']}>
|
||||||
|
{formatTime(timeLeft)}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Progress
|
||||||
|
value={progressValue}
|
||||||
|
color={timeLeft < 60 ? 'red' : colors['blue-button']}
|
||||||
|
size="sm"
|
||||||
|
animated
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Loader size="sm" color={colors['blue-button']} />
|
<Loader size="sm" color={colors['blue-button']} />
|
||||||
|
|
||||||
<Text ta="center" size="xs" c="dimmed">
|
<Text ta="center" size="xs" c="dimmed">
|
||||||
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* 🚪 Tombol Keluar */}
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
onClick={handleLogout}
|
||||||
|
c="dimmed"
|
||||||
|
>
|
||||||
|
Keluar dari Halaman Ini
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
Reference in New Issue
Block a user