From e09074eed0cd591c744ba3569bf8c51425075920 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 11:55:47 +0700 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 8a0adf847ea8aaa268a4fb08e47495fe070b9aa5 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 9 Dec 2025 10:33:38 +0700 Subject: [PATCH 06/11] fix(FE-279): adjust closing project flock kandang --- .../production/project-flock/closing/page.tsx | 8 +- src/components/helper/RequireAuth.tsx | 199 ++++++++++++--- src/components/pages/ApprovalSteps.tsx | 66 +++-- .../production/chickin/form/ChickinForm.tsx | 6 +- .../chickin/form/tabs/ChickLogsView.tsx | 2 +- .../closing/ProjectFlockClosingForm.tsx | 6 +- .../detail/ProjectFlockDetail.tsx | 47 +++- src/config/approval-line.ts | 21 +- src/config/constant.ts | 7 - .../api/production/project-flock-kandang.ts | 226 +++++++++--------- 10 files changed, 398 insertions(+), 190 deletions(-) diff --git a/src/app/production/project-flock/closing/page.tsx b/src/app/production/project-flock/closing/page.tsx index d734f669..d10bdfa2 100644 --- a/src/app/production/project-flock/closing/page.tsx +++ b/src/app/production/project-flock/closing/page.tsx @@ -14,13 +14,13 @@ const ProjectFlockClosingPage = () => { const projectFlockKandangId = searchParams.get('projectFlockKandangId'); const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } = - useSWR(projectFlockKandangId, (id: number) => - ProjectFlockKandangApi.getSingle(id) + useSWR(`get-flock-kandang-id/${projectFlockKandangId}`, () => + ProjectFlockKandangApi.getSingle(parseInt(projectFlockKandangId ?? '')) ); const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( - projectFlockId, - (id: number) => ProjectFlockApi.getSingle(id) + `get-flock-id/${projectFlockId}`, + () => ProjectFlockApi.getSingle(parseInt(projectFlockId ?? '')) ); if (!projectFlockId || !projectFlockKandangId) { 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/ApprovalSteps.tsx b/src/components/pages/ApprovalSteps.tsx index d5dcabc0..6ae7c13a 100644 --- a/src/components/pages/ApprovalSteps.tsx +++ b/src/components/pages/ApprovalSteps.tsx @@ -144,33 +144,45 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => { export const formatGroupedApprovalsToApprovalSteps = ( approvalLine: ApprovalLine, - groupedApprovals: BaseGroupedApproval[], - latestApproval: BaseApproval + groupedApprovals: BaseGroupedApproval[] | undefined, + latestApproval: BaseApproval | undefined ): ApprovalStepsProps['approvals'] => { const formattedApprovalSteps: ApprovalStepsProps['approvals'] = approvalLine.map((approvalLineItem) => { - const approvalGroup = groupedApprovals.find( + const approvalGroup = groupedApprovals?.find( (approvalGroupItem) => approvalGroupItem.step_number === approvalLineItem.step_number ); const currentStepNumber = approvalLineItem.step_number; const lastStepNumber = - groupedApprovals[groupedApprovals.length - 1]?.step_number; + groupedApprovals?.[groupedApprovals.length - 1]?.step_number; - const isLatestApprovalRejected = latestApproval.action === 'REJECTED'; + const isLatestApprovalRejected = latestApproval?.action === 'REJECTED'; - if (!approvalGroup && currentStepNumber <= lastStepNumber) { - throw new Error( - `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!` - ); + // Only throw error if we have a valid lastStepNumber to compare against + if ( + !approvalGroup && + lastStepNumber !== undefined && + currentStepNumber <= lastStepNumber + ) { + // throw new Error( + // `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!` + // ); } if (!approvalGroup) { - const isWaiting = currentStepNumber === latestApproval.step_number + 1; + // Check if this step is waiting (only if we have latestApproval) + const isWaiting = + latestApproval?.step_number !== undefined && + currentStepNumber === latestApproval.step_number + 1; + + // Check if previous approval was rejected const isPreviousApprovalRejected = - groupedApprovals[groupedApprovals.length - 1].approvals[0].action === - 'REJECTED'; + groupedApprovals && + groupedApprovals.length > 0 && + groupedApprovals[groupedApprovals.length - 1]?.approvals?.[0] + ?.action === 'REJECTED'; return { name: approvalLineItem.step_name, @@ -184,7 +196,11 @@ export const formatGroupedApprovalsToApprovalSteps = ( let approvalStatus: ApprovalStepStatus = 'IDLE'; - if (approvalGroup.step_number <= latestApproval.step_number) { + // Only compare if latestApproval and its step_number exist + if ( + latestApproval?.step_number !== undefined && + approvalGroup.step_number <= latestApproval.step_number + ) { if (approvalGroup.approvals) { switch (approvalGroup?.approvals[0]?.action) { case 'CREATED': @@ -203,6 +219,7 @@ export const formatGroupedApprovalsToApprovalSteps = ( } } } else if ( + latestApproval?.step_number !== undefined && approvalGroup.step_number === latestApproval.step_number + 1 && !isLatestApprovalRejected ) { @@ -353,14 +370,33 @@ const useApprovalSteps = ({ // Formatting Akhir const approvals = useMemo(() => { - if (isLoading || !approvalLines.length || !latestApproval) { + if (isLoading || !approvalLines.length) { return []; } + + // Try to derive latestApproval from groupedApprovals if not provided + let effectiveLatestApproval = latestApproval; + + if (!effectiveLatestApproval && groupedApprovals.length > 0) { + // Get all approvals from grouped data + const allApprovals = groupedApprovals.flatMap((group) => group.approvals); + + if (allApprovals.length > 0) { + // Use the most recent approval (last in array) + effectiveLatestApproval = allApprovals[allApprovals.length - 1]; + } + } + + // If still no latestApproval, return empty + if (!effectiveLatestApproval) { + return []; + } + try { return formatGroupedApprovalsToApprovalSteps( approvalLines, groupedApprovals, - latestApproval + effectiveLatestApproval ); } catch (error) { console.warn('Gagal memformat approval steps:', error); diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index 84c5b5a5..b6c5a2c0 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -11,12 +11,12 @@ import { useState } from 'react'; import ApprovalSteps, { useApprovalSteps, } from '@/components/pages/ApprovalSteps'; -import { PROJECT_FLOCK_KANDANG_APPROVAL_LINE } from '@/config/approval-line'; import ChickinFormView from '@/components/pages/production/chickin/form/tabs/ChickinFormView'; import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { Icon } from '@iconify/react'; import Badge from '@/components/Badge'; +import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; const ChickinFormKandang = ({ formType = 'add', initialValues, @@ -34,8 +34,8 @@ const ChickinFormKandang = ({ refresh: refreshApprovals, } = useApprovalSteps({ latestApproval: initialValues?.approval, - approvalLines: PROJECT_FLOCK_KANDANG_APPROVAL_LINE, - moduleName: 'PROJECT_FLOCK_KANDANGS', + approvalLines: CHICKINS_APPROVAL_LINE, + moduleName: 'CHICKINS', moduleId: initialValues?.id.toString() ?? '', }); diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 865091d7..094f0d93 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -145,7 +145,7 @@ const ChickinLogsView = ({ }) )} - {initialValues?.approval?.step_number == 1 && ( + {initialValues?.approval?.step_number >= 1 && ( - - ); - } - - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 094f0d93..99eb1cb3 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -145,7 +145,7 @@ const ChickinLogsView = ({ }) )} - {initialValues?.approval?.step_number >= 1 && ( + {initialValues?.approval?.step_number <= 2 && (