From e09074eed0cd591c744ba3569bf8c51425075920 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 11:55:47 +0700 Subject: [PATCH 1/7] feat(FE): add sapronak table --- .../{_closing => closing}/detail/layout.tsx | 0 src/app/{_closing => closing}/detail/page.tsx | 6 +- src/components/helper/RequireAuth.tsx | 199 +++++++++++++++--- .../sapronak/SapronakCalculationTable.tsx | 5 + 4 files changed, 176 insertions(+), 34 deletions(-) rename src/app/{_closing => closing}/detail/layout.tsx (100%) rename src/app/{_closing => closing}/detail/page.tsx (86%) create mode 100644 src/components/pages/closing/sapronak/SapronakCalculationTable.tsx diff --git a/src/app/_closing/detail/layout.tsx b/src/app/closing/detail/layout.tsx similarity index 100% rename from src/app/_closing/detail/layout.tsx rename to src/app/closing/detail/layout.tsx diff --git a/src/app/_closing/detail/page.tsx b/src/app/closing/detail/page.tsx similarity index 86% rename from src/app/_closing/detail/page.tsx rename to src/app/closing/detail/page.tsx index 038e5072..c5619c48 100644 --- a/src/app/_closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -5,6 +5,7 @@ import useSWR from 'swr'; import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; +import SapronakCalculationTable from '@/components/pages/closing/sapronak/SapronakCalculationTable'; const ClosingDetailPage = () => { const router = useRouter(); @@ -46,7 +47,10 @@ const ClosingDetailPage = () => { )} {!isLoadingClosing && isResponseSuccess(closing) && ( - + <> + + + )} ); diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..dbd4b6bc 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; -import { AxiosError } from 'axios'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { GetMeResponse } from '@/types/api/api-general'; + +// TODO: delete this later, DONT HARDCODE USER DATA +const DUMMY_USER = { + id: 1, + email: 'admin@mbugroup.id', + npk: '0001', + name: 'Super Admin', + image: null, + created_at: '2025-09-30T03:24:20.899229Z', + updated_at: '2025-09-30T03:24:20.899229Z', + roles: [ + { + id: 1, + key: 'mbu.super_admin', + name: 'MBU Administrator', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + permissions: [ + { + id: 1, + name: 'mbu:purchase:read', + action: 'read', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 2, + name: 'mbu:purchase:create', + action: 'create', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 3, + name: 'mbu:purchase:approve', + action: 'approve', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + ], + }, + { + id: 2, + key: 'lti.super_admin', + name: 'LTI Administrator', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + permissions: [ + { + id: 4, + name: 'lti:purchase:read', + action: 'read', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 5, + name: 'lti:purchase:create', + action: 'create', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 6, + name: 'lti:purchase:approve', + action: 'approve', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + ], + }, + { + id: 3, + key: 'manbu.super_admin', + name: 'MANBU Administrator', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + permissions: [ + { + id: 7, + name: 'manbu:purchase:read', + action: 'read', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 8, + name: 'manbu:purchase:create', + action: 'create', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 9, + name: 'manbu:purchase:approve', + action: 'approve', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + ], + }, + ], +}; interface RequireAuthProps { children?: ReactNode; @@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { - data: userResponse, - isLoading: isLoadingUserResponse, - error: userErrorResponse, - } = useSWRImmutable< - GetMeResponse & { ok?: boolean }, - AxiosError, - SWRHttpKey - >('/sso/userinfo', httpClientFetcher, { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - }); + const { data: userResponse, isLoading: isLoadingUserResponse } = + useSWRImmutable( + '/auth/sso/userinfo', + httpClientFetcher, + { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + } + ); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else if ( - isResponseError(userErrorResponse?.response?.data) && - typeof window !== 'undefined' - ) { - router.replace( - `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` - ); + } else { + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setIsLoadingUser, setUser]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { - return ( -
- -
- ); - } + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx new file mode 100644 index 00000000..15ee31eb --- /dev/null +++ b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx @@ -0,0 +1,5 @@ +const SapronakCalculationTable = () => { + return
SapronakCalculationTable
; +}; + +export default SapronakCalculationTable; From a5c71ff8ceb5c361d58938e67a0c719926b2dfcd Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:43:22 +0700 Subject: [PATCH 2/7] feat(FE-284): Slicing and API Integration Perhitungan Sapronak --- src/app/closing/detail/page.tsx | 59 +++- .../sapronak/SapronakCalculationTable.tsx | 309 +++++++++++++++++- src/dummy/closing.dummy.ts | 225 +++++++++++++ src/services/api/closing.ts | 35 +- src/types/api/closing/closing.d.ts | 36 ++ 5 files changed, 644 insertions(+), 20 deletions(-) create mode 100644 src/dummy/closing.dummy.ts diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index c5619c48..73cce850 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -4,13 +4,17 @@ import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; +import { isResponseSuccess } from '@/lib/api-helper'; import SapronakCalculationTable from '@/components/pages/closing/sapronak/SapronakCalculationTable'; +import Tabs from '@/components/Tabs'; +import { useState } from 'react'; const ClosingDetailPage = () => { const router = useRouter(); const searchParams = useSearchParams(); + const [activeTab, setActiveTab] = useState('perhitungan_sapronak'); + const closingId = searchParams.get('closingId'); const { data: closing, isLoading: isLoadingClosing } = useSWR( @@ -24,6 +28,17 @@ const ClosingDetailPage = () => { } ); + const { data: sapronakCalculation, isLoading: isLoadingSapronakCalculation } = + useSWR(`/closing/${closingId}/perhitungan_sapronak`, () => { + const numericId = parseInt(closingId ?? '', 10); + if (isNaN(numericId) || numericId <= 0) { + throw new Error('Invalid closing ID'); + } + const res = ClosingApi.getPerhitunganSapronak(numericId); + console.log(res); + return res; + }); + if (!closingId) { router.back(); @@ -34,24 +49,34 @@ const ClosingDetailPage = () => { ); } - if (!isLoadingClosing && (!closing || isResponseError(closing))) { - router.replace('/404'); - return; - } - return (
- {isLoadingClosing && ( -
- -
- )} - {!isLoadingClosing && isResponseSuccess(closing) && ( - <> - - - - )} + + ), + }, + { + id: 'penjualan', + label: 'Penjualan', + content: isResponseSuccess(closing) && ( + + ), + }, + ]} + />
); }; diff --git a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx index 15ee31eb..679ec5e7 100644 --- a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx +++ b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx @@ -1,5 +1,310 @@ -const SapronakCalculationTable = () => { - return
SapronakCalculationTable
; +'use client'; + +import Card from '@/components/Card'; + +import Table from '@/components/Table'; +import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { + SapronakCalculation, + RowSapronakCalculation, + TotalSapronakCalculation, +} from '@/types/api/closing/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; + +interface SapronakCalculationTableProps { + type?: 'detail'; + initialValues?: SapronakCalculation; +} + +interface FooterSapronakCalculationRow extends RowSapronakCalculation { + _isFooter: true; +} + +const SapronakCalculationTable = ({ + type, + initialValues, +}: SapronakCalculationTableProps) => { + const columns: ColumnDef[] = useMemo( + () => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + const value = props.getValue() as string; + if (isFooter) { + return ( +
+ {value} +
+ ); + } + return value || '-'; + }, + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatNumber(value)} +
+ ); + }, + }, + { + header: 'Uraian', + accessorKey: 'uraian', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + ], + [] + ); + + const createFooterRow = ( + total?: TotalSapronakCalculation + ): FooterSapronakCalculationRow[] => { + if (!total) return []; + return [ + { + id: -999, + tanggal: '', + no_referensi: total.label, + qty_masuk: total.qty_masuk, + qty_keluar: total.qty_keluar, + qty_pakai: total.qty_pakai, + uraian: '', + kategori_produk: '', + harga_beli_per_qty: total.harga_beli_per_qty, + total_harga: total.total_harga, + keterangan: '', + _isFooter: true, + }, + ]; + }; + + const docBroilerFooter = useMemo( + () => createFooterRow(initialValues?.doc_broiler.total), + [initialValues?.doc_broiler.total] + ); + + const ovkFooter = useMemo( + () => createFooterRow(initialValues?.ovk.total), + [initialValues?.ovk.total] + ); + + const pakanFooter = useMemo( + () => createFooterRow(initialValues?.pakan.total), + [initialValues?.pakan.total] + ); + + return ( +
+ <> + + + data={initialValues?.doc_broiler.rows ?? []} + columns={columns} + footerData={docBroilerFooter} + renderFooter={ + (initialValues?.doc_broiler.rows.length ?? 0) > 0 && + !!initialValues?.doc_broiler.total + } + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.doc_broiler.rows.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + + + data={initialValues?.ovk.rows ?? []} + columns={columns} + footerData={ovkFooter} + renderFooter={ + (initialValues?.ovk.rows.length ?? 0) > 0 && + !!initialValues?.ovk.total + } + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.ovk.rows.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + + + data={initialValues?.pakan.rows ?? []} + columns={columns} + footerData={pakanFooter} + renderFooter={ + (initialValues?.pakan.rows.length ?? 0) > 0 && + !!initialValues?.pakan.total + } + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.pakan.rows.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + +
+ ); }; export default SapronakCalculationTable; diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts new file mode 100644 index 00000000..0b1b3f5c --- /dev/null +++ b/src/dummy/closing.dummy.ts @@ -0,0 +1,225 @@ +import { SapronakCalculation } from '@/types/api/closing/closing'; + +// Dummy data +const DUMMY_SAPRONAK_CALCULATION: SapronakCalculation = { + doc_broiler: { + rows: [ + { + id: 1, + tanggal: '11-Sep-2025', + no_referensi: 'PO-PULLET-388', + qty_masuk: 32800, + qty_keluar: 0, + qty_pakai: 32800, + uraian: 'PULLET LOHMANN (16 MINGGU)', + kategori_produk: 'PULLET LAYER', + harga_beli_per_qty: 60136, + total_harga: 1972556800, + keterangan: '-', + }, + { + id: 2, + tanggal: '24-Sep-2025', + no_referensi: 'PO-PULLET-410', + qty_masuk: 14758, + qty_keluar: 0, + qty_pakai: 14758, + uraian: 'PULLET HY-LINE (17 MINGGU)', + kategori_produk: 'PULLET LAYER', + harga_beli_per_qty: 65421, + total_harga: 965908998, + keterangan: '-', + }, + { + id: 3, + tanggal: '29-Sep-2025', + no_referensi: 'PO-PULLET-196', + qty_masuk: 35439, + qty_keluar: 0, + qty_pakai: 35439, + uraian: 'PULLET ISA BROWN (15 MINGGU)', + kategori_produk: 'PULLET LAYER', + harga_beli_per_qty: 55909, + total_harga: 1981297351, + keterangan: '-', + }, + ], + total: { + label: 'TOTAL PULLET', + qty_masuk: 82997, + qty_keluar: 0, + qty_pakai: 82997, + harga_beli_per_qty: 59271.85, + total_harga: 4919763149, + }, + }, + ovk: { + rows: [ + { + id: 1, + tanggal: '28-Sep-2025', + no_referensi: 'PO-OVK-276', + qty_masuk: 52, + qty_keluar: 0, + qty_pakai: 52, + uraian: 'ND-IB VACCINE', + kategori_produk: 'OVK VAKSIN', + harga_beli_per_qty: 204652, + total_harga: 10641904, + keterangan: 'Program kesehatan & biosecurity', + }, + { + id: 2, + tanggal: '26-Sep-2025', + no_referensi: 'PO-OVK-811', + qty_masuk: 43, + qty_keluar: 0, + qty_pakai: 43, + uraian: 'GUMBORO VACCINE', + kategori_produk: 'OVK VAKSIN', + harga_beli_per_qty: 298379, + total_harga: 12830297, + keterangan: 'Program kesehatan & biosecurity', + }, + { + id: 3, + tanggal: '28-Sep-2025', + no_referensi: 'PO-OVK-879', + qty_masuk: 21, + qty_keluar: 0, + qty_pakai: 21, + uraian: 'AMOXITIN SOLUBLE', + kategori_produk: 'OVK OBAT', + harga_beli_per_qty: 145952, + total_harga: 3064992, + keterangan: 'Program kesehatan & biosecurity', + }, + { + id: 4, + tanggal: '11-Okt-2025', + no_referensi: 'PO-OVK-340', + qty_masuk: 38, + qty_keluar: 0, + qty_pakai: 38, + uraian: 'TILOXIN SOLUBLE', + kategori_produk: 'OVK OBAT', + harga_beli_per_qty: 200424, + total_harga: 7616112, + keterangan: 'Program kesehatan & biosecurity', + }, + { + id: 5, + tanggal: '27-Sep-2025', + no_referensi: 'PO-OVK-364', + qty_masuk: 7, + qty_keluar: 0, + qty_pakai: 7, + uraian: 'EGG STIMULANT', + kategori_produk: 'OVK VITAMIN', + harga_beli_per_qty: 115024, + total_harga: 805168, + keterangan: 'Program kesehatan & biosecurity', + }, + { + id: 6, + tanggal: '16-Sep-2025', + no_referensi: 'PO-OVK-982', + qty_masuk: 57, + qty_keluar: 0, + qty_pakai: 57, + uraian: 'MULTIVIT-AMINO', + kategori_produk: 'OVK VITAMIN', + harga_beli_per_qty: 65123, + total_harga: 3712011, + keterangan: 'Program kesehatan & biosecurity', + }, + { + id: 7, + tanggal: '04-Okt-2025', + no_referensi: 'PO-OVK-876', + qty_masuk: 4, + qty_keluar: 0, + qty_pakai: 4, + uraian: 'BKC DESINFEKTAN', + kategori_produk: 'OVK KIMIA', + harga_beli_per_qty: 105677, + total_harga: 422708, + keterangan: 'Program kesehatan & biosecurity', + }, + ], + total: { + label: 'TOTAL OVK', + qty_masuk: 222, + qty_keluar: 0, + qty_pakai: 222, + harga_beli_per_qty: 176096.36, + total_harga: 39093192, + }, + }, + pakan: { + rows: [ + { + id: 1, + tanggal: '13-Ags-2025', + no_referensi: 'PO-FEED-730', + qty_masuk: 4833, + qty_keluar: 0, + qty_pakai: 4833, + uraian: 'FEED PRE-LAY', + kategori_produk: 'PAKAN PRE-LAY', + harga_beli_per_qty: 7578, + total_harga: 36625874, + keterangan: 'Konsumsi pakan kandang layer', + }, + { + id: 2, + tanggal: '28-Jul-2025', + no_referensi: 'PO-FEED-555', + qty_masuk: 6500, + qty_keluar: 0, + qty_pakai: 6500, + uraian: 'FEED LAYER PHASE 1', + kategori_produk: 'PAKAN LAYER', + harga_beli_per_qty: 8116, + total_harga: 52754000, + keterangan: 'Konsumsi pakan kandang layer', + }, + { + id: 3, + tanggal: '24-Agu-2025', + no_referensi: 'PO-FEED-683', + qty_masuk: 8802, + qty_keluar: 0, + qty_pakai: 8802, + uraian: 'FEED LAYER PHASE 2', + kategori_produk: 'PAKAN LAYER', + harga_beli_per_qty: 8801, + total_harga: 77465402, + keterangan: 'Konsumsi pakan kandang layer', + }, + { + id: 4, + tanggal: '02-Sep-2025', + no_referensi: 'PO-FEED-448', + qty_masuk: 2185, + qty_keluar: 0, + qty_pakai: 2185, + uraian: 'JAGUNG GILING', + kategori_produk: 'PAKAN MIX', + harga_beli_per_qty: 5573, + total_harga: 12187705, + keterangan: 'Konsumsi pakan kandang layer', + }, + ], + total: { + label: 'TOTAL PAKAN', + qty_masuk: 22320, + qty_keluar: 0, + qty_pakai: 22320, + harga_beli_per_qty: 8020.93, + total_harga: 179032981, + }, + }, +}; + +export default DUMMY_SAPRONAK_CALCULATION; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 66f88c76..8f2290ee 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -1,6 +1,7 @@ +import DUMMY_SAPRONAK_CALCULATION from '@/dummy/closing.dummy'; import { BaseApiService } from './base'; import { BaseApiResponse } from '@/types/api/api-general'; -import { ClosingSales } from '@/types/api/closing/closing'; +import { ClosingSales, SapronakCalculation } from '@/types/api/closing/closing'; export class ClosingApiService extends BaseApiService< ClosingSales, @@ -23,6 +24,38 @@ export class ClosingApiService extends BaseApiService< return undefined; } } + + async getPerhitunganSapronak( + projectFlockId: number + ): Promise | undefined> { + // Dummy implementation - simulate API call with delay + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + status: 'success', + message: 'Retrieved sapronak calculation successfully', + data: DUMMY_SAPRONAK_CALCULATION, + }); + }, 500); // Simulate 500ms network delay + }); + + /* + // Real API implementation - uncomment when backend is ready + try { + const path = `${this.basePath}/${projectFlockId}/perhitungan_sapronak`; + + return await httpClient>(path, { + method: 'GET', + }); + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + */ + } } export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 64d0d465..c56afb78 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -27,3 +27,39 @@ export type BaseClosingSales = { }; export type ClosingSales = BaseMetadata & BaseClosingSales; + +// ====== PERHITUNGAN SAPRONAK ====== + +export type RowSapronakCalculation = { + id: number; + tanggal: string; + no_referensi: string; + qty_masuk: number; + qty_keluar: number; + qty_pakai: number; + uraian: string; + kategori_produk: string; + harga_beli_per_qty: number; + total_harga: number; + keterangan: string; +}; + +export type TotalSapronakCalculation = { + label: string; + qty_masuk: number; + qty_keluar: number; + qty_pakai: number; + harga_beli_per_qty: number; + total_harga: number; +}; + +export type SapronakCalculationItem = { + rows: RowSapronakCalculation[]; + total: TotalSapronakCalculation; +}; + +export type SapronakCalculation = { + doc_broiler: SapronakCalculationItem; + ovk: SapronakCalculationItem; + pakan: SapronakCalculationItem; +}; From 375b50b6465890f16e756e39cbdd002131ba7560 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:45:07 +0700 Subject: [PATCH 3/7] fix(FE): revert RequireAuth Component --- src/components/helper/RequireAuth.tsx | 199 +++++--------------------- 1 file changed, 33 insertions(+), 166 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { GetMeResponse } from '@/types/api/api-general'; - -// TODO: delete this later, DONT HARDCODE USER DATA -const DUMMY_USER = { - id: 1, - email: 'admin@mbugroup.id', - npk: '0001', - name: 'Super Admin', - image: null, - created_at: '2025-09-30T03:24:20.899229Z', - updated_at: '2025-09-30T03:24:20.899229Z', - roles: [ - { - id: 1, - key: 'mbu.super_admin', - name: 'MBU Administrator', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - permissions: [ - { - id: 1, - name: 'mbu:purchase:read', - action: 'read', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 2, - name: 'mbu:purchase:create', - action: 'create', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 3, - name: 'mbu:purchase:approve', - action: 'approve', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - ], - }, - { - id: 2, - key: 'lti.super_admin', - name: 'LTI Administrator', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - permissions: [ - { - id: 4, - name: 'lti:purchase:read', - action: 'read', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 5, - name: 'lti:purchase:create', - action: 'create', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 6, - name: 'lti:purchase:approve', - action: 'approve', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - ], - }, - { - id: 3, - key: 'manbu.super_admin', - name: 'MANBU Administrator', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - permissions: [ - { - id: 7, - name: 'manbu:purchase:read', - action: 'read', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 8, - name: 'manbu:purchase:create', - action: 'create', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 9, - name: 'manbu:purchase:approve', - action: 'approve', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); + } else if ( + isResponseError(userErrorResponse?.response?.data) && + typeof window !== 'undefined' + ) { + router.replace( + `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
+ +
+ ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth; From 195bbbe44960f49f0230e5134827d4a0ad83491c Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:51:13 +0700 Subject: [PATCH 4/7] fix(FE): change closing folder name --- src/app/{closing => _closing}/detail/layout.tsx | 0 src/app/{closing => _closing}/detail/page.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/app/{closing => _closing}/detail/layout.tsx (100%) rename src/app/{closing => _closing}/detail/page.tsx (100%) diff --git a/src/app/closing/detail/layout.tsx b/src/app/_closing/detail/layout.tsx similarity index 100% rename from src/app/closing/detail/layout.tsx rename to src/app/_closing/detail/layout.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/_closing/detail/page.tsx similarity index 100% rename from src/app/closing/detail/page.tsx rename to src/app/_closing/detail/page.tsx From 3569955e7fdd74f263711b950b332fa246f51e3b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Mon, 8 Dec 2025 14:01:13 +0700 Subject: [PATCH 5/7] fix(FE): fix warn issue next js --- package-lock.json | 14 ++------------ package.json | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index f960d1c5..f0212474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "^15.5.7", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1855,7 +1855,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1925,7 +1924,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2449,7 +2447,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3063,8 +3060,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/daisyui": { "version": "5.5.8", @@ -3520,7 +3516,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3694,7 +3689,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6173,7 +6167,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6204,7 +6197,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7091,7 +7083,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7259,7 +7250,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index e1f92aaf..52fc6ce2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "^15.5.7", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From f9dfe7b27fdb74f25a3d793b3fe75c82e75fe06a Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 9 Dec 2025 17:57:46 +0700 Subject: [PATCH 6/7] feat(FE-284): Refactor table component support for nesting header --- src/components/Table.tsx | 191 ++-- src/components/helper/RequireAuth.tsx | 199 +++- .../ClosingSapronakCalculationTable.tsx | 340 ++---- src/dummy/closing.dummy.ts | 984 ++++++++++++++++++ src/services/api/closing.ts | 83 +- 5 files changed, 1446 insertions(+), 351 deletions(-) create mode 100644 src/dummy/closing.dummy.ts diff --git a/src/components/Table.tsx b/src/components/Table.tsx index f1466744..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -57,8 +58,6 @@ export interface TableProps { setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; - footerContent?: ReactNode; - footerData?: TData[]; withCheckbox?: boolean; rowOptions?: number[]; } @@ -73,22 +72,22 @@ const emptyContentDefaultValue = ( ); -const TABLE_DEFAULT_STYLING = { +export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', - headerColumnClassName: 'px-4 py-3 text-base-content/50', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', tableBodyClassName: '', - bodyRowClassName: 'border-t border-t-base-content/10', + bodyRowClassName: 'border-t border-base-content/10', bodyColumnClassName: 'px-4 py-3 text-base-content', paginationClassName: '', - - tableFooterClassName: '', - footerRowClassName: '', - footerColumnClassName: '', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ @@ -111,8 +110,6 @@ const Table = ({ setRowSelection, enableRowSelection, renderFooter = false, - footerContent, - footerData = [], withCheckbox = false, rowOptions = [10, 20, 50, 100], }: TableProps) => { @@ -187,14 +184,6 @@ const Table = ({ const table = useReactTable(tableOptions); const { setPageSize } = table; - const footerTableOptions: TableOptions = { - columns, - data: footerData, - getCoreRowModel: getCoreRowModel(), - }; - - const footerTable = useReactTable(footerTableOptions); - const prevPageClickHandler = () => { table.previousPage(); @@ -235,58 +224,82 @@ const Table = ({ key={headerGroup.id} className={tableClassNames.headerRowClassName} > - {headerGroup.headers.map((header) => ( - -
- {flexRender( - header.column.columnDef.header, - header.getContext() + {headerGroup.headers.map((header) => { + const columnRelativeDepth = + header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + return ( + 1, + }, + tableClassNames.headerColumnClassName )} + > +
1, + })} + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} - {header.column.getCanSort() && ( -
- - -
- )} -
- - ))} + {header.column.getCanSort() && ( +
+ + +
+ )} +
+ + ); + })} ))} @@ -311,25 +324,27 @@ const Table = ({ ))} - - {renderFooter && - (footerData && footerData.length > 0 - ? footerTable.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - : footerContent)} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} + + ))} + + )} diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..dbd4b6bc 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; -import { AxiosError } from 'axios'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { GetMeResponse } from '@/types/api/api-general'; + +// TODO: delete this later, DONT HARDCODE USER DATA +const DUMMY_USER = { + id: 1, + email: 'admin@mbugroup.id', + npk: '0001', + name: 'Super Admin', + image: null, + created_at: '2025-09-30T03:24:20.899229Z', + updated_at: '2025-09-30T03:24:20.899229Z', + roles: [ + { + id: 1, + key: 'mbu.super_admin', + name: 'MBU Administrator', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + permissions: [ + { + id: 1, + name: 'mbu:purchase:read', + action: 'read', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 2, + name: 'mbu:purchase:create', + action: 'create', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 3, + name: 'mbu:purchase:approve', + action: 'approve', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + ], + }, + { + id: 2, + key: 'lti.super_admin', + name: 'LTI Administrator', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + permissions: [ + { + id: 4, + name: 'lti:purchase:read', + action: 'read', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 5, + name: 'lti:purchase:create', + action: 'create', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 6, + name: 'lti:purchase:approve', + action: 'approve', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + ], + }, + { + id: 3, + key: 'manbu.super_admin', + name: 'MANBU Administrator', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + permissions: [ + { + id: 7, + name: 'manbu:purchase:read', + action: 'read', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 8, + name: 'manbu:purchase:create', + action: 'create', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 9, + name: 'manbu:purchase:approve', + action: 'approve', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + ], + }, + ], +}; interface RequireAuthProps { children?: ReactNode; @@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { - data: userResponse, - isLoading: isLoadingUserResponse, - error: userErrorResponse, - } = useSWRImmutable< - GetMeResponse & { ok?: boolean }, - AxiosError, - SWRHttpKey - >('/sso/userinfo', httpClientFetcher, { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - }); + const { data: userResponse, isLoading: isLoadingUserResponse } = + useSWRImmutable( + '/auth/sso/userinfo', + httpClientFetcher, + { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + } + ); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else if ( - isResponseError(userErrorResponse?.response?.data) && - typeof window !== 'undefined' - ) { - router.replace( - `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` - ); + } else { + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setIsLoadingUser, setUser]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { - return ( -
- -
- ); - } + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index cd2b8c68..73c10331 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -5,7 +5,6 @@ import Card from '@/components/Card'; import Table from '@/components/Table'; import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { - ClosingSapronakCalculation, RowSapronakCalculation, TotalSapronakCalculation, } from '@/types/api/closing'; @@ -20,10 +19,6 @@ interface ClosingSapronakCalculationTableProps { projectFlockId: number; } -interface FooterSapronakCalculationRow extends RowSapronakCalculation { - _isFooter: true; -} - const ClosingSapronakCalculationTable = ({ type, projectFlockId, @@ -33,176 +28,124 @@ const ClosingSapronakCalculationTable = ({ () => ClosingApi.getPerhitunganSapronak(projectFlockId) ); - const columns: ColumnDef[] = useMemo( - () => [ - { - header: 'Tanggal', - accessorKey: 'tanggal', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'No. Referensi', - accessorKey: 'no_referensi', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - const value = props.getValue() as string; - if (isFooter) { - return ( -
- {value} -
- ); - } - return value || '-'; - }, - }, - { - header: 'QTY Masuk', - accessorKey: 'qty_masuk', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, - }, - { - header: 'QTY Keluar', - accessorKey: 'qty_keluar', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, - }, - { - header: 'QTY Pakai', - accessorKey: 'qty_pakai', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, - }, - { - header: 'Uraian', - accessorKey: 'uraian', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'Kategori Produk', - accessorKey: 'kategori_produk', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'Harga Beli/Qty (Rp)', - accessorKey: 'harga_beli_per_qty', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); - }, - }, - { - header: 'Total Harga (Rp)', - accessorKey: 'total_harga', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); - }, - }, - { - header: 'Keterangan', - accessorKey: 'keterangan', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - ], - [] - ); - - const createFooterRow = ( + // Helper function to create columns with footer support + const createColumns = ( total?: TotalSapronakCalculation - ): FooterSapronakCalculationRow[] => { - if (!total) return []; - return [ - { - id: -999, - tanggal: '', - no_referensi: total.label, - qty_masuk: total.qty_masuk, - qty_keluar: total.qty_keluar, - qty_pakai: total.qty_pakai, - uraian: '', - kategori_produk: '', - harga_beli_per_qty: total.harga_beli_per_qty, - total_harga: total.total_harga, - keterangan: '', - _isFooter: true, - }, - ]; - }; + ): ColumnDef[] => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => (props.getValue() as string) || '-', + footer: 'Total', + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
+ {formatNumber(total.qty_masuk)} +
+ ) + : '', + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
+ {formatNumber(total.qty_keluar)} +
+ ) + : '', + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
+ {formatNumber(total.qty_pakai)} +
+ ) + : '', + }, + { + header: 'Uraian', + accessorKey: 'uraian', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => formatCurrency(props.getValue() as number), + footer: total + ? () => ( +
+ {formatCurrency(total.harga_beli_per_qty)} +
+ ) + : '', + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => formatCurrency(props.getValue() as number), + footer: total + ? () => ( +
+ {formatCurrency(total.total_harga)} +
+ ) + : '', + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + ]; - const docBroilerFooter = useMemo( + // Memoize columns untuk setiap kategori + const docBroilerColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createFooterRow(sapronakCalculation.data?.doc_broiler.total) - : [], + ? createColumns(sapronakCalculation.data?.doc_broiler.total) + : createColumns(), [sapronakCalculation] ); - const ovkFooter = useMemo( + const ovkColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createFooterRow(sapronakCalculation.data?.ovk.total) - : [], + ? createColumns(sapronakCalculation.data?.ovk.total) + : createColumns(), [sapronakCalculation] ); - const pakanFooter = useMemo( + const pakanColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createFooterRow(sapronakCalculation.data?.pakan.total) - : [], + ? createColumns(sapronakCalculation.data?.pakan.total) + : createColumns(), [sapronakCalculation] ); @@ -212,39 +155,20 @@ const ClosingSapronakCalculationTable = ({ <> data={sapronakCalculation.data?.doc_broiler.rows ?? []} - columns={columns} - footerData={docBroilerFooter} - renderFooter={ - (sapronakCalculation.data?.doc_broiler.rows.length ?? 0) > 0 && - !!sapronakCalculation.data?.doc_broiler.total - } + columns={docBroilerColumns} className={{ - containerClassName: cn({ - 'mb-20': - sapronakCalculation.data?.doc_broiler.rows.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + containerClassName: 'my-4', }} + renderFooter /> @@ -259,29 +183,11 @@ const ClosingSapronakCalculationTable = ({ > data={sapronakCalculation.data?.ovk.rows ?? []} - columns={columns} - footerData={ovkFooter} - renderFooter={ - (sapronakCalculation.data?.ovk.rows.length ?? 0) > 0 && - !!sapronakCalculation.data?.ovk.total - } + columns={ovkColumns} className={{ - containerClassName: cn({ - 'mb-20': sapronakCalculation.data?.ovk.rows.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + containerClassName: 'my-4', }} + renderFooter /> @@ -296,29 +202,11 @@ const ClosingSapronakCalculationTable = ({ > data={sapronakCalculation.data?.pakan.rows ?? []} - columns={columns} - footerData={pakanFooter} - renderFooter={ - (sapronakCalculation.data?.pakan.rows.length ?? 0) > 0 && - !!sapronakCalculation.data?.pakan.total - } + columns={pakanColumns} className={{ - containerClassName: cn({ - 'mb-20': sapronakCalculation.data?.pakan.rows.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + containerClassName: 'my-4', }} + renderFooter /> diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts new file mode 100644 index 00000000..8ebb0164 --- /dev/null +++ b/src/dummy/closing.dummy.ts @@ -0,0 +1,984 @@ +/** + * Dummy Data untuk Closing API + * + * File ini berisi dummy data untuk testing API Closing sebelum backend siap. + * + * Struktur data mengikuti tipe yang didefinisikan di @/types/api/closing.d.ts + * + * @example + * // 1. Menggunakan getAllFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * '/closings', + * ClosingApi.getAllFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of Closing objects + * } + * + * @example + * // 2. Menggunakan getSingle: + * import { ClosingApi } from '@/services/api/closing'; + * + * const response = await ClosingApi.getSingle(1); + * if (response?.status === 'success') { + * console.log(response.data); // Single Closing object + * } else if (response?.status === 'error') { + * console.error(response.message); // Error message + * } + * + * @example + * // 3. Menggunakan getGeneralInfo dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const closingId = 1; + * const { data, error, isLoading } = useSWR( + * closingId, + * (id: number) => ClosingApi.getGeneralInfo(id) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // ClosingGeneralInformation object + * } + * + * @example + * // 4. Menggunakan getAllIncomingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/incoming`, + * ClosingApi.getAllIncomingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingIncomingSapronak + * } + * + * @example + * // 5. Menggunakan getAllOutgoingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/outgoing`, + * ClosingApi.getAllOutgoingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingOutgoingSapronak + * } + * + * @see {@link /home/sweetpotet/Documents/projects/lti-web-client/src/types/api/closing.d.ts} + */ + +import { format } from 'date-fns'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, + ClosingSapronakCalculation, +} from '@/types/api/closing'; +import { CreatedUser, BaseApiResponse } from '@/types/api/api-general'; + +// Waktu saat ini untuk created_at/updated_at +const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); +const today = format(new Date(), 'yyyy-MM-dd'); +const yesterday = format( + new Date().setDate(new Date().getDate() - 1), + 'yyyy-MM-dd' +); +const lastWeek = format( + new Date().setDate(new Date().getDate() - 7), + 'yyyy-MM-dd' +); +const lastMonth = format( + new Date().setMonth(new Date().getMonth() - 1), + 'yyyy-MM-dd' +); + +// ====================== +// 👤 Created User +// ====================== +export const createdUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin Utama', +}; + +// ====================== +// 📊 Closing Dummy Data +// ====================== +export const dummyClosings: Closing[] = [ + // 1. Closing dengan status Pengajuan - GROWING + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. Closing dengan status Aktif - LAYING + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. Closing dengan status Selesai - GROWING + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. Closing dengan status Aktif - LAYING + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. Closing dengan status Selesai - GROWING + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. Closing dengan status Pengajuan - LAYING + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, // Belum ada tanggal closing + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. Closing dengan status Aktif - GROWING + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. Closing dengan status Selesai - LAYING + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📊 Closing General Information Dummy Data +// ====================== +export const dummyClosingGeneralInformations: ClosingGeneralInformation[] = [ + // 1. General Info - GROWING - Pengajuan + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + flock_id: 101, + project_type: 'GROWING', + population: 15000, + active_house_count: 3, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. General Info - LAYING - Aktif + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + flock_id: 102, + project_type: 'LAYING', + population: 10000, + active_house_count: 2, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. General Info - GROWING - Selesai + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + flock_id: 103, + project_type: 'GROWING', + population: 20000, + active_house_count: 4, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. General Info - LAYING - Aktif + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + flock_id: 104, + project_type: 'LAYING', + population: 5000, + active_house_count: 1, + closing_status: 'In Progress', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. General Info - GROWING - Selesai + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 105, + project_type: 'GROWING', + population: 25000, + active_house_count: 5, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. General Info - LAYING - Pengajuan + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + flock_id: 106, + project_type: 'LAYING', + population: 12000, + active_house_count: 2, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. General Info - GROWING - Aktif + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + flock_id: 107, + project_type: 'GROWING', + population: 18000, + active_house_count: 3, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. General Info - LAYING - Selesai + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 108, + project_type: 'LAYING', + population: 30000, + active_house_count: 6, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📦 Incoming Sapronak Dummy Data +// ====================== +export const dummyIncomingSapronaks: ClosingIncomingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'IN-2025-001', + transaction_type: 'Pembelian', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang A1', + quantity: 5000, + unit: 'Ekor', + formatted_quantity: '5,000 Ekor', + notes: 'DOC berkualitas tinggi dari supplier terpercaya', + }, + { + id: 2, + date: yesterday, + reference_number: 'IN-2025-002', + transaction_type: 'Transfer Masuk', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Gudang Area Bandung', + destination_warehouse: 'Kandang B1', + quantity: 100, + unit: 'Sak', + formatted_quantity: '100 Sak (5,000 Kg)', + notes: 'Pakan starter untuk periode awal', + }, + { + id: 3, + date: lastWeek, + reference_number: 'IN-2025-003', + transaction_type: 'Pembelian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Supplier Medion', + destination_warehouse: 'Gudang Farmasi', + quantity: 50, + unit: 'Botol', + formatted_quantity: '50 Botol', + notes: 'Vitamin untuk meningkatkan daya tahan tubuh', + }, + { + id: 4, + date: today, + reference_number: 'IN-2025-004', + transaction_type: 'Pembelian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang C1', + quantity: 200, + unit: 'Sak', + formatted_quantity: '200 Sak (10,000 Kg)', + notes: 'Pakan finisher untuk periode akhir', + }, + { + id: 5, + date: yesterday, + reference_number: 'IN-2025-005', + transaction_type: 'Transfer Masuk', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Area Jakarta', + destination_warehouse: 'Gudang Farmasi', + quantity: 30, + unit: 'Box', + formatted_quantity: '30 Box', + notes: 'Antibiotik untuk pencegahan penyakit', + }, +]; + +// ====================== +// 📤 Outgoing Sapronak Dummy Data +// ====================== +export const dummyOutgoingSapronaks: ClosingOutgoingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'OUT-2025-001', + transaction_type: 'Pemakaian', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang A1', + destination_warehouse: 'Konsumsi Kandang A1', + quantity: 50, + unit: 'Sak', + formatted_quantity: '50 Sak (2,500 Kg)', + notes: 'Pemakaian pakan harian periode starter', + }, + { + id: 2, + date: yesterday, + reference_number: 'OUT-2025-002', + transaction_type: 'Transfer Keluar', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Kandang B1', + destination_warehouse: 'Kandang B2', + quantity: 1000, + unit: 'Ekor', + formatted_quantity: '1,000 Ekor', + notes: 'Transfer DOC ke kandang baru', + }, + { + id: 3, + date: lastWeek, + reference_number: 'OUT-2025-003', + transaction_type: 'Pemakaian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 10, + unit: 'Botol', + formatted_quantity: '10 Botol', + notes: 'Pemberian vitamin untuk meningkatkan kesehatan', + }, + { + id: 4, + date: today, + reference_number: 'OUT-2025-004', + transaction_type: 'Pemakaian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Kandang C1', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 80, + unit: 'Sak', + formatted_quantity: '80 Sak (4,000 Kg)', + notes: 'Pemakaian pakan harian periode finisher', + }, + { + id: 5, + date: yesterday, + reference_number: 'OUT-2025-005', + transaction_type: 'Pemakaian', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang D1', + quantity: 5, + unit: 'Box', + formatted_quantity: '5 Box', + notes: 'Pengobatan untuk ayam yang sakit', + }, + { + id: 6, + date: lastWeek, + reference_number: 'OUT-2025-006', + transaction_type: 'Transfer Keluar', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang E1', + destination_warehouse: 'Kandang E2', + quantity: 30, + unit: 'Sak', + formatted_quantity: '30 Sak (1,500 Kg)', + notes: 'Transfer pakan antar kandang', + }, +]; + +// ====================== +// 📊 Perhitungan Sapronak Dummy Data +// ====================== +export const dummySapronakCalculation: ClosingSapronakCalculation = { + // DOC Broiler Calculation + doc_broiler: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-001', + qty_masuk: 5000, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 40000000, + keterangan: 'Pembelian DOC dari supplier', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'OUT-2025-002', + qty_masuk: 0, + qty_keluar: 1000, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 8000000, + keterangan: 'Transfer DOC ke kandang lain', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'USE-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 400000, + keterangan: 'Mortalitas DOC', + }, + ], + total: { + label: 'Total DOC Broiler', + qty_masuk: 5000, + qty_keluar: 1000, + qty_pakai: 50, + harga_beli_per_qty: 8000, + total_harga: 48400000, + }, + }, + + // OVK Calculation + ovk: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-003', + qty_masuk: 50, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 7500000, + keterangan: 'Pembelian vitamin', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'IN-2025-005', + qty_masuk: 30, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 7500000, + keterangan: 'Pembelian antibiotik', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'OUT-2025-003', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 10, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 1500000, + keterangan: 'Pemakaian vitamin', + }, + { + id: 4, + tanggal: yesterday, + no_referensi: 'OUT-2025-005', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 5, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 1250000, + keterangan: 'Pemakaian antibiotik', + }, + ], + total: { + label: 'Total OVK', + qty_masuk: 80, + qty_keluar: 0, + qty_pakai: 15, + harga_beli_per_qty: 200000, + total_harga: 17750000, + }, + }, + + // Pakan Calculation + pakan: { + rows: [ + { + id: 1, + tanggal: yesterday, + no_referensi: 'IN-2025-002', + qty_masuk: 100, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 45000000, + keterangan: 'Pembelian pakan starter', + }, + { + id: 2, + tanggal: today, + no_referensi: 'IN-2025-004', + qty_masuk: 200, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 96000000, + keterangan: 'Pembelian pakan finisher', + }, + { + id: 3, + tanggal: today, + no_referensi: 'OUT-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 22500000, + keterangan: 'Pemakaian pakan starter', + }, + { + id: 4, + tanggal: today, + no_referensi: 'OUT-2025-004', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 80, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 38400000, + keterangan: 'Pemakaian pakan finisher', + }, + { + id: 5, + tanggal: lastWeek, + no_referensi: 'OUT-2025-006', + qty_masuk: 0, + qty_keluar: 30, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 13500000, + keterangan: 'Transfer pakan ke kandang lain', + }, + ], + total: { + label: 'Total Pakan', + qty_masuk: 300, + qty_keluar: 30, + qty_pakai: 130, + harga_beli_per_qty: 465000, + total_harga: 215400000, + }, + }, +}; + +// ====================== +// 🔧 Dummy API Response Functions +// ====================== + +/** + * Dummy implementation for getAllFetcher + * Returns all closing records + */ +export const dummyGetAllFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: Closing[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: dummyClosings, + }; +}; + +/** + * Dummy implementation for getSingle + * Returns a single closing by ID + */ +export const dummyGetSingle = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closing = dummyClosings.find((c) => c.id === id); + + if (!closing) { + return { + code: 404, + status: 'error', + message: `Closing dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: closing, + }; +}; + +/** + * Dummy implementation for getAllIncomingSapronakFetcher + * Returns all incoming sapronak records + */ +export const dummyGetAllIncomingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingIncomingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak masuk berhasil diambil', + data: dummyIncomingSapronaks, + }; +}; + +/** + * Dummy implementation for getAllOutgoingSapronakFetcher + * Returns all outgoing sapronak records + */ +export const dummyGetAllOutgoingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingOutgoingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak keluar berhasil diambil', + data: dummyOutgoingSapronaks, + }; +}; + +/** + * Dummy implementation for getGeneralInfo + * Returns closing general information by ID + */ +export const dummyGetGeneralInfo = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closingInfo = dummyClosingGeneralInformations.find((c) => c.id == id); + + if (!closingInfo) { + return { + code: 404, + status: 'error', + message: `Closing general information dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing general information berhasil diambil', + data: closingInfo, + }; +}; + +/** + * Dummy implementation for getPerhitunganSapronak + * Returns sapronak calculation data + */ +export const dummyGetPerhitunganSapronak = async ( + id: number +): Promise< + | { + code: number; + status: 'success'; + message: string; + data: ClosingSapronakCalculation; + } + | undefined +> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data perhitungan sapronak berhasil diambil', + data: dummySapronakCalculation, + }; +}; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 9dc5ab30..9514f6a3 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -8,17 +8,62 @@ import { ClosingOutgoingSapronak, ClosingSapronakCalculation, } from '@/types/api/closing'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; +import { + dummyGetAllFetcher, + dummyGetSingle, + dummyGetAllIncomingSapronakFetcher, + dummyGetAllOutgoingSapronakFetcher, + dummyGetGeneralInfo, + dummyGetPerhitunganSapronak, +} from '@/dummy/closing.dummy'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; export class ClosingApiService extends BaseApiService { constructor(basePath: string) { super(basePath); } + async getAllFetcher(endpoint: string): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllFetcher(); + + // Uncomment this when backend is ready + return await httpClientFetcher>(endpoint); + } + + async getSingle(id: number): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetSingle(id); + // } catch (error) { + // if (axios.isAxiosError>(error)) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready + try { + const getSinglePath = `${this.basePath}/${id}`; + const getSingleRes = + await httpClient>(getSinglePath); + return getSingleRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + async getAllIncomingSapronakFetcher( endpoint: string ): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllIncomingSapronakFetcher(); + + // Uncomment this when backend is ready return await httpClientFetcher>( endpoint ); @@ -27,19 +72,37 @@ export class ClosingApiService extends BaseApiService { async getAllOutgoingSapronakFetcher( endpoint: string ): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllOutgoingSapronakFetcher(); + + // Uncomment this when backend is ready return await httpClientFetcher>( endpoint ); } - async getGeneralInfo(id: number) { + async getGeneralInfo( + id: number + ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetGeneralInfo(id); + // } catch (error) { + // if ( + // axios.isAxiosError>(error) + // ) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready try { const getGeneralInfoPath = `${this.basePath}/${id}`; const getGeneralInfoRes = await httpClient>( getGeneralInfoPath ); - return getGeneralInfoRes; } catch (error) { if ( @@ -54,9 +117,21 @@ export class ClosingApiService extends BaseApiService { async getPerhitunganSapronak( id: number ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetPerhitunganSapronak(id); + // } catch (error) { + // if ( + // axios.isAxiosError>(error) + // ) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready try { const path = `${this.basePath}/${id}/perhitungan_sapronak`; - return await httpClient>( path, { From 9af140e58d0f3737dbfe8940a1083e24591abaf2 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 16:56:25 +0700 Subject: [PATCH 7/7] fix(FE): fix merge conflict --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f0187471..6028a8cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -160,6 +160,6 @@ deploy:dev: # variables: # S3_BUCKET: "lti-erp.mbugroup.id" # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" -# environment: +# environment: # name: production