diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 309addbd..96487258 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -3,7 +3,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; -import ClosingDetail from '@/components/pages/closing/ClosingDetail'; +import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -34,33 +34,6 @@ const ClosingDetailPage = () => { () => ProjectFlockKandangApi.getSingle(Number(kandangId)) ); - const { data: salesData, isLoading: isLoadingSales } = useSWR( - kandangId - ? `sales-${closingId}-${kandangId}` - : closingId - ? `sales-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId)) - : ClosingApi.getPenjualan(Number(closingId)) - ); - - const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( - kandangId - ? `hpp-ekspedisi-${closingId}-${kandangId}` - : closingId - ? `hpp-ekspedisi-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getHppEkspedisiByKandang( - Number(closingId), - Number(kandangId) - ) - : ClosingApi.getHppEkspedisi(Number(closingId)) - ); - if (!closingId) { router.back(); @@ -76,12 +49,7 @@ const ClosingDetailPage = () => { return; } - const isLoading = - isLoadingClosing || - isLoadingSales || - isLoadingHppEkspedisi || - isLoadingProject || - isLoadingKandang; + const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang; return (
@@ -91,12 +59,6 @@ const ClosingDetailPage = () => { { return ( -
+
); diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx similarity index 57% rename from src/components/pages/closing/ClosingDetail.tsx rename to src/components/pages/closing/ClosingDetailTabs.tsx index c3c91a5a..dc8bd6f8 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -5,28 +5,23 @@ import { useMemo, useState } from 'react'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; -import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; -import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent'; -import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent'; +import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable'; +import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab'; +import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab'; -import { - ClosingGeneralInformation, - BaseClosingSales, - ClosingHppExpedition, -} from '@/types/api/closing'; -import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; -import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; -import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent'; -import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; -import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable'; +import { ClosingGeneralInformation } from '@/types/api/closing'; +import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab'; +import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab'; +import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab'; +import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab'; +import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab'; import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { useClosingTabStore } from '@/stores/closing/closing-tab.store'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; - salesData?: BaseClosingSales; - hppExpeditionData?: ClosingHppExpedition; projectData?: ProjectFlock; kandangData?: ProjectFlockKandang; } @@ -34,25 +29,24 @@ interface ClosingDetailProps { const ClosingDetail: React.FC = ({ id, initialValue, - salesData, - hppExpeditionData, projectData, kandangData, }) => { - const [activeTab, setActiveTab] = useState('sapronak'); + const [activeTabId, setActiveTabId] = useState('sapronak'); + const tabActions = useClosingTabStore((state) => state.tabActions); const closingDetailTabs = useMemo(() => { const validTabs = [ { id: 'sapronak', label: 'Sapronak', - content: , + content: , }, { id: 'perhitunganSapronak', label: 'Perhitungan Sapronak', content: ( - @@ -61,13 +55,13 @@ const ClosingDetail: React.FC = ({ { id: 'penjualan', label: 'Penjualan', - content: , + content: , }, { id: 'overhead', label: 'Overhead', content: ( - = ({ { id: 'hppEkspedisi', label: 'HPP Ekspedisi', - content: , + content: , }, { id: 'dataProduksi', label: 'Data Produksi', - content: , + content: , }, { id: 'keuangan', label: 'Keuangan', - content: , + content: , }, ]; return validTabs; - }, [initialValue]); + }, [initialValue, kandangData, id]); return ( <> -
+
diff --git a/src/components/pages/closing/ClosingFinanceTabContent.tsx b/src/components/pages/closing/ClosingFinanceTabContent.tsx deleted file mode 100644 index 92386178..00000000 --- a/src/components/pages/closing/ClosingFinanceTabContent.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable'; - -const ClosingFinanceTabContent = ({ - projectFlockId, -}: { - projectFlockId: number; -}) => { - return ( -
- {projectFlockId && ( - - )} -
- ); -}; - -export default ClosingFinanceTabContent; diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx deleted file mode 100644 index 6225f5e7..00000000 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import Card from '@/components/Card'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { formatCurrency, formatTitleCase } from '@/lib/helper'; -import { ClosingApi } from '@/services/api/closing'; -import { HppItem, ProfitLossItem } from '@/types/api/closing'; -import { useSearchParams } from 'next/navigation'; -import { useMemo } from 'react'; -import useSWR from 'swr'; - -const ClosingFinanceTable = ({ - projectFlockId, -}: { - projectFlockId: number; -}) => { - const searchParams = useSearchParams(); - const kandangId = searchParams.get('kandangId'); - - const { data: finance, isLoading } = useSWR( - `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, - () => - ClosingApi.getFinance( - projectFlockId, - kandangId ? Number(kandangId) : undefined - ) - ); - - const hppTableData: HppItem[] = useMemo(() => { - if (isResponseSuccess(finance)) { - const customItems = { - label: 'HPP dan Pengeluaran', - code: 'custom_row', - } as HppItem; - const purchases = finance.data.hpp.items.filter( - (item) => item.category === 'purchase' - ); - const totalBudgeting = { - label: 'HPP dan Bahan Baku', - code: 'custom_row', - } as HppItem; - const overheads = finance.data.hpp.items.filter( - (item) => item.category === 'overhead' - ); - return [customItems, ...purchases, totalBudgeting, ...overheads]; - } - return []; - }, [finance]); - - const profitLossTableData: ProfitLossItem[] = useMemo(() => { - if (isResponseSuccess(finance)) { - const incomes = finance.data.profit_loss.items.filter( - (item) => item.type === 'income' - ); - const purchases = finance.data.profit_loss.items.filter( - (item) => item.type === 'purchase' - ); - const overheads = finance.data.profit_loss.items.filter( - (item) => item.type === 'overhead' - ); - const grossProfit = { - label: 'LABA RUGI BRUTO', - code: 'custom_row', - type: 'gross_profit', - rp_per_bird: - finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0, - rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0, - amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0, - } as ProfitLossItem; - const subtotal = { - label: 'Subtotal', - code: 'custom_row', - type: 'subtotal', - rp_per_bird: - finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0, - rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0, - amount: finance.data.profit_loss.summary.sub_total.amount ?? 0, - } as ProfitLossItem; - return [...incomes, ...purchases, grossProfit, ...overheads, subtotal]; - } - return []; - }, [finance]); - - return ( -
- <> - -
-
-
Laba Rugi Brutto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.gross_profit.amount - ) - : '-'} -
-
-
-
Laba Rugi Netto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit.amount - ) - : '-'} -
-
-
-
- -
- - data={hppTableData} - isLoading={isLoading} - columns={[ - { - header: 'No.', - enableSorting: false, - accessorFn: (item, index) => { - if (item.code === 'custom_row') return '-'; - const dataRowsBefore = hppTableData - .slice(0, index) - .filter((row) => row.code !== 'custom_row').length; - return dataRowsBefore + 1; - }, - footer: (props) => { - return 'HPP'; - }, - }, - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => formatTitleCase(item.label || '-'), - }, - { - header: 'Budgeting', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'budgeting_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting - ?.rp_per_bird || 0 - ) - : '-'; - }, - }, - { - header: 'Rp/Kg', - id: 'budgeting_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.rp_per_kg || - 0 - ) - : '-'; - }, - }, - { - header: 'Jumlah (Rp)', - id: 'budgeting_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.amount || 0), - footer: (props) => { - return props.column.id === 'budgeting_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.amount || 0 - ) - : '-'; - }, - }, - ], - }, - { - header: 'Realization', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'realization_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_bird || 0 - ) - : '-'; - }, - }, - { - header: 'Rp/Kg', - id: 'realization_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_kg || 0 - ) - : '-'; - }, - }, - { - header: 'Jumlah (Rp)', - id: 'realization_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.amount || 0), - footer: (props) => { - return props.column.id === 'realization_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization?.amount || 0 - ) - : '-'; - }, - }, - ], - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( - - - -
- {formatTitleCase(rowData.label ?? '-')} -
- - - ); - } - return null; - }} - renderFooter={isResponseSuccess(finance)} - /> -
-
- -
- - data={profitLossTableData} - isLoading={isLoading} - columns={[ - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => item.label, - cell: (item) => ( -
- {formatTitleCase(item.row.original.label || '-')} -
- ), - footer: () => ( -
LABA RUGI NETTO
- ), - }, - { - header: 'Rp/Ekor', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_bird || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Rp/Kg', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_kg || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Jumlah (Rp)', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.amount || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .amount || 0 - ) - : formatCurrency(0)} -
- ), - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( - - -
- {formatTitleCase(rowData.label ?? '-')} -
- - -
- {formatCurrency(rowData.rp_per_bird ?? 0)} -
- - -
- {formatCurrency(rowData.rp_per_kg ?? 0)} -
- - -
- {formatCurrency(rowData.amount ?? 0)} -
- - - ); - } - return null; - }} - className={{ - paginationClassName: 'hidden', - }} - renderFooter={isResponseSuccess(finance)} - /> -
-
- -
- ); -}; - -export default ClosingFinanceTable; diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index dd3083a7..4ecf607f 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -10,18 +10,18 @@ const ClosingKandangList = ({ projectData?: ProjectFlock; }) => { return ( -
+
-

Kandang

-
+

Kandang

+
{projectData?.kandangs?.map((kandang) => ( diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx deleted file mode 100644 index 9295d283..00000000 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ /dev/null @@ -1,308 +0,0 @@ -'use client'; - -import { useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { formatNumber } from '@/lib/helper'; - -interface ClosingProductionDataTabContentProps { - projectFlockId: number; -} - -const ClosingProductionDataTabContent = ({ - projectFlockId, -}: ClosingProductionDataTabContentProps) => { - const searchParams = useSearchParams(); - const kandangId = searchParams.get('kandangId'); - - const { data: productionData, isLoading } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, - () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) - ); - - if (isLoading) { - return ( -
- -
- ); - } - - if (!productionData || !isResponseSuccess(productionData)) { - return ( -
- Gagal memuat data produksi. -
- ); - } - - const { purchase, sales, performance } = productionData.data; - - // Helper for consistent row styling - const DataRow = ({ - label, - value, - unit = '', - valueClassName = 'font-bold text-gray-800', - unitClassName = 'text-gray-500 w-12 text-right', - }: { - label: string; - value: string | number; - unit?: string; - valueClassName?: string; - unitClassName?: string; - }) => ( -
- {label} -
- {value} - {unit && {unit}} -
-
- ); - - return ( -
-

Data Produksi

- -
- {/* Left Column */} -
- {/* Purchase Section */} -
-

- Pembelian -

-
- - - - - -
-
- - {/* Sales Section */} -
-

- Penjualan -

-
- {/* Chicken Sales */} -
- - - - -
- - {/* Egg Sales (if available) */} - {sales.egg && ( - <> -
-
- - - - -
- - )} -
-
-
- - {/* Divider Line (Absolute centered) */} -
- - {/* Right Column */} -
- {/* Performance Section */} -
-

- Performance -

-
- - - - - - {/* - */} - - - - - - - {/* Laying Specific Fields */} - {performance.hen_day_act !== undefined && ( - <> - - - - )} - - {performance.egg_mass !== undefined && ( - <> - - - - )} - - {performance.egg_weight !== undefined && ( - <> - - - - )} - - {performance.hen_housed_act !== undefined && ( - <> - - - - )} -
-
-
-
-
- ); -}; - -export default ClosingProductionDataTabContent; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx deleted file mode 100644 index 77cef803..00000000 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ /dev/null @@ -1,268 +0,0 @@ -'use client'; - -import Card from '@/components/Card'; - -import Table from '@/components/Table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; -import { - RowSapronakCalculation, - TotalSapronakCalculation, -} from '@/types/api/closing'; -import { ColumnDef } from '@tanstack/react-table'; -import { useMemo } from 'react'; -import useSWR from 'swr'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { ClosingGeneralInformation } from '@/types/api/closing'; -import { useSearchParams } from 'next/navigation'; - -interface ClosingSapronakCalculationTableProps { - projectFlockId: number; - closingGeneralInformation?: ClosingGeneralInformation; -} - -const ClosingSapronakCalculationTable = ({ - projectFlockId, - closingGeneralInformation, -}: ClosingSapronakCalculationTableProps) => { - const searchParams = useSearchParams(); - const kandangId = searchParams.get('kandangId'); - - const { data: sapronakCalculation, isLoading } = useSWR( - `/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, - () => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)), - { - keepPreviousData: true, - } - ); - - // Helper function to create columns with footer support - const createColumns = ( - total?: TotalSapronakCalculation - ): ColumnDef[] => [ - { - header: 'Tanggal', - accessorKey: 'date', - cell: (props) => - props.row.original.date - ? formatDate(props.row.original.date, 'DD MMM YYYY') - : '-', - footer: 'Total', - }, - { - header: 'No. Referensi', - accessorKey: 'reference_number', - cell: (props) => (props.row.original.reference_number as string) || '-', - footer: '', - }, - { - header: 'QTY Masuk', - accessorKey: 'qty_in', - cell: (props) => - props.row.original.qty_in - ? formatNumber(props.row.original.qty_in as number) - : '0', - footer: total - ? () => ( -
- {total?.qty_in ? formatNumber(total?.qty_in) : '0'} -
- ) - : '', - }, - { - header: 'QTY Keluar', - accessorKey: 'qty_out', - cell: (props) => - props.row.original.qty_out - ? formatNumber(props.row.original.qty_out as number) - : '0', - footer: total - ? () => ( -
- {total?.qty_out ? formatNumber(total?.qty_out) : '0'} -
- ) - : '', - }, - { - header: 'QTY Pakai', - accessorKey: 'qty_used', - cell: (props) => - props.row.original.qty_used - ? formatNumber(props.row.original.qty_used as number) - : '0', - footer: total - ? () => ( -
- {total?.qty_used ? formatNumber(total?.qty_used) : '0'} -
- ) - : '', - }, - { - header: 'Uraian', - accessorKey: 'description', - cell: (props) => (props.row.original.description as string) || '-', - footer: '', - }, - { - header: 'Kategori Produk', - accessorKey: 'product_category', - cell: (props) => (props.row.original.product_category as string) || '-', - footer: '', - }, - { - header: 'Harga Beli/Qty (Rp)', - accessorKey: 'unit_price', - cell: (props) => - props.row.original.unit_price - ? formatCurrency(props.row.original.unit_price as number) - : '-', - footer: total - ? () => ( -
- {total?.avg_unit_price - ? formatCurrency(total?.avg_unit_price) - : '-'} -
- ) - : '', - }, - { - header: 'Total Harga (Rp)', - accessorKey: 'total_amount', - cell: (props) => - props.row.original.total_amount - ? formatCurrency(props.row.original.total_amount as number) - : '-', - footer: total - ? () => ( -
- {total?.total_amount ? formatCurrency(total?.total_amount) : '-'} -
- ) - : '', - }, - { - header: 'Keterangan', - accessorKey: 'notes', - cell: (props) => (props.row.original.notes as string) || '-', - footer: '', - }, - ]; - - // Memoize columns untuk setiap kategori - const docColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.doc?.total) - : createColumns(), - [sapronakCalculation] - ); - - const ovkColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.ovk?.total) - : createColumns(), - [sapronakCalculation] - ); - - const pakanColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.pakan?.total) - : createColumns(), - [sapronakCalculation] - ); - - return ( -
- {/* Table DOC jika kategori Project Flock Growing */} - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc?.rows ?? []) - : [] - } - columns={docColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.doc?.rows.length > 0 - } - /> - - - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.ovk?.rows ?? []) - : [] - } - columns={ovkColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.ovk?.rows.length > 0 - } - /> - - - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pakan?.rows ?? []) - : [] - } - columns={pakanColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.pakan?.rows.length > 0 - } - /> - -
- ); -}; - -export default ClosingSapronakCalculationTable; diff --git a/src/components/pages/closing/ClosingSapronakTabContent.tsx b/src/components/pages/closing/ClosingSapronakTabContent.tsx deleted file mode 100644 index 03c3c984..00000000 --- a/src/components/pages/closing/ClosingSapronakTabContent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; -import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; -import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable'; -import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable'; - -interface ClosingSapronakTableProps { - projectFlockId?: number; -} - -const ClosingSapronakTabContent = ({ - projectFlockId, -}: ClosingSapronakTableProps) => { - return ( -
- {projectFlockId && ( - <> - - - - - - - - - )} -
- ); -}; - -export default ClosingSapronakTabContent; diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index dc9609ac..12114110 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -1,68 +1,116 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useEffect, useState, useMemo } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; +import StatusBadge from '@/components/helper/StatusBadge'; +import Modal, { useModal } from '@/components/Modal'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { useFormik } from 'formik'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { LocationApi } from '@/services/api/master-data'; import { Location } from '@/types/api/master-data/location'; import { ClosingApi } from '@/services/api/closing'; import { Closing } from '@/types/api/closing'; - -const PROJECT_STATUS_OPTIONS = [ - { - value: 1, - label: 'Pengajuan', - }, - { - value: 2, - label: 'Aktif', - }, -]; +import { Color } from '@/types/theme'; +import { + ClosingFilterSchema, + ClosingFilterType, +} from '@/components/pages/closing/filter/ClosingFilter'; +import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton'; const RowOptionsMenu = ({ - type = 'dropdown', props, + popoverPosition = 'bottom', + detailClickHandler, }: { - type: 'dropdown' | 'collapse'; props: CellContext; + popoverPosition: 'bottom' | 'top'; + detailClickHandler: (id: number) => void; }) => { + const popoverId = `closing#${props.row.original.id}`; + const popoverAnchorName = `--anchor-closing#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + + const detailClickHandlerWrapper = () => { + detailClickHandler(props.row.original.id); + closePopover(); + }; + return ( - -
- - - -
-
+
+ + + + + +
+ + + +
+
+
); }; const ClosingsTable = () => { + // ===== ROUTER ===== + const router = useRouter(); + + // ===== STATUS BADGE COLOR HELPER ===== + const getProjectStatusBadgeColor = (status: string): Color => { + const normalizedValue = status.toLowerCase(); + + if (normalizedValue === 'aktif') { + return 'success'; + } + + if (normalizedValue === 'pengajuan') { + return 'neutral'; + } + + return 'neutral'; + }; + + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + const { state: tableFilterState, updateFilter, @@ -72,36 +120,67 @@ const ClosingsTable = () => { } = useTableFilter({ initial: { search: '', - nameSort: '', - transactionDate: '', - realizationDate: '', - locationId: '', - projectStatus: '', - userId: '', + // nameSort: '', + // transactionDate: '', + // realizationDate: '', + location_id: '', + project_status: '', + // userId: '', }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - transactionDate: 'transaction_date', - realizationDate: 'realization_date', - locationId: 'location_id', - projectStatus: 'project_status', - userId: 'user_id', + // nameSort: 'sort_name', + // transactionDate: 'transaction_date', + // realizationDate: 'realization_date', + // locationId: 'location_id', + // projectStatus: 'project_status', + // userId: 'user_id', + search: 'search', + location_id: 'location_id', + project_status: 'project_status', }, }); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + location_id: null, + project_status: null, + }, + validationSchema: ClosingFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('location_id', values.location_id || ''); + updateFilter('project_status', values.project_status || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('location_id', ''); + updateFilter('project_status', ''); + }, + }); + + // ===== DATA FETCHING ===== const { data: closings, isLoading: isLoadingClosings } = useSWR( `${ClosingApi.basePath}${getTableFilterQueryString()}`, ClosingApi.getAllFetcher ); + const data = useMemo( + () => + isResponseSuccess(closings) ? (closings?.data as Closing[]) || [] : [], + [closings] + ); + + // ===== PAGINATION & STATE ===== const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); + // ===== TABLE COLUMNS ===== const closingsColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { @@ -133,6 +212,19 @@ const ClosingsTable = () => { { accessorKey: 'project_status', header: 'Status', + cell: (props) => { + const status = props.row.original.project_status; + const badgeColor = getProjectStatusBadgeColor(status); + return ( + + ); + }, }, { header: 'Aksi', @@ -142,27 +234,24 @@ const ClosingsTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const detailClickHandler = (id: number) => { + router.push(`/closing/detail/?closingId=${id}`); + }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, ]; + // ===== LOCATION OPTIONS ===== const { setInputValue: setLocationInputValue, options: locationOptions, @@ -170,115 +259,246 @@ const ClosingsTable = () => { loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); - const [selectedLocation, setSelectedLocation] = useState( - null + // ===== PROJECT STATUS OPTIONS ===== + const projectStatusOptions = useMemo( + () => [ + { value: '1', label: 'Pengajuan' }, + { value: '2', label: 'Aktif' }, + ], + [] ); - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'locationId', - val ? ((val as OptionType).value as string) : '' + // ===== FILTER HELPERS ===== + const locationIdValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null ); - }; + }, [formik.values.location_id, locationOptions]); - const [selectedProjectStatus, setSelectedProjectStatus] = - useState(null); - - const projectStatusChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedProjectStatus(val as OptionType); - updateFilter( - 'projectStatus', - val ? ((val as OptionType).value as string) : '' + const projectStatusValue = useMemo(() => { + if (!formik.values.project_status) return null; + return ( + projectStatusOptions.find( + (opt) => opt.value === formik.values.project_status + ) || null ); - }; + }, [formik.values.project_status, projectStatusOptions]); + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (tableFilterState.location_id) { + count += 1; + } + + if (tableFilterState.project_status) { + count += 1; + } + + return count; + }, [tableFilterState.location_id, tableFilterState.project_status]); + + const hasFilters = activeFiltersCount > 0; + + // ===== SEARCH CHANGE HANDLER ===== const searchChangeHandler: ChangeEventHandler = (e) => { updateFilter('search', e.target.value); }; + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); if (!isNameSorted) { - updateFilter('nameSort', ''); + // updateFilter('nameSort', ''); } else { - updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + // updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); } - }, [sorting, updateFilter]); + }, [sorting]); return ( <> -
-
-
-
+
+
+
+
-
- -
- + } className={{ - wrapper: 'col-span-12 sm:col-span-6', + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} /> - +
-
- - data={isResponseSuccess(closings) ? closings?.data : []} - columns={closingsColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={isResponseSuccess(closings) ? closings?.meta?.page : 0} - totalItems={ - isResponseSuccess(closings) ? closings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoadingClosings} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(closings) && closings?.data?.length === 0, - }), - }} - /> + {isLoadingClosings ? ( +
+ +
+ ) : data.length === 0 ? ( + + } + title='Data Closing Belum Tersedia' + subtitle='Tidak ada data closing untuk saat ini.' + /> + ) : ( + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn('mt-3', { + 'w-full mb-0': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'location_id', + val?.value ? String(val.value) : null + ); + } + }} + onInputChange={setLocationInputValue} + isLoading={isLoadingLocationOptions} + isClearable + onMenuScrollToBottom={loadMoreLocations} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('project_status', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/closing/filter/ClosingFilter.ts b/src/components/pages/closing/filter/ClosingFilter.ts new file mode 100644 index 00000000..77f0c9d2 --- /dev/null +++ b/src/components/pages/closing/filter/ClosingFilter.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export type ClosingFilterType = { + location_id: string | null; + project_status: string | null; +}; + +export const ClosingFilterSchema = yup.object({ + location_id: yup.string().nullable(), + project_status: yup.string().nullable(), +}); + +export type ClosingFilterValues = yup.InferType; diff --git a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx deleted file mode 100644 index da89d963..00000000 --- a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx +++ /dev/null @@ -1,109 +0,0 @@ -'use client'; - -import React, { useMemo } from 'react'; -import { ColumnDef } from '@tanstack/react-table'; -import Table from '@/components/Table'; -import Card from '@/components/Card'; -import { formatCurrency } from '@/lib/helper'; -import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing'; - -interface HppExpeditionReportTableProps { - type?: 'detail'; - initialValues?: BaseHppExpedition; -} - -const HppExpeditionReportTable = ({ - initialValues, -}: HppExpeditionReportTableProps) => { - const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { - return initialValues?.expedition_costs || []; - }, [initialValues]); - - const totals = useMemo(() => { - const totalHpp = initialValues?.total_hpp_amount || 0; - - return { - totalHpp, - }; - }, [initialValues]); - - const costOfRevenueExpeditionColumns: ColumnDef[] = - useMemo( - () => [ - { - id: 'id', - accessorKey: 'id', - header: 'No', - cell: (props) => { - return
{props.row.index + 1}
; - }, - footer: () => ( -
- Total HPP Ekspedisi -
- ), - }, - { - id: 'expedition_vendor_name', - accessorKey: 'expedition_vendor_name', - header: 'Nama Ekspedisi', - cell: (props) => props.getValue() || '-', - }, - { - id: 'hpp_amount', - accessorKey: 'hpp_amount', - header: 'HPP Ekspedisi', - cell: (props) => { - const value = props.getValue() as number; - return
{formatCurrency(value)}
; - }, - footer: () => ( -
- {formatCurrency(totals.totalHpp)} -
- ), - }, - ], - [totals] - ); - - return ( - <> -
-
-

HPP Ekspedisi

- - 0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - - - ); -}; - -export default HppExpeditionReportTable; diff --git a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx new file mode 100644 index 00000000..44defca8 --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx @@ -0,0 +1,36 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTabSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ClosingTabSkeleton; diff --git a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx new file mode 100644 index 00000000..4b59510a --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Closing } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTableSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ClosingTableSkeleton; diff --git a/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx new file mode 100644 index 00000000..1168710c --- /dev/null +++ b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const FinanceClosingSkeleton = ({ + title = 'Data Keuangan Belum Tersedia', + subtitle = 'Tidak ada data keuangan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + +
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default FinanceClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx new file mode 100644 index 00000000..d9be9971 --- /dev/null +++ b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppExpeditionClosingSkeleton = ({ + columns, + title = 'Data HPP Ekspedisi Belum Tersedia', + subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default HppExpeditionClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx new file mode 100644 index 00000000..7404f5d2 --- /dev/null +++ b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { Overhead } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const OverheadClosingSkeleton = ({ + columns, + title = 'Data Overhead Belum Tersedia', + subtitle = 'Tidak ada data overhead untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default OverheadClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx new file mode 100644 index 00000000..e0031394 --- /dev/null +++ b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx @@ -0,0 +1,33 @@ +import { Icon } from '@iconify/react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const ProductionDataClosingSkeleton = ({ + title = 'Data Produksi Belum Tersedia', + subtitle = 'Tidak ada data produksi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( +
+
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default ProductionDataClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx new file mode 100644 index 00000000..a9ec35aa --- /dev/null +++ b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseSales } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SalesClosingSkeleton = ({ + columns, + title = 'Data Penjualan Belum Tersedia', + subtitle = 'Tidak ada data penjualan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SalesClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx new file mode 100644 index 00000000..97d4a56c --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { RowSapronakCalculation } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakCalculationClosingSkeleton = ({ + columns, + title = 'Data Perhitungan Sapronak Belum Tersedia', + subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SapronakCalculationClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx new file mode 100644 index 00000000..130cd846 --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakClosingSkeleton = ({ + columns, + type = 'incoming', + title, + subtitle, + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + type?: 'incoming' | 'outgoing'; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultTitle = + type === 'incoming' + ? 'Data Sapronak Masuk Belum Tersedia' + : 'Data Sapronak Keluar Belum Tersedia'; + + const defaultSubtitle = + type === 'incoming' + ? 'Tidak ada data sapronak masuk untuk periode ini.' + : 'Tidak ada data sapronak keluar untuk periode ini.'; + + return ( + + columns={columns} + icon={ + + } + title={title || defaultTitle} + subtitle={subtitle || defaultSubtitle} + /> + ); +}; + +export default SapronakClosingSkeleton; diff --git a/src/components/pages/closing/tab/FinanceClosingTab.tsx b/src/components/pages/closing/tab/FinanceClosingTab.tsx new file mode 100644 index 00000000..53a5068b --- /dev/null +++ b/src/components/pages/closing/tab/FinanceClosingTab.tsx @@ -0,0 +1,13 @@ +import FinanceClosingTable from '@/components/pages/closing/table/FinanceClosingTable'; + +const FinanceClosingTab = ({ projectFlockId }: { projectFlockId: number }) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default FinanceClosingTab; diff --git a/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx new file mode 100644 index 00000000..ad7f0ec1 --- /dev/null +++ b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx @@ -0,0 +1,19 @@ +import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable'; + +interface HppExpeditionClosingTabProps { + projectFlockId: number; +} + +const HppExpeditionClosingTab = ({ + projectFlockId, +}: HppExpeditionClosingTabProps) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default HppExpeditionClosingTab; diff --git a/src/components/pages/closing/ClosingOverheadTabContent.tsx b/src/components/pages/closing/tab/OverheadClosingTab.tsx similarity index 67% rename from src/components/pages/closing/ClosingOverheadTabContent.tsx rename to src/components/pages/closing/tab/OverheadClosingTab.tsx index e6b0cb5a..85942a62 100644 --- a/src/components/pages/closing/ClosingOverheadTabContent.tsx +++ b/src/components/pages/closing/tab/OverheadClosingTab.tsx @@ -1,22 +1,22 @@ -import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable'; +import OverheadClosingTable from '@/components/pages/closing/table/OverheadClosingTable'; import { ClosingGeneralInformation } from '@/types/api/closing'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -interface ClosingOverheadTabContentProps { +interface OverheadClosingTabProps { projectFlockId: number; generalInformation?: ClosingGeneralInformation; kandangData?: ProjectFlockKandang; } -const ClosingOverheadTabContent = ({ +const OverheadClosingTab = ({ projectFlockId, generalInformation, kandangData, -}: ClosingOverheadTabContentProps) => { +}: OverheadClosingTabProps) => { return (
{projectFlockId && ( - { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: productionData, isLoading } = useSWR( + `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, + () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) + ); + + if (isLoading) { + return ; + } + + if (!productionData || !isResponseSuccess(productionData)) { + return ( + + ); + } + + const { purchase, sales, performance } = productionData.data; + + // Helper for consistent row styling + const DataRow = ({ + label, + value, + unit = '', + valueClassName = 'font-bold text-gray-800', + unitClassName = 'text-gray-500 w-12 text-right', + }: { + label: string; + value: string | number; + unit?: string; + valueClassName?: string; + unitClassName?: string; + }) => ( +
+ {label} +
+ {value} + {unit && {unit}} +
+
+ ); + + return ( +
+ +
+
+ {/* Left Column */} +
+ {/* Purchase Section */} +
+

+ Pembelian +

+
+ + + + + +
+
+ + {/* Sales Section */} +
+

+ Penjualan +

+
+ {/* Chicken Sales */} +
+ + + + +
+ + {/* Egg Sales (if available) */} + {sales.egg && ( + <> +
+
+ + + + +
+ + )} +
+
+
+ + {/* Divider Line (Absolute centered) */} +
+ + {/* Right Column */} +
+ {/* Performance Section */} +
+

+ Performance +

+
+ + + + + + {/* + */} + + + + + + + {/* Laying Specific Fields */} + {performance.hen_day_act !== undefined && ( + <> + + + + )} + + {performance.egg_mass !== undefined && ( + <> + + + + )} + + {performance.egg_weight !== undefined && ( + <> + + + + )} + + {performance.hen_housed_act !== undefined && ( + <> + + + + )} +
+
+
+
+
+ +
+ ); +}; + +export default ProductionDataClosingTab; diff --git a/src/components/pages/closing/tab/SalesClosingTab.tsx b/src/components/pages/closing/tab/SalesClosingTab.tsx new file mode 100644 index 00000000..ee343da0 --- /dev/null +++ b/src/components/pages/closing/tab/SalesClosingTab.tsx @@ -0,0 +1,15 @@ +import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable'; + +interface SalesClosingTabProps { + projectFlockId: number; +} + +const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => { + return ( +
+ {projectFlockId && } +
+ ); +}; + +export default SalesClosingTab; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx b/src/components/pages/closing/tab/SapronakCalculationClosingTab.tsx similarity index 56% rename from src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx rename to src/components/pages/closing/tab/SapronakCalculationClosingTab.tsx index b8add15b..77a74d71 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx +++ b/src/components/pages/closing/tab/SapronakCalculationClosingTab.tsx @@ -1,22 +1,22 @@ 'use client'; -import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; +import SapronakCalculationClosingTable from '@/components/pages/closing/table/SapronakCalculationClosingTable'; import { ClosingGeneralInformation } from '@/types/api/closing'; -interface ClosingSapronakCalculationTabContentProps { +interface SapronakCalculationClosingTabProps { projectFlockId?: number; closingGeneralInformation?: ClosingGeneralInformation; } -const ClosingSapronakCalculationTabContent = ({ +const SapronakCalculationClosingTab = ({ projectFlockId, closingGeneralInformation, -}: ClosingSapronakCalculationTabContentProps) => { +}: SapronakCalculationClosingTabProps) => { return (
{projectFlockId && ( <> - @@ -26,4 +26,4 @@ const ClosingSapronakCalculationTabContent = ({ ); }; -export default ClosingSapronakCalculationTabContent; +export default SapronakCalculationClosingTab; diff --git a/src/components/pages/closing/tab/SapronakClosingTab.tsx b/src/components/pages/closing/tab/SapronakClosingTab.tsx new file mode 100644 index 00000000..21bb3b3f --- /dev/null +++ b/src/components/pages/closing/tab/SapronakClosingTab.tsx @@ -0,0 +1,30 @@ +'use client'; + +import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable'; +import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable'; +import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable'; +import OutgoingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable'; + +interface SapronakClosingTabProps { + projectFlockId?: number; +} + +const SapronakClosingTab = ({ projectFlockId }: SapronakClosingTabProps) => { + return ( +
+ {projectFlockId && ( + <> + + + + + + + + + )} +
+ ); +}; + +export default SapronakClosingTab; diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/table/ClosingGeneralInformationTable.tsx similarity index 100% rename from src/components/pages/closing/ClosingGeneralInformationTable.tsx rename to src/components/pages/closing/table/ClosingGeneralInformationTable.tsx diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx new file mode 100644 index 00000000..760fbd04 --- /dev/null +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -0,0 +1,507 @@ +import Alert from '@/components/Alert'; +import Card from '@/components/Card'; +import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatCurrency, formatTitleCase } from '@/lib/helper'; +import { ClosingApi } from '@/services/api/closing'; +import { HppItem, ProfitLossItem } from '@/types/api/closing'; +import { Icon } from '@iconify/react'; +import { useSearchParams } from 'next/navigation'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton'; + +const FinanceClosingTable = ({ + projectFlockId, +}: { + projectFlockId: number; +}) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: finance, isLoading } = useSWR( + `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => + ClosingApi.getFinance( + projectFlockId, + kandangId ? Number(kandangId) : undefined + ) + ); + + const hppTableData: HppItem[] = useMemo(() => { + if (isResponseSuccess(finance)) { + const customItems = { + label: 'HPP dan Pengeluaran', + code: 'custom_row', + } as HppItem; + const purchases = finance.data.hpp.items.filter( + (item) => item.category === 'purchase' + ); + const totalBudgeting = { + label: 'HPP dan Bahan Baku', + code: 'custom_row', + } as HppItem; + const overheads = finance.data.hpp.items.filter( + (item) => item.category === 'overhead' + ); + return [customItems, ...purchases, totalBudgeting, ...overheads]; + } + return []; + }, [finance]); + + const profitLossTableData: ProfitLossItem[] = useMemo(() => { + if (isResponseSuccess(finance)) { + const incomes = finance.data.profit_loss.items.filter( + (item) => item.type === 'income' + ); + const purchases = finance.data.profit_loss.items.filter( + (item) => item.type === 'purchase' + ); + const overheads = finance.data.profit_loss.items.filter( + (item) => item.type === 'overhead' + ); + const grossProfit = { + label: 'LABA RUGI BRUTO', + code: 'custom_row', + type: 'gross_profit', + rp_per_bird: + finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0, + rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0, + amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0, + } as ProfitLossItem; + const subtotal = { + label: 'Subtotal', + code: 'custom_row', + type: 'subtotal', + rp_per_bird: + finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0, + rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0, + amount: finance.data.profit_loss.summary.sub_total.amount ?? 0, + } as ProfitLossItem; + return [...incomes, ...purchases, grossProfit, ...overheads, subtotal]; + } + return []; + }, [finance]); + + return ( +
+ {isLoading ? ( + + ) : !isResponseSuccess(finance) ? ( + + ) : ( + <> +
+ +
+ + + +
+

+ Laba Rugi Brutto +

+

+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.gross_profit.amount + ) + : '-'} +

+
+
+
+ + +
+ + + +
+

+ Laba Rugi Netto +

+

+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit.amount + ) + : '-'} +

+
+
+
+
+ +
+ + data={hppTableData} + isLoading={isLoading} + columns={[ + { + header: 'No.', + enableSorting: false, + accessorFn: (item, index) => { + if (item.code === 'custom_row') return '-'; + const dataRowsBefore = hppTableData + .slice(0, index) + .filter((row) => row.code !== 'custom_row').length; + return dataRowsBefore + 1; + }, + footer: () => { + return 'HPP'; + }, + }, + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => formatTitleCase(item.label || '-'), + }, + { + header: 'Budgeting', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'budgeting_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_bird || 0 + ) + : '-'; + }, + }, + { + header: 'Rp/Kg', + id: 'budgeting_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_kg || 0 + ) + : '-'; + }, + }, + { + header: 'Jumlah (Rp)', + id: 'budgeting_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.amount || 0), + footer: (props) => { + return props.column.id === 'budgeting_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting?.amount || 0 + ) + : '-'; + }, + }, + ], + }, + { + header: 'Realization', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'realization_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === + 'realization_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_bird || 0 + ) + : '-'; + }, + }, + { + header: 'Rp/Kg', + id: 'realization_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'realization_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_kg || 0 + ) + : '-'; + }, + }, + { + header: 'Jumlah (Rp)', + id: 'realization_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.amount || 0), + footer: (props) => { + return props.column.id === 'realization_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization?.amount || + 0 + ) + : '-'; + }, + }, + ], + }, + ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + renderCustomRow={(row) => { + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
+ + + + ); + } + return null; + }} + renderFooter={isResponseSuccess(finance)} + /> + + + +
+ + data={profitLossTableData} + isLoading={isLoading} + columns={[ + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => item.label, + cell: (item) => ( +
+ {formatTitleCase(item.row.original.label || '-')} +
+ ), + footer: () => ( +
LABA RUGI NETTO
+ ), + }, + { + header: 'Rp/Ekor', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_bird || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Rp/Kg', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_kg || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Jumlah (Rp)', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.amount || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .amount || 0 + ) + : formatCurrency(0)} +
+ ), + }, + ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + renderCustomRow={(row) => { + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
+ + + + + + ); + } + return null; + }} + renderFooter={isResponseSuccess(finance)} + /> + + + + )} + + ); +}; + +export default FinanceClosingTable; diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx new file mode 100644 index 00000000..5389e3d5 --- /dev/null +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -0,0 +1,147 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import Table from '@/components/Table'; +import Card from '@/components/Card'; +import { formatCurrency } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import HppExpeditionClosingSkeleton from '@/components/pages/closing/skeleton/HppExpeditionClosingSkeleton'; + +interface HppExpeditionClosingTableProps { + projectFlockId: number; +} + +const HppExpeditionClosingTable = ({ + projectFlockId, +}: HppExpeditionClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: hppExpedition, isLoading } = useSWR( + kandangId + ? `/closing/hpp-expedition/${projectFlockId}/${kandangId}` + : `/closing/hpp-expedition/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getHppEkspedisiByKandang(projectFlockId, Number(kandangId)) + : ClosingApi.getHppEkspedisi(projectFlockId) + ); + + const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { + if (isResponseSuccess(hppExpedition)) { + return hppExpedition.data.expedition_costs || []; + } + return []; + }, [hppExpedition]); + + const totals = useMemo(() => { + if (isResponseSuccess(hppExpedition)) { + return { + totalHpp: hppExpedition.data.total_hpp_amount || 0, + }; + } + return { + totalHpp: 0, + }; + }, [hppExpedition]); + + const costOfRevenueExpeditionColumns: ColumnDef[] = + useMemo( + () => [ + { + id: 'id', + accessorKey: 'id', + header: 'No', + cell: (props) => { + return
{props.row.index + 1}
; + }, + footer: () => ( +
+ Total HPP Ekspedisi +
+ ), + }, + { + id: 'expedition_vendor_name', + accessorKey: 'expedition_vendor_name', + header: 'Nama Ekspedisi', + cell: (props) => props.getValue() || '-', + }, + { + id: 'hpp_amount', + accessorKey: 'hpp_amount', + header: 'HPP Ekspedisi', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalHpp)} +
+ ), + }, + ], + [totals] + ); + + return ( +
+ + {isLoading ? ( + + ) : costOfRevenueExpeditionData.length === 0 ? ( + + ) : ( +
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatCurrency(rowData.rp_per_bird ?? 0)} +
+
+
+ {formatCurrency(rowData.rp_per_kg ?? 0)} +
+
+
+ {formatCurrency(rowData.amount ?? 0)} +
+
0} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + + + ); +}; + +export default HppExpeditionClosingTable; diff --git a/src/components/pages/closing/ClosingOverheadTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx similarity index 73% rename from src/components/pages/closing/ClosingOverheadTable.tsx rename to src/components/pages/closing/table/OverheadClosingTable.tsx index a7a170eb..a6c31e6c 100644 --- a/src/components/pages/closing/ClosingOverheadTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -14,18 +14,19 @@ import { ColumnDef } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import { useMemo } from 'react'; import useSWR from 'swr'; +import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton'; -interface ClosingOverheadTableProps { +interface OverheadClosingTableProps { projectFlockId: number; generalInformation?: ClosingGeneralInformation; kandangData?: ProjectFlockKandang; } -const ClosingOverheadTable = ({ +const OverheadClosingTable = ({ projectFlockId, generalInformation, kandangData, -}: ClosingOverheadTableProps) => { +}: OverheadClosingTableProps) => { const searchParams = useSearchParams(); const kandangId = searchParams.get('kandangId'); @@ -208,42 +209,84 @@ const ClosingOverheadTable = ({ ); return ( - <> +
- - data={ - kandangId - ? isResponseSuccess(overheadKandang) - ? (overheadKandang.data?.overheads ?? []) - : [] - : isResponseSuccess(overhead) - ? (overhead.data?.overheads ?? []) - : [] - } - columns={columns} - className={{ - containerClassName: 'my-4', - headerColumnClassName: cn( - TABLE_DEFAULT_STYLING.headerColumnClassName, - 'whitespace-nowrap' - ), - }} - isLoading={isLoadingOverhead} - renderFooter={ - isResponseSuccess(overhead) - ? overhead.data?.overheads.length > 0 - : false - } - /> - {kandangId && ( + {isLoadingOverhead ? ( + + ) : !isResponseSuccess(overhead) ? ( + + ) : kandangId && !isResponseSuccess(overheadKandang) ? ( + + ) : (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && + isResponseSuccess(overheadKandang) && + overheadKandang.data?.overheads.length === 0) ? ( + + ) : ( + + data={ + kandangId + ? isResponseSuccess(overheadKandang) + ? (overheadKandang.data?.overheads ?? []) + : [] + : isResponseSuccess(overhead) + ? (overhead.data?.overheads ?? []) + : [] + } + columns={columns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: cn( + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + 'whitespace-nowrap' + ), + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + isLoading={isLoadingOverhead} + renderFooter={ + isResponseSuccess(overhead) + ? overhead.data?.overheads.length > 0 + : false + } + /> + )} + {kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && ( )} - +
); }; -export default ClosingOverheadTable; +export default OverheadClosingTable; diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx similarity index 73% rename from src/components/pages/closing/sale/SalesReportTable.tsx rename to src/components/pages/closing/table/SalesClosingTable.tsx index 0632676b..5105d965 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -5,28 +5,47 @@ import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { - BaseClosingSales, - BaseSales, - ClosingSalesSummary, -} from '@/types/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseSales, ClosingSalesSummary } from '@/types/api/closing'; import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton'; -interface SalesReportTableProps { - type?: 'detail'; - initialValues?: BaseClosingSales; +interface SalesClosingTableProps { + projectFlockId: number; } -const SalesReportTable = ({ initialValues }: SalesReportTableProps) => { +const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: sales, isLoading } = useSWR( + kandangId + ? `/closing/sales/${projectFlockId}/${kandangId}` + : `/closing/sales/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId)) + : ClosingApi.getPenjualan(projectFlockId) + ); + const salesData: BaseSales[] = useMemo(() => { - return initialValues?.sales || []; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.sales || []; + } + return []; + }, [sales]); const summary: ClosingSalesSummary | undefined = useMemo(() => { - return initialValues?.summary; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.summary; + } + return undefined; + }, [sales]); const totals = useMemo(() => { if (salesData.length === 0) { @@ -293,41 +312,55 @@ const SalesReportTable = ({ initialValues }: SalesReportTableProps) => { ); return ( - <> -
-
-

Penjualan

- + + {isLoading ? ( + + ) : salesData.length === 0 ? ( + + ) : ( +
0} className={{ - wrapper: 'w-full bg-base-100', - body: 'p-0', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} - > -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - - + /> + )} + + ); }; -export default SalesReportTable; +export default SalesClosingTable; diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx new file mode 100644 index 00000000..6f1252fc --- /dev/null +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -0,0 +1,359 @@ +'use client'; + +import Card from '@/components/Card'; + +import Table from '@/components/Table'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + RowSapronakCalculation, + TotalSapronakCalculation, +} from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { ClosingGeneralInformation } from '@/types/api/closing'; +import { useSearchParams } from 'next/navigation'; +import SapronakCalculationClosingSkeleton from '@/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton'; + +interface SapronakCalculationClosingTableProps { + projectFlockId: number; + closingGeneralInformation?: ClosingGeneralInformation; +} + +const SapronakCalculationClosingTable = ({ + projectFlockId, + closingGeneralInformation, +}: SapronakCalculationClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: sapronakCalculation, isLoading } = useSWR( + `/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)), + { + keepPreviousData: true, + } + ); + + // Helper function to create columns with footer support + const createColumns = ( + total?: TotalSapronakCalculation + ): ColumnDef[] => [ + { + header: 'Tanggal', + accessorKey: 'date', + cell: (props) => + props.row.original.date + ? formatDate(props.row.original.date, 'DD MMM YYYY') + : '-', + footer: 'Total', + }, + { + header: 'No. Referensi', + accessorKey: 'reference_number', + cell: (props) => (props.row.original.reference_number as string) || '-', + footer: '', + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_in', + cell: (props) => + props.row.original.qty_in + ? formatNumber(props.row.original.qty_in as number) + : '0', + footer: total + ? () => ( +
+ {total?.qty_in ? formatNumber(total?.qty_in) : '0'} +
+ ) + : '', + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_out', + cell: (props) => + props.row.original.qty_out + ? formatNumber(props.row.original.qty_out as number) + : '0', + footer: total + ? () => ( +
+ {total?.qty_out ? formatNumber(total?.qty_out) : '0'} +
+ ) + : '', + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_used', + cell: (props) => + props.row.original.qty_used + ? formatNumber(props.row.original.qty_used as number) + : '0', + footer: total + ? () => ( +
+ {total?.qty_used ? formatNumber(total?.qty_used) : '0'} +
+ ) + : '', + }, + { + header: 'Uraian', + accessorKey: 'description', + cell: (props) => (props.row.original.description as string) || '-', + footer: '', + }, + { + header: 'Kategori Produk', + accessorKey: 'product_category', + cell: (props) => (props.row.original.product_category as string) || '-', + footer: '', + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'unit_price', + cell: (props) => + props.row.original.unit_price + ? formatCurrency(props.row.original.unit_price as number) + : '-', + footer: total + ? () => ( +
+ {total?.avg_unit_price + ? formatCurrency(total?.avg_unit_price) + : '-'} +
+ ) + : '', + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_amount', + cell: (props) => + props.row.original.total_amount + ? formatCurrency(props.row.original.total_amount as number) + : '-', + footer: total + ? () => ( +
+ {total?.total_amount ? formatCurrency(total?.total_amount) : '-'} +
+ ) + : '', + }, + { + header: 'Keterangan', + accessorKey: 'notes', + cell: (props) => (props.row.original.notes as string) || '-', + footer: '', + }, + ]; + + // Memoize columns untuk setiap kategori + const docColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.doc?.total) + : createColumns(), + [sapronakCalculation] + ); + + const ovkColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.ovk?.total) + : createColumns(), + [sapronakCalculation] + ); + + const pakanColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.pakan?.total) + : createColumns(), + [sapronakCalculation] + ); + + return ( +
+ {/* Table DOC jika kategori Project Flock Growing */} + + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.doc?.rows ?? []) + : [] + } + columns={docColumns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length > 0 + } + /> + )} + + + + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.ovk?.rows ?? []) + : [] + } + columns={ovkColumns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length > 0 + } + /> + )} + + + + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.pakan?.rows ?? []) + : [] + } + columns={pakanColumns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length > 0 + } + /> + )} + +
+ ); +}; + +export default SapronakCalculationClosingTable; diff --git a/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx similarity index 50% rename from src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 49e4f108..78773dbb 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -1,20 +1,20 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; import { cn, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksSummaryTableProps { projectFlockId: number; @@ -55,20 +55,60 @@ const ClosingIncomingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { accessorKey: 'category', header: 'Kategori', + cell: (props) => { + const categories = props.row.original.category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'total_qty', @@ -78,10 +118,6 @@ const ClosingIncomingSapronaksSummaryTable = ({ }, ]; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -93,44 +129,35 @@ const ClosingIncomingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries.data.length > 0 - : false - ); - } - }, [incomingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Masuk
- - - - } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
+ {isLoadingIncomingSapronakSummaries ? ( + + ) : isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(incomingSapronakSummaries) @@ -158,16 +185,21 @@ const ClosingIncomingSapronaksSummaryTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
- -
+ )} + +
); }; diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx similarity index 51% rename from src/components/pages/closing/ClosingIncomingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 3d3a9d70..b0bd2744 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -9,13 +9,14 @@ import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; import { cn, formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksTableProps { projectFlockId: number; @@ -51,14 +52,12 @@ const ClosingIncomingSapronaksTable = ({ ClosingApi.getAllIncomingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { @@ -81,6 +80,48 @@ const ClosingIncomingSapronaksTable = ({ { accessorKey: 'product_category', header: 'Kategori Produk', + cell: (props) => { + const categories = props.row.original.product_category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'source_warehouse', @@ -117,56 +158,59 @@ const ClosingIncomingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks.data.length > 0 - : false - ); - } - }, [incomingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Masuk
- - + +
+
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
- } - className='w-full!' - titleClassName='w-full p-0!' - > -
-
-
- -
-
+
+ {isLoadingIncomingSapronaks ? ( + + ) : isResponseSuccess(incomingSapronaks) && + incomingSapronaks.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(incomingSapronaks) @@ -194,16 +238,21 @@ const ClosingIncomingSapronaksTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronaks) && - incomingSapronaks?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
-
-
+ )} +
+ ); }; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx similarity index 50% rename from src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index 42fcb588..b50b012b 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -1,20 +1,20 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; import { cn, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksSummaryTableProps { projectFlockId: number; @@ -55,20 +55,60 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { accessorKey: 'category', header: 'Kategori', + cell: (props) => { + const categories = props.row.original.category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'total_qty', @@ -78,10 +118,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({ }, ]; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -93,44 +129,35 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries.data.length > 0 - : false - ); - } - }, [outgoingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Keluar
- - - - } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
+ {isLoadingOutgoingSapronakSummaries ? ( + + ) : isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(outgoingSapronakSummaries) @@ -158,16 +185,21 @@ const ClosingOutgoingSapronaksSummaryTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
- -
+ )} + +
); }; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx similarity index 50% rename from src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index acbbc52d..23e3e8b0 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -9,13 +9,16 @@ import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { cn } from '@/lib/helper'; + +import { formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksTableProps { projectFlockId: number; @@ -51,14 +54,12 @@ const ClosingOutgoingSapronaksTable = ({ ClosingApi.getAllOutgoingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { @@ -81,6 +82,48 @@ const ClosingOutgoingSapronaksTable = ({ { accessorKey: 'product_category', header: 'Kategori Produk', + cell: (props) => { + const categories = props.row.original.product_category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'source_warehouse', @@ -117,56 +160,59 @@ const ClosingOutgoingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks.data.length > 0 - : false - ); - } - }, [outgoingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Keluar
- - + +
+
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
- } - className='w-full!' - titleClassName='w-full p-0!' - > -
-
-
- -
-
+
+ {isLoadingOutgoingSapronaks ? ( + + ) : isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(outgoingSapronaks) @@ -194,16 +240,21 @@ const ClosingOutgoingSapronaksTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
-
-
+ )} +
+ ); }; diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index e800ee68..bdffda33 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -2,8 +2,6 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import Card from '@/components/Card'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useModal } from '@/components/Modal'; -import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; @@ -13,6 +11,7 @@ import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandan import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const ChickinLogsView = ({ initialValues, @@ -23,32 +22,26 @@ const ChickinLogsView = ({ afterSubmit?: () => void; rawDataApprovals: BaseApproval[]; }) => { - const confirmModal = useModal(); - const [isApproveLoading, setIsApproveLoading] = useState(false); const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + const { openChickinApproveModal } = useChickinStore(); const handleClickApprove = () => { - confirmModal.openModal(); - }; - - const confirmationModalApproveClickHandler = async (notes?: string) => { - setChickinErrorMessage(''); - setIsApproveLoading(true); - const approveChickinRes = await ChickinApi.singleApproval( - initialValues?.id as number, - 'APPROVED', - notes - ); - if (isResponseSuccess(approveChickinRes)) { - toast.success(approveChickinRes?.message as string); - } - if (isResponseError(approveChickinRes)) { - toast.error(approveChickinRes?.message as string); - setChickinErrorMessage(approveChickinRes?.message as string); - } - confirmModal.closeModal(); - setIsApproveLoading(false); - afterSubmit && afterSubmit(); + openChickinApproveModal(initialValues, async (notes?: string) => { + setChickinErrorMessage(''); + const approveChickinRes = await ChickinApi.singleApproval( + initialValues?.id as number, + 'APPROVED', + notes + ); + if (isResponseSuccess(approveChickinRes)) { + toast.success(approveChickinRes?.message as string); + } + if (isResponseError(approveChickinRes)) { + toast.error(approveChickinRes?.message as string); + setChickinErrorMessage(approveChickinRes?.message as string); + } + afterSubmit && afterSubmit(); + }); }; return ( @@ -83,7 +76,7 @@ const ChickinLogsView = ({ key={chickin.id || index} variant='bordered' className={{ - wrapper: 'w-full', + wrapper: 'w-full mt-3', body: 'p-3', }} > @@ -176,23 +169,6 @@ const ChickinLogsView = ({ )} - - { - confirmationModalApproveClickHandler(notes); - }, - isLoading: isApproveLoading, - }} - /> ); }; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index e8280fa8..040948ff 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -36,6 +36,7 @@ import PopoverContent from '@/components/popover/PopoverContent'; import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const RowOptionsMenu = ({ props, @@ -193,6 +194,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const deleteModal = useModal(); const confirmModal = useModal(); const successModal = useModal(); + const chickinApproveModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); @@ -200,6 +202,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const [isApproveLoading, setIsApproveLoading] = useState(false); const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const { + isChickinApproveModalOpen, + isChickinApproveLoading, + chickinApproveCallback, + closeChickinApproveModal, + setChickinApproveLoading, + } = useChickinStore(); // ===== Fetch Data ===== const { @@ -271,7 +280,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); if (isResponseSuccess(approveProjectFlockRes)) { - toast.success('Project Flock berhasil di-approve!'); + const successMessage = + approvalAction === 'APPROVED' + ? 'Project Flock berhasil di-approve!' + : 'Project Flock berhasil di-reject!'; + toast.success(successMessage); confirmModal.closeModal(); } if (isResponseError(approveProjectFlockRes)) { @@ -288,6 +301,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { refreshProjectFlocks(); }, [refresh]); + useEffect(() => { + if (isChickinApproveModalOpen) { + chickinApproveModal.openModal(); + } else { + chickinApproveModal.closeModal(); + } + }, [isChickinApproveModalOpen, chickinApproveModal]); + useEffect(() => { if (isSuccess) { successModal.openModal(); @@ -970,6 +991,40 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { onClose={handleSuccessModalClose} secondaryButton={undefined} /> + + {/* Chickin Approval Modal */} + { + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + }, + }} + primaryButton={{ + text: 'Ya', + color: 'success', + onClick: async (notes) => { + if (chickinApproveCallback) { + setChickinApproveLoading(true); + try { + await chickinApproveCallback(notes); + } finally { + setChickinApproveLoading(false); + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + } + } + }, + isLoading: isChickinApproveLoading, + }} + /> ); }; diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a67f44f9..65e658f9 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -38,38 +38,26 @@ import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', - Disetujui: 'Disetujui', REJECTED: 'Ditolak', - Ditolak: 'Ditolak', - CREATED: 'Dibuat', + CREATED: 'Pengajuan', UPDATED: 'Diperbarui', }; const getStatusText = (status: string): string => { - return statusTextMap[status] || status; + const normalizedStatus = status.toUpperCase(); + return statusTextMap[normalizedStatus] || status; }; const statusBadgeColorMap: Record = { APPROVED: 'success', - Disetujui: 'success', - approved: 'success', - disetujui: 'success', REJECTED: 'error', - Ditolak: 'error', - rejected: 'error', - ditolak: 'error', CREATED: 'neutral', - Dibuat: 'neutral', - created: 'neutral', - dibuat: 'neutral', UPDATED: 'warning', - Diperbarui: 'warning', - updated: 'warning', - diperbarui: 'warning', }; const getStatusBadgeColor = (status: string): Color => { - return statusBadgeColorMap[status] || 'neutral'; + const normalizedStatus = status.toUpperCase(); + return statusBadgeColorMap[normalizedStatus] || 'neutral'; }; const RowOptionsMenu = ({ @@ -852,8 +840,7 @@ const RecordingTable = () => { const status = approval.action; const statusColor = getStatusBadgeColor(status); - - const statusText = approval.step_name || getStatusText(status); + const statusText = getStatusText(status); return ( ()( + devtools( + (...args) => ({ + ...createClosingTabSlice(...args), + }), + { + name: 'ClosingTabStore', + } + ) +); diff --git a/src/stores/closing/slices/closing-tab.slice.ts b/src/stores/closing/slices/closing-tab.slice.ts new file mode 100644 index 00000000..cd47bbdc --- /dev/null +++ b/src/stores/closing/slices/closing-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ClosingTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createClosingTabSlice: StateCreator< + ClosingTabSlice, + [], + [], + ClosingTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); diff --git a/src/stores/production/chickin/chickin.store.ts b/src/stores/production/chickin/chickin.store.ts new file mode 100644 index 00000000..697b1de4 --- /dev/null +++ b/src/stores/production/chickin/chickin.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; +import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; + +export type ChickinStore = ChickinApprovalSlice; + +export const useChickinStore = create()( + devtools( + (...args) => ({ + ...createChickinApprovalSlice(...args), + }), + { + name: 'ChickinStore', + } + ) +); diff --git a/src/stores/production/chickin/slices/chickin-approval.slice.ts b/src/stores/production/chickin/slices/chickin-approval.slice.ts new file mode 100644 index 00000000..30f0a857 --- /dev/null +++ b/src/stores/production/chickin/slices/chickin-approval.slice.ts @@ -0,0 +1,58 @@ +import { StateCreator } from 'zustand'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; + +export type ChickinApprovalSlice = { + // State + isChickinApproveModalOpen: boolean; + selectedChickinForApproval: ProjectFlockKandang | null; + isChickinApproveLoading: boolean; + chickinApproveCallback: ((notes?: string) => Promise) | null; + + // Actions + openChickinApproveModal: ( + data: ProjectFlockKandang, + callback: (notes?: string) => Promise + ) => void; + closeChickinApproveModal: () => void; + setChickinApproveLoading: (loading: boolean) => void; + resetChickinApproval: () => void; +}; + +export const createChickinApprovalSlice: StateCreator< + ChickinApprovalSlice, + [], + [], + ChickinApprovalSlice +> = (set) => ({ + // Initial state + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + + // Actions + openChickinApproveModal: (data, callback) => + set({ + isChickinApproveModalOpen: true, + selectedChickinForApproval: data, + chickinApproveCallback: callback, + }), + + closeChickinApproveModal: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + chickinApproveCallback: null, + }), + + setChickinApproveLoading: (loading) => + set({ isChickinApproveLoading: loading }), + + resetChickinApproval: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + }), +});