Sinkronisasi UI & API Admin - User Submenu lowongan kerja lokal, Menu Ekonomi

This commit is contained in:
2025-08-20 17:17:04 +08:00
parent 90a6605efd
commit 1c01397c0d
4 changed files with 157 additions and 96 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -61,13 +62,39 @@ const lowonganKerjaState = proxy({
findMany: { findMany: {
data: null as data: null as
| Prisma.LowonganPekerjaanGetPayload<{ | Prisma.LowonganPekerjaanGetPayload<{
omit: { isActive: true }; omit: {
isActive: true;
};
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get(); totalPages: 1,
if (res.status === 200) { loading: false,
lowonganKerjaState.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
lowonganKerjaState.findMany.loading = true; // ✅ Akses langsung via nama path
lowonganKerjaState.findMany.page = page;
lowonganKerjaState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.lowongankerja["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
lowonganKerjaState.findMany.data = res.data.data ?? [];
lowonganKerjaState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
lowonganKerjaState.findMany.data = [];
lowonganKerjaState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch lowongan kerja paginated:", err);
lowonganKerjaState.findMany.data = [];
lowonganKerjaState.findMany.totalPages = 1;
} finally {
lowonganKerjaState.findMany.loading = false;
} }
}, },
}, },

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,20 +30,21 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
const lowonganState = useProxy(lowonganKerjaState) const lowonganState = useProxy(lowonganKerjaState)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = lowonganState.findMany
useShallowEffect(() => { useShallowEffect(() => {
lowonganState.findMany.load(); load(page, 10, search)
}, []) }, [page, search])
const filteredData = (lowonganState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.posisi.toLowerCase().includes(keyword) ||
item.namaPerusahaan.toLowerCase().includes(keyword) ||
item.lokasi.toLowerCase().includes(keyword)
);
});
if (!lowonganState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -60,18 +61,24 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Bekerja Sebagai</TableTh> <TableTh>Pekerjaan</TableTh>
<TableTh>Nama Usaha</TableTh> <TableTh>Nama Perusahaan</TableTh>
<TableTh>Alamat Usaha</TableTh> <TableTh>Lokasi</TableTh>
<TableTh>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.posisi}</TableTd> <TableTd>
<TableTd>{item.namaPerusahaan}</TableTd> <Text fz={"md"}>{item.posisi}</Text>
<TableTd>{item.lokasi}</TableTd> </TableTd>
<TableTd>
<Text fz={"md"}>{item.namaPerusahaan}</Text>
</TableTd>
<TableTd>
<Text fz={"md"}>{item.lokasi}</Text>
</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}> <Button onClick={() => router.push(`/admin/ekonomi/lowongan-kerja-lokal/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
@@ -82,6 +89,14 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,21 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function lowonganKerjaFindMany() { async function lowonganKerjaFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.lowonganPekerjaan.findMany({ const page = Number(context.query.page) || 1;
where: { isActive: true }, const limit = Number(context.query.limit) || 10;
}); const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return { // Buat where clause
success: true, const where: any = { isActive: true };
message: "Success fetch lowongan kerja",
data, // Tambahkan pencarian (jika ada)
}; if (search) {
} catch (e) { where.OR = [
console.error("Find many error:", e); { posisi: { contains: search, mode: 'insensitive' } },
return { { namaPerusahaan: { contains: search, mode: 'insensitive' } },
success: false, { lokasi: { contains: search, mode: 'insensitive' } },
message: "Failed fetch lowongan kerja", ];
}; }
}
} try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.lowonganPekerjaan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.lowonganPekerjaan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil lowongan kerja dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data lowongan kerja",
};
}
}
export default lowonganKerjaFindMany;

View File

@@ -1,64 +1,39 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, TextInput, Group, SimpleGrid, Paper, Flex, Button } from '@mantine/core'; import { Stack, Box, Text, TextInput, Group, SimpleGrid, Paper, Flex, Button, Skeleton, Center, Pagination } 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 { 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';
import { useProxy } from 'valtio/utils';
import lowonganKerjaState from '@/app/admin/(dashboard)/_state/ekonomi/lowongan-kerja';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
const data = [
{
id: 1,
kerja: 'Kasir',
tempat: 'Toko Sumber Rejeki',
alamat: 'Desa Munggu , Badung',
gaji: 'Rp. 2.500.000 / bulan'
},
{
id: 2,
kerja: 'Kasir',
tempat: 'Toko Sumber Rejeki',
alamat: 'Desa Munggu , Badung',
gaji: 'Rp. 2.500.000 / bulan'
},
{
id: 3,
kerja: 'Kasir',
tempat: 'Toko Sumber Rejeki',
alamat: 'Desa Munggu , Badung',
gaji: 'Rp. 2.500.000 / bulan'
},
{
id: 4,
kerja: 'Kasir',
tempat: 'Toko Sumber Rejeki',
alamat: 'Desa Munggu , Badung',
gaji: 'Rp. 2.500.000 / bulan'
},
{
id: 5,
kerja: 'Kasir',
tempat: 'Toko Sumber Rejeki',
alamat: 'Desa Munggu , Badung',
gaji: 'Rp. 2.500.000 / bulan'
},
{
id: 6,
kerja: 'Kasir',
tempat: 'Toko Sumber Rejeki',
alamat: 'Desa Munggu , Badung',
gaji: 'Rp. 2.500.000 / bulan'
},
]
function Page() { function Page() {
const state = useProxy(lowonganKerjaState)
const router = useRouter() const router = useRouter()
const [search, setSearch] = useState('')
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => {
load(page, 6, search)
}, [page, search])
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -74,6 +49,8 @@ function Page() {
w={{ base: 500, md: 700 }} w={{ base: 500, md: 700 }}
placeholder='Cari Pekerjaan' placeholder='Cari Pekerjaan'
leftSection={<IconSearch size={20} />} leftSection={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/> />
</Group> </Group>
</Box> </Box>
@@ -93,15 +70,15 @@ function Page() {
<Flex gap={'xl'} align={'center'}> <Flex gap={'xl'} align={'center'}>
<IconBriefcase color={colors['blue-button']} size={50} /> <IconBriefcase color={colors['blue-button']} size={50} />
<Box> <Box>
<Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>{v.kerja}</Text> <Text fw={'bold'} fz={'h4'} c={colors['blue-button']}>{v.posisi}</Text>
<Text fz={'h4'}>{v.tempat}</Text> <Text fz={'h4'}>{v.namaPerusahaan}</Text>
</Box> </Box>
</Flex> </Flex>
</Box> </Box>
<Box> <Box>
<Flex gap={'xl'} align={'center'}> <Flex gap={'xl'} align={'center'}>
<IconMapPin color={colors['blue-button']} size={50} /> <IconMapPin color={colors['blue-button']} size={50} />
<Text fz={'h4'}>{v.alamat}</Text> <Text fz={'h4'}>{v.lokasi}</Text>
</Flex> </Flex>
</Box> </Box>
<Box> <Box>
@@ -119,6 +96,14 @@ function Page() {
) )
})} })}
</SimpleGrid> </SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my="md"
/>
</Center>
</Stack> </Stack>
</Box > </Box >
</Stack > </Stack >