From a5c71ff8ceb5c361d58938e67a0c719926b2dfcd Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:43:22 +0700 Subject: [PATCH] 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; +};