Merge branch 'fix/closing-project-flock-recording-issue' into 'development'

[FIX/FE] Adjustment Closing Related (Closing Report, Closing Detail and Closing Table), Project Flock Detail (Form, Chickin), Recording Table

See merge request mbugroup/lti-web-client!327
This commit is contained in:
Rivaldi A N S
2026-02-19 09:45:18 +00:00
46 changed files with 2913 additions and 1748 deletions
+2 -40
View File
@@ -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 (
<div className='w-full p-4 flex flex-row justify-center'>
@@ -91,12 +59,6 @@ const ClosingDetailPage = () => {
<ClosingDetail
id={Number(closingId)}
initialValue={closing.data}
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
hppExpeditionData={
isResponseSuccess(hppEkspedisiData)
? hppEkspedisiData.data
: undefined
}
projectData={
isResponseSuccess(projectData) ? projectData.data : undefined
}
+1 -1
View File
@@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable';
const Closing = () => {
return (
<section className='w-full p-4'>
<section className='w-full p-3'>
<ClosingsTable />
</section>
);
@@ -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<ClosingDetailProps> = ({
id,
initialValue,
salesData,
hppExpeditionData,
projectData,
kandangData,
}) => {
const [activeTab, setActiveTab] = useState<string>('sapronak');
const [activeTabId, setActiveTabId] = useState<string>('sapronak');
const tabActions = useClosingTabStore((state) => state.tabActions);
const closingDetailTabs = useMemo(() => {
const validTabs = [
{
id: 'sapronak',
label: 'Sapronak',
content: <ClosingSapronakTabContent projectFlockId={id} />,
content: <SapronakClosingTab projectFlockId={id} />,
},
{
id: 'perhitunganSapronak',
label: 'Perhitungan Sapronak',
content: (
<ClosingSapronakCalculationTabContent
<SapronakCalculationClosingTab
closingGeneralInformation={initialValue}
projectFlockId={id}
/>
@@ -61,13 +55,13 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{
id: 'penjualan',
label: 'Penjualan',
content: <SalesReportTable initialValues={salesData} />,
content: <SalesClosingTab projectFlockId={id} />,
},
{
id: 'overhead',
label: 'Overhead',
content: (
<ClosingOverheadTabContent
<OverheadClosingTab
projectFlockId={id}
generalInformation={initialValue}
kandangData={kandangData}
@@ -77,26 +71,26 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{
id: 'hppEkspedisi',
label: 'HPP Ekspedisi',
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
content: <HppExpeditionClosingTab projectFlockId={id} />,
},
{
id: 'dataProduksi',
label: 'Data Produksi',
content: <ClosingProductionDataTabContent projectFlockId={id} />,
content: <ProductionDataClosingTab projectFlockId={id} />,
},
{
id: 'keuangan',
label: 'Keuangan',
content: <ClosingFinanceTabContent projectFlockId={id} />,
content: <FinanceClosingTab projectFlockId={id} />,
},
];
return validTabs;
}, [initialValue]);
}, [initialValue, kandangData, id]);
return (
<>
<section className='w-full max-w-7xl pb-16'>
<section className='w-full'>
<header className='flex flex-col gap-4'>
<Button
href={
@@ -126,13 +120,17 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
)}
<Tabs
activeTabId={activeTab}
onTabChange={setActiveTab}
activeTabId={activeTabId}
onTabChange={setActiveTabId}
tabs={closingDetailTabs}
variant='lifted'
variant='boxed'
className={{
wrapper: 'w-full mt-4',
tabHeaderWrapper:
'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10',
tab: 'w-fit',
content: 'p-0 m-0',
}}
sideContent={tabActions[activeTabId] || null}
/>
</section>
</>
@@ -1,17 +0,0 @@
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
const ClosingFinanceTabContent = ({
projectFlockId,
}: {
projectFlockId: number;
}) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<ClosingFinanceTable projectFlockId={projectFlockId} />
)}
</div>
);
};
export default ClosingFinanceTabContent;
@@ -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 (
<div className='flex flex-col gap-4'>
<>
<Card
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='grid grid-cols-2 gap-6'>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Brutto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.gross_profit.amount
)
: '-'}
</div>
</div>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Netto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit.amount
)
: '-'}
</div>
</div>
</div>
</Card>
<Card
title='HPP Purchases'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<HppItem>
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 (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
></td>
<td
colSpan={7}
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
<Card
title='Profit/Loss'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<ProfitLossItem>
data={profitLossTableData}
isLoading={isLoading}
columns={[
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.label,
cell: (item) => (
<div className=''>
{formatTitleCase(item.row.original.label || '-')}
</div>
),
footer: () => (
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
),
},
{
header: 'Rp/Ekor',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Rp/Kg',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Jumlah (Rp)',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.amount || 0
)
: formatCurrency(0)}
</div>
),
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.footerRowClassName}
>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_bird ?? 0)}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_kg ?? 0)}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.amount ?? 0)}
</div>
</td>
</tr>
);
}
return null;
}}
className={{
paginationClassName: 'hidden',
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
</>
</div>
);
};
export default ClosingFinanceTable;
@@ -10,18 +10,18 @@ const ClosingKandangList = ({
projectData?: ProjectFlock;
}) => {
return (
<div className='w-full my-4 @container'>
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
<div className='flex flex-col @sm:flex-row gap-4'>
<div className='w-full'>
<div className='overflow-x-auto'>
<h1 className='font-bold my-4'>Kandang</h1>
<div className='flex flex-wrap gap-2 mb-4'>
<h1 className='font-bold mb-3'>Kandang</h1>
<div className='flex flex-wrap gap-2'>
{projectData?.kandangs?.map((kandang) => (
<Button
key={kandang.id}
variant='outline'
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
className='min-w-32'
>
{kandang.name}
</Button>
@@ -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 (
<div className='w-full flex justify-center py-8'>
<span className='loading loading-spinner loading-lg' />
</div>
);
}
if (!productionData || !isResponseSuccess(productionData)) {
return (
<div className='w-full text-center py-8 text-gray-500'>
Gagal memuat data produksi.
</div>
);
}
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;
}) => (
<div className='flex justify-between items-center py-1'>
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
<div className='flex gap-2 w-1/2 justify-end items-center'>
<span className={valueClassName}>{value}</span>
{unit && <span className={unitClassName}>{unit}</span>}
</div>
</div>
);
return (
<div className='w-full rounded-xl p-8 shadow-sm'>
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
{/* Left Column */}
<div className='space-y-10'>
{/* Purchase Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Pembelian
</h3>
<div className='space-y-1'>
<DataRow
label='Populasi Awal'
value={formatNumber(purchase.initial_population)}
unit='Ekor'
/>
<DataRow
label='Claim Culling'
value={formatNumber(purchase.claim_culling)}
unit='Ekor'
/>
<DataRow
label='Populasi Akhir'
value={formatNumber(purchase.final_population)}
unit='Ekor'
/>
<DataRow
label='Pakan Masuk'
value={formatNumber(purchase.feed_in)}
unit='Kg'
/>
<DataRow
label='Pakan Terpakai'
value={formatNumber(purchase.feed_used)}
unit='Kg'
/>
</div>
</section>
{/* Sales Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Penjualan
</h3>
<div className='space-y-4'>
{/* Chicken Sales */}
<div className='space-y-1'>
<DataRow
label='Penjualan (Ekor)'
value={formatNumber(sales.chicken.sales_population)}
unit='Ekor'
/>
<DataRow
label='Penjualan (Kg)'
value={formatNumber(sales.chicken.sales_weight)}
unit='Kg'
/>
<DataRow
label='Bobot Rata-Rata'
value={formatNumber(sales.chicken.avg_weight)}
unit='Kg/Ekor'
/>
<DataRow
label='Harga Jual Rata-Rata'
value={formatNumber(sales.chicken.avg_selling_price)}
unit='Rupiah'
/>
</div>
{/* Egg Sales (if available) */}
{sales.egg && (
<>
<div className='h-px bg-gray-100 my-2' />
<div className='space-y-1'>
<DataRow
label='Telur (Butir)'
value={formatNumber(sales.egg.egg_pieces)}
unit='Butir'
/>
<DataRow
label='Telur (Kg)'
value={formatNumber(sales.egg.egg_mass)}
unit='Kg'
/>
<DataRow
label='Berat Telur Rata-Rata'
value={formatNumber(sales.egg.avg_egg_weight)}
unit='Kg'
/>
<DataRow
label='Harga Jual Telur Rata-Rata'
value={formatNumber(sales.egg.avg_selling_price)}
unit='Rupiah'
/>
</div>
</>
)}
</div>
</section>
</div>
{/* Divider Line (Absolute centered) */}
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
{/* Right Column */}
<div className='space-y-10 flex flex-col h-full'>
{/* Performance Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Performance
</h3>
<div className='space-y-1'>
<DataRow
label='Deplesi'
value={formatNumber(performance.depletion)}
unit='Ekor'
/>
<DataRow
label='Umur'
value={formatNumber(performance.age_day)}
unit='Hari'
/>
<DataRow
label='Mortalitas Std'
value={formatNumber(performance.mor_std)}
unitClassName='hidden'
/>
<DataRow
label='Mortalitas Act'
value={formatNumber(performance.mor_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF Mortalitas'
value={formatNumber(performance.mor_diff)}
unitClassName='hidden'
/>
{/* <DataRow
label='AWG Std'
value={formatNumber(performance.awg_std)}
unit='Gr/Hari'
/>
<DataRow
label='AWG Act'
value={formatNumber(performance.awg_act)}
unit='Gr/Hari'
/> */}
<DataRow
label='Feed Intake Std'
value={formatNumber(performance.feed_intake_std)}
unitClassName='hidden'
/>
<DataRow
label='Feed Intake Act'
value={formatNumber(performance.feed_intake)}
unitClassName='hidden'
/>
<DataRow
label='FCR Std'
value={formatNumber(performance.fcr_std)}
unitClassName='hidden'
/>
<DataRow
label='FCR Act'
value={formatNumber(performance.fcr_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF FCR'
value={formatNumber(performance.fcr_diff)}
unitClassName='hidden'
/>
{/* Laying Specific Fields */}
{performance.hen_day_act !== undefined && (
<>
<DataRow
label='Hen Day Std'
value={formatNumber(performance.hen_day_std!)}
unit='%'
/>
<DataRow
label='Hen Day Act'
value={formatNumber(performance.hen_day_act)}
unit='%'
/>
</>
)}
{performance.egg_mass !== undefined && (
<>
<DataRow
label='Egg Mass Std'
value={formatNumber(performance.egg_mass_std!)}
unit='Kg'
/>
<DataRow
label='Egg Mass Act'
value={formatNumber(performance.egg_mass)}
unit='Kg'
/>
</>
)}
{performance.egg_weight !== undefined && (
<>
<DataRow
label='Egg Weight Std'
value={formatNumber(performance.egg_weight_std!)}
unit='Gr'
/>
<DataRow
label='Egg Weight Act'
value={formatNumber(performance.egg_weight)}
unit='Gr'
/>
</>
)}
{performance.hen_housed_act !== undefined && (
<>
<DataRow
label='Hen Housed Std'
value={formatNumber(performance.hen_housed_std!)}
unit='%'
/>
<DataRow
label='Hen Housed Act'
value={formatNumber(performance.hen_housed_act)}
unit='%'
/>
</>
)}
</div>
</section>
</div>
</div>
</div>
);
};
export default ClosingProductionDataTabContent;
@@ -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<RowSapronakCalculation>[] => [
{
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
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_out',
cell: (props) =>
props.row.original.qty_out
? formatNumber(props.row.original.qty_out as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_used',
cell: (props) =>
props.row.original.qty_used
? formatNumber(props.row.original.qty_used as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
</div>
)
: '',
},
{
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
? () => (
<div className='font-semibold text-gray-900'>
{total?.avg_unit_price
? formatCurrency(total?.avg_unit_price)
: '-'}
</div>
)
: '',
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_amount',
cell: (props) =>
props.row.original.total_amount
? formatCurrency(props.row.original.total_amount as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
</div>
)
: '',
},
{
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 (
<div className='flex flex-col gap-4'>
{/* Table DOC jika kategori Project Flock Growing */}
<Card
title={
closingGeneralInformation?.project_type == 'GROWING'
? 'DOC'
: 'Pullet'
}
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc?.rows ?? [])
: []
}
columns={docColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.doc?.rows.length > 0
}
/>
</Card>
<Card
title='OVK'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.ovk?.rows ?? [])
: []
}
columns={ovkColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.ovk?.rows.length > 0
}
/>
</Card>
<Card
title='Pakan'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pakan?.rows ?? [])
: []
}
columns={pakanColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={
isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.pakan?.rows.length > 0
}
/>
</Card>
</div>
);
};
export default ClosingSapronakCalculationTable;
@@ -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 (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<>
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
<ClosingIncomingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
<ClosingOutgoingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
</>
)}
</div>
);
};
export default ClosingSapronakTabContent;
+362 -142
View File
@@ -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<Closing, unknown>;
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 (
<RowOptionsMenuWrapper type={type}>
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<RequirePermission permissions='lti.closing.detail'>
<Button
href={`/closing/detail/?closingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
</div>
</RowOptionsMenuWrapper>
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.closing.detail'>
<Button
variant='ghost'
color='none'
onClick={detailClickHandlerWrapper}
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:eye' width={20} height={20} />
View Details
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
);
};
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<ClosingFilterType>({
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<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
// ===== TABLE COLUMNS =====
const closingsColumns: ColumnDef<Closing>[] = [
{
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 (
<StatusBadge
color={badgeColor}
text={status}
className={{
badge: 'whitespace-nowrap',
}}
/>
);
},
},
{
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 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
<RowOptionsMenu
props={props}
detailClickHandler={detailClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
];
// ===== LOCATION OPTIONS =====
const {
setInputValue: setLocationInputValue,
options: locationOptions,
@@ -170,115 +259,246 @@ const ClosingsTable = () => {
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
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<OptionType | null>(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<HTMLInputElement> = (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 (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-end items-end sm:items-center gap-4'>
<div className='w-full'>
<div className='flex flex-col mb-4'>
<div className='relative w-full p-3 pt-0 px-0 flex flex-row justify-between gap-3 flex-wrap after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10'>
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Cari Closing'
value={tableFilterState.search}
placeholder='Search'
value={tableFilterState.search ?? ''}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<div className='grid grid-cols-12 justify-end gap-2'>
<SelectInput
label='Lokasi'
options={locationOptions}
isLoading={isLoadingLocationOptions}
value={selectedLocation}
onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
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',
}}
/>
<SelectInput
label='Status Project'
placeholder='Pilih Status'
options={PROJECT_STATUS_OPTIONS}
value={selectedProjectStatus}
onChange={projectStatusChangeHandler}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-6',
}}
/>
<Button
variant='outline'
color='none'
onClick={handleFilterModalOpen}
className={cn(
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
{
'border-primary-gradient text-primary': hasFilters,
}
)}
>
<Icon icon='heroicons:funnel' width={20} height={20} />
Filter
{hasFilters && (
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
{activeFiltersCount}
</span>
)}
</Button>
</div>
</div>
</div>
<Table<Closing>
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 ? (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
) : data.length === 0 ? (
<ClosingTableSkeleton
columns={closingsColumns}
icon={
<Icon
icon='heroicons:chart-bar'
className='text-white'
width={20}
height={20}
/>
}
title='Data Closing Belum Tersedia'
subtitle='Tidak ada data closing untuk saat ini.'
/>
) : (
<Table<Closing>
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',
}}
/>
)}
</div>
</div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<div className='p-4 flex flex-col gap-1.5'>
<SelectInput
label='Lokasi'
placeholder='Pilih Lokasi'
options={locationOptions}
value={locationIdValue}
onChange={(val) => {
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' }}
/>
<SelectInputRadio
label='Status Project'
placeholder='Pilih Status'
options={projectStatusOptions}
value={projectStatusValue}
onChange={(val) => {
if (!Array.isArray(val)) {
formik.setFieldValue('project_status', val?.value || null);
}
}}
className={{ wrapper: 'w-full' }}
isClearable={true}
/>
</div>
{/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
type='reset'
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
>
Reset Filter
</Button>
<Button
type='submit'
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
disabled={!formik.isValid || formik.isSubmitting}
>
Apply Filter
</Button>
</div>
</form>
</Modal>
</>
);
};
@@ -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<typeof ClosingFilterSchema>;
@@ -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<BaseExpeditionCost>[] =
useMemo(
() => [
{
id: 'id',
accessorKey: 'id',
header: 'No',
cell: (props) => {
return <div>{props.row.index + 1}</div>;
},
footer: () => (
<div className='font-semibold text-gray-900'>
Total HPP Ekspedisi
</div>
),
},
{
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 <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalHpp)}
</div>
),
},
],
[totals]
);
return (
<>
<section className='w-full'>
<div className='p-4'>
<h2 className='text-xl font-semibold mb-4'>HPP Ekspedisi</h2>
<Card
className={{
wrapper: 'w-full bg-base-100',
body: 'p-0',
}}
>
<Table
data={costOfRevenueExpeditionData}
columns={costOfRevenueExpeditionColumns}
renderFooter={costOfRevenueExpeditionData.length > 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',
}}
/>
</Card>
</div>
</section>
</>
);
};
export default HppExpeditionReportTable;
@@ -0,0 +1,36 @@
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table';
const ClosingTabSkeleton = <T extends object>({
columns,
icon,
title,
subtitle,
}: {
columns: ColumnDef<T, unknown>[];
icon: React.ReactNode;
title: string;
subtitle: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default ClosingTabSkeleton;
@@ -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<Closing>[];
icon: React.ReactNode;
title: string;
subtitle: string;
}) => {
return (
<div className='relative size-full'>
<Table
data={[]}
columns={columns}
isLoading={true}
className={{
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
headerColumnClassName: 'whitespace-nowrap',
containerClassName: 'mb-0 overflow-hidden',
tableWrapperClassName: 'overflow-hidden',
}}
/>
<div className='absolute inset-0 flex items-center justify-center'>
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
</div>
</div>
);
};
export default ClosingTableSkeleton;
@@ -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 (
<Card
variant='bordered'
className={{
wrapper: 'w-full',
body: 'p-8',
}}
>
<div className='flex items-center justify-center p-8'>
<DataStateSkeleton
icon={
<Icon
icon={iconName}
className='text-white'
width={20}
height={20}
/>
}
title={title}
description={subtitle}
/>
</div>
</Card>
);
};
export default FinanceClosingSkeleton;
@@ -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<BaseExpeditionCost>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<BaseExpeditionCost>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default HppExpeditionClosingSkeleton;
@@ -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<Overhead>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<Overhead>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default OverheadClosingSkeleton;
@@ -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 (
<div className='w-full rounded-xl p-8 shadow-sm'>
<div className='flex items-center justify-center p-12'>
<DataStateSkeleton
icon={
<Icon
icon={iconName}
className='text-white'
width={20}
height={20}
/>
}
title={title}
description={subtitle}
/>
</div>
</div>
);
};
export default ProductionDataClosingSkeleton;
@@ -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<BaseSales>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<BaseSales>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default SalesClosingSkeleton;
@@ -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<RowSapronakCalculation>[];
title?: string;
subtitle?: string;
iconName?: string;
}) => {
return (
<ClosingTabSkeleton<RowSapronakCalculation>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title}
subtitle={subtitle}
/>
);
};
export default SapronakCalculationClosingSkeleton;
@@ -0,0 +1,40 @@
import { Icon } from '@iconify/react';
import ClosingTabSkeleton from './ClosingTabSkeleton';
import { ColumnDef } from '@tanstack/react-table';
const SapronakClosingSkeleton = <T extends object>({
columns,
type = 'incoming',
title,
subtitle,
iconName = 'heroicons:chart-bar',
}: {
columns: ColumnDef<T, unknown>[];
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 (
<ClosingTabSkeleton<T>
columns={columns}
icon={
<Icon icon={iconName} className='text-white' width={20} height={20} />
}
title={title || defaultTitle}
subtitle={subtitle || defaultSubtitle}
/>
);
};
export default SapronakClosingSkeleton;
@@ -0,0 +1,13 @@
import FinanceClosingTable from '@/components/pages/closing/table/FinanceClosingTable';
const FinanceClosingTab = ({ projectFlockId }: { projectFlockId: number }) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<FinanceClosingTable projectFlockId={projectFlockId} />
)}
</div>
);
};
export default FinanceClosingTab;
@@ -0,0 +1,19 @@
import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable';
interface HppExpeditionClosingTabProps {
projectFlockId: number;
}
const HppExpeditionClosingTab = ({
projectFlockId,
}: HppExpeditionClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<HppExpeditionClosingTable projectFlockId={projectFlockId} />
)}
</div>
);
};
export default HppExpeditionClosingTab;
@@ -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 (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<ClosingOverheadTable
<OverheadClosingTable
projectFlockId={projectFlockId}
generalInformation={generalInformation}
kandangData={kandangData}
@@ -26,4 +26,4 @@ const ClosingOverheadTabContent = ({
);
};
export default ClosingOverheadTabContent;
export default OverheadClosingTab;
@@ -0,0 +1,321 @@
'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';
import ProductionDataClosingSkeleton from '@/components/pages/closing/skeleton/ProductionDataClosingSkeleton';
import Card from '@/components/Card';
interface ProductionDataClosingTabProps {
projectFlockId: number;
}
const ProductionDataClosingTab = ({
projectFlockId,
}: ProductionDataClosingTabProps) => {
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 <ProductionDataClosingSkeleton />;
}
if (!productionData || !isResponseSuccess(productionData)) {
return (
<ProductionDataClosingSkeleton
iconName='heroicons:exclamation-circle'
title='Gagal Memuat Data Produksi'
subtitle='Terjadi kesalahan saat memuat data produksi. Silakan coba lagi.'
/>
);
}
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;
}) => (
<div className='flex justify-between items-center py-1'>
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
<div className='flex gap-2 w-1/2 justify-end items-center'>
<span className={valueClassName}>{value}</span>
{unit && <span className={unitClassName}>{unit}</span>}
</div>
</div>
);
return (
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Data Produksi'
collapsible
defaultCollapsed={false}
>
<div className='p-6'>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
{/* Left Column */}
<div className='space-y-10'>
{/* Purchase Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Pembelian
</h3>
<div className='space-y-1'>
<DataRow
label='Populasi Awal'
value={formatNumber(purchase.initial_population)}
unit='Ekor'
/>
<DataRow
label='Claim Culling'
value={formatNumber(purchase.claim_culling)}
unit='Ekor'
/>
<DataRow
label='Populasi Akhir'
value={formatNumber(purchase.final_population)}
unit='Ekor'
/>
<DataRow
label='Pakan Masuk'
value={formatNumber(purchase.feed_in)}
unit='Kg'
/>
<DataRow
label='Pakan Terpakai'
value={formatNumber(purchase.feed_used)}
unit='Kg'
/>
</div>
</section>
{/* Sales Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Penjualan
</h3>
<div className='space-y-4'>
{/* Chicken Sales */}
<div className='space-y-1'>
<DataRow
label='Penjualan (Ekor)'
value={formatNumber(sales.chicken.sales_population)}
unit='Ekor'
/>
<DataRow
label='Penjualan (Kg)'
value={formatNumber(sales.chicken.sales_weight)}
unit='Kg'
/>
<DataRow
label='Bobot Rata-Rata'
value={formatNumber(sales.chicken.avg_weight)}
unit='Kg/Ekor'
/>
<DataRow
label='Harga Jual Rata-Rata'
value={formatNumber(sales.chicken.avg_selling_price)}
unit='Rupiah'
/>
</div>
{/* Egg Sales (if available) */}
{sales.egg && (
<>
<div className='h-px bg-gray-100 my-2' />
<div className='space-y-1'>
<DataRow
label='Telur (Butir)'
value={formatNumber(sales.egg.egg_pieces)}
unit='Butir'
/>
<DataRow
label='Telur (Kg)'
value={formatNumber(sales.egg.egg_mass)}
unit='Kg'
/>
<DataRow
label='Berat Telur Rata-Rata'
value={formatNumber(sales.egg.avg_egg_weight)}
unit='Kg'
/>
<DataRow
label='Harga Jual Telur Rata-Rata'
value={formatNumber(sales.egg.avg_selling_price)}
unit='Rupiah'
/>
</div>
</>
)}
</div>
</section>
</div>
{/* Divider Line (Absolute centered) */}
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
{/* Right Column */}
<div className='space-y-10 flex flex-col h-full'>
{/* Performance Section */}
<section>
<h3 className='font-bold text-gray-700 mb-4 text-base'>
Performance
</h3>
<div className='space-y-1'>
<DataRow
label='Deplesi'
value={formatNumber(performance.depletion)}
unit='Ekor'
/>
<DataRow
label='Umur'
value={formatNumber(performance.age_day)}
unit='Hari'
/>
<DataRow
label='Mortalitas Std'
value={formatNumber(performance.mor_std)}
unitClassName='hidden'
/>
<DataRow
label='Mortalitas Act'
value={formatNumber(performance.mor_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF Mortalitas'
value={formatNumber(performance.mor_diff)}
unitClassName='hidden'
/>
{/* <DataRow
label='AWG Std'
value={formatNumber(performance.awg_std)}
unit='Gr/Hari'
/>
<DataRow
label='AWG Act'
value={formatNumber(performance.awg_act)}
unit='Gr/Hari'
/> */}
<DataRow
label='Feed Intake Std'
value={formatNumber(performance.feed_intake_std)}
unitClassName='hidden'
/>
<DataRow
label='Feed Intake Act'
value={formatNumber(performance.feed_intake)}
unitClassName='hidden'
/>
<DataRow
label='FCR Std'
value={formatNumber(performance.fcr_std)}
unitClassName='hidden'
/>
<DataRow
label='FCR Act'
value={formatNumber(performance.fcr_act)}
unitClassName='hidden'
/>
<DataRow
label='DEFF FCR'
value={formatNumber(performance.fcr_diff)}
unitClassName='hidden'
/>
{/* Laying Specific Fields */}
{performance.hen_day_act !== undefined && (
<>
<DataRow
label='Hen Day Std'
value={formatNumber(performance.hen_day_std!)}
unit='%'
/>
<DataRow
label='Hen Day Act'
value={formatNumber(performance.hen_day_act)}
unit='%'
/>
</>
)}
{performance.egg_mass !== undefined && (
<>
<DataRow
label='Egg Mass Std'
value={formatNumber(performance.egg_mass_std!)}
unit='Kg'
/>
<DataRow
label='Egg Mass Act'
value={formatNumber(performance.egg_mass)}
unit='Kg'
/>
</>
)}
{performance.egg_weight !== undefined && (
<>
<DataRow
label='Egg Weight Std'
value={formatNumber(performance.egg_weight_std!)}
unit='Gr'
/>
<DataRow
label='Egg Weight Act'
value={formatNumber(performance.egg_weight)}
unit='Gr'
/>
</>
)}
{performance.hen_housed_act !== undefined && (
<>
<DataRow
label='Hen Housed Std'
value={formatNumber(performance.hen_housed_std!)}
unit='%'
/>
<DataRow
label='Hen Housed Act'
value={formatNumber(performance.hen_housed_act)}
unit='%'
/>
</>
)}
</div>
</section>
</div>
</div>
</div>
</Card>
</div>
);
};
export default ProductionDataClosingTab;
@@ -0,0 +1,15 @@
import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable';
interface SalesClosingTabProps {
projectFlockId: number;
}
const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => {
return (
<div className='flex flex-col gap-4'>
{projectFlockId && <SalesClosingTable projectFlockId={projectFlockId} />}
</div>
);
};
export default SalesClosingTab;
@@ -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 (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<>
<ClosingSapronakCalculationTable
<SapronakCalculationClosingTable
closingGeneralInformation={closingGeneralInformation}
projectFlockId={projectFlockId}
/>
@@ -26,4 +26,4 @@ const ClosingSapronakCalculationTabContent = ({
);
};
export default ClosingSapronakCalculationTabContent;
export default SapronakCalculationClosingTab;
@@ -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 (
<div className='flex flex-col gap-4'>
{projectFlockId && (
<>
<IncomingSapronaksTable projectFlockId={projectFlockId} />
<IncomingSapronaksSummaryTable projectFlockId={projectFlockId} />
<OutgoingSapronaksTable projectFlockId={projectFlockId} />
<OutgoingSapronaksSummaryTable projectFlockId={projectFlockId} />
</>
)}
</div>
);
};
export default SapronakClosingTab;
@@ -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 (
<div className='flex flex-col gap-4 pt-3'>
{isLoading ? (
<FinanceClosingSkeleton />
) : !isResponseSuccess(finance) ? (
<FinanceClosingSkeleton
iconName='heroicons:chart-bar'
title='Data Keuangan Tidak Ditemukan'
subtitle='Tidak ada data keuangan untuk periode ini.'
/>
) : (
<>
<section className='grid grid-cols-1 md:grid-cols-2 gap-3'>
<Card
className={{
wrapper: 'w-full rounded-xl border border-base-content/10',
body: 'p-0',
wrapperContent:
'h-full flex flex-col items-between justify-between',
}}
variant='bordered'
>
<div className='flex flex-row items-center gap-4 px-4 py-4'>
<Alert
variant='soft'
color='success'
className='rounded-lg p-3 bg-success/12 flex items-center justify-center'
>
<Icon
icon='heroicons:chart-bar-square'
width={24}
height={24}
/>
</Alert>
<div className='space-y-1'>
<h3 className='text-base-content/50 font-semibold text-sm'>
Laba Rugi Brutto
</h3>
<p className='text-xl font-semibold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.gross_profit.amount
)
: '-'}
</p>
</div>
</div>
</Card>
<Card
className={{
wrapper: 'w-full rounded-xl border border-base-content/10',
body: 'p-0',
wrapperContent:
'h-full flex flex-col items-between justify-between',
}}
variant='bordered'
>
<div className='flex flex-row items-center gap-4 px-4 py-4'>
<Alert
variant='soft'
color='info'
className='rounded-lg p-3 bg-info/12 flex items-center justify-center'
>
<Icon
icon='heroicons:currency-dollar'
width={24}
height={24}
/>
</Alert>
<div className='space-y-1'>
<h3 className='text-base-content/50 font-semibold text-sm'>
Laba Rugi Netto
</h3>
<p className='text-xl font-semibold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit.amount
)
: '-'}
</p>
</div>
</div>
</Card>
</section>
<Card
title='HPP Purchases'
variant='bordered'
collapsible
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
<div className='p-0'>
<Table<HppItem>
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 (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
></td>
<td
colSpan={7}
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
<Card
title='Profit/Loss'
variant='bordered'
collapsible
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
<div className='p-0'>
<Table<ProfitLossItem>
data={profitLossTableData}
isLoading={isLoading}
columns={[
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.label,
cell: (item) => (
<div className=''>
{formatTitleCase(item.row.original.label || '-')}
</div>
),
footer: () => (
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
),
},
{
header: 'Rp/Ekor',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Rp/Kg',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Jumlah (Rp)',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.amount || 0
)
: formatCurrency(0)}
</div>
),
},
]}
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 (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.footerRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_bird ?? 0)}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_kg ?? 0)}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.amount ?? 0)}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
</>
)}
</div>
);
};
export default FinanceClosingTable;
@@ -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<BaseExpeditionCost>[] =
useMemo(
() => [
{
id: 'id',
accessorKey: 'id',
header: 'No',
cell: (props) => {
return <div>{props.row.index + 1}</div>;
},
footer: () => (
<div className='font-semibold text-gray-900'>
Total HPP Ekspedisi
</div>
),
},
{
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 <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
<div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.totalHpp)}
</div>
),
},
],
[totals]
);
return (
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='HPP Ekspedisi'
collapsible
defaultCollapsed={false}
>
{isLoading ? (
<HppExpeditionClosingSkeleton
columns={costOfRevenueExpeditionColumns}
/>
) : costOfRevenueExpeditionData.length === 0 ? (
<HppExpeditionClosingSkeleton
columns={costOfRevenueExpeditionColumns}
iconName='heroicons:chart-bar'
/>
) : (
<Table
data={costOfRevenueExpeditionData}
columns={costOfRevenueExpeditionColumns}
isLoading={isLoading}
renderFooter={costOfRevenueExpeditionData.length > 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',
}}
/>
)}
</Card>
</div>
);
};
export default HppExpeditionClosingTable;
@@ -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 (
<>
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Pengeluaran Overhead'
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Table<Overhead>
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 ? (
<OverheadClosingSkeleton columns={columns} />
) : !isResponseSuccess(overhead) ? (
<OverheadClosingSkeleton
columns={columns}
iconName='heroicons:chart-bar'
title='Data Overhead Tidak Ditemukan'
subtitle='Tidak ada data overhead untuk periode ini.'
/>
) : kandangId && !isResponseSuccess(overheadKandang) ? (
<OverheadClosingSkeleton
columns={columns}
iconName='heroicons:chart-bar'
title='Data Overhead Tidak Ditemukan'
subtitle='Tidak ada data overhead untuk periode ini.'
/>
) : (!kandangId && overhead.data?.overheads.length === 0) ||
(kandangId &&
isResponseSuccess(overheadKandang) &&
overheadKandang.data?.overheads.length === 0) ? (
<OverheadClosingSkeleton
columns={columns}
iconName='heroicons:chart-bar'
/>
) : (
<Table<Overhead>
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) && (
<Card
className={{
wrapper: 'w-full',
@@ -298,8 +341,8 @@ const ClosingOverheadTable = ({
</Card>
)}
</Card>
</>
</div>
);
};
export default ClosingOverheadTable;
export default OverheadClosingTable;
@@ -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 (
<>
<section className='w-full'>
<div className='p-4'>
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
<Card
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Penjualan'
collapsible
defaultCollapsed={false}
>
{isLoading ? (
<SalesClosingSkeleton columns={salesColumns} />
) : salesData.length === 0 ? (
<SalesClosingSkeleton
columns={salesColumns}
iconName='heroicons:chart-bar'
/>
) : (
<Table
data={salesData}
columns={salesColumns}
isLoading={isLoading}
renderFooter={salesData.length > 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',
}}
>
<Table
data={salesData}
columns={salesColumns}
renderFooter={salesData.length > 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',
}}
/>
</Card>
</div>
</section>
</>
/>
)}
</Card>
</div>
);
};
export default SalesReportTable;
export default SalesClosingTable;
@@ -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<RowSapronakCalculation>[] => [
{
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
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_out',
cell: (props) =>
props.row.original.qty_out
? formatNumber(props.row.original.qty_out as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_used',
cell: (props) =>
props.row.original.qty_used
? formatNumber(props.row.original.qty_used as number)
: '0',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
</div>
)
: '',
},
{
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
? () => (
<div className='font-semibold text-gray-900'>
{total?.avg_unit_price
? formatCurrency(total?.avg_unit_price)
: '-'}
</div>
)
: '',
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_amount',
cell: (props) =>
props.row.original.total_amount
? formatCurrency(props.row.original.total_amount as number)
: '-',
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
</div>
)
: '',
},
{
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 (
<div className='flex flex-col gap-4 pt-3'>
{/* Table DOC jika kategori Project Flock Growing */}
<Card
title={
closingGeneralInformation?.project_type == 'GROWING'
? 'DOC'
: 'Pullet'
}
collapsible
defaultCollapsed={false}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
>
{isLoading ? (
<SapronakCalculationClosingSkeleton columns={docColumns} />
) : isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.doc?.rows?.length === 0 ? (
<SapronakCalculationClosingSkeleton
columns={docColumns}
iconName='heroicons:chart-bar'
title='Data Perhitungan Sapronak Tidak Ditemukan'
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
/>
) : (
<Table<RowSapronakCalculation>
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
}
/>
)}
</Card>
<Card
title='OVK'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
{isLoading ? (
<SapronakCalculationClosingSkeleton columns={ovkColumns} />
) : isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.ovk?.rows?.length === 0 ? (
<SapronakCalculationClosingSkeleton
columns={ovkColumns}
iconName='heroicons:chart-bar'
title='Data Perhitungan Sapronak Tidak Ditemukan'
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
/>
) : (
<Table<RowSapronakCalculation>
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
}
/>
)}
</Card>
<Card
title='Pakan'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
>
{isLoading ? (
<SapronakCalculationClosingSkeleton columns={pakanColumns} />
) : isResponseSuccess(sapronakCalculation) &&
sapronakCalculation.data?.pakan?.rows?.length === 0 ? (
<SapronakCalculationClosingSkeleton
columns={pakanColumns}
iconName='heroicons:chart-bar'
title='Data Perhitungan Sapronak Tidak Ditemukan'
subtitle='Tidak ada data perhitungan sapronak untuk periode ini.'
/>
) : (
<Table<RowSapronakCalculation>
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
}
/>
)}
</Card>
</div>
);
};
export default SapronakCalculationClosingTable;
@@ -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<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
[
{
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 (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
accessorKey: 'total_qty',
@@ -78,10 +118,6 @@ const ClosingIncomingSapronaksSummaryTable = ({
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (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 (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Ringkasan Sapronak Masuk</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
<div className='w-full'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Ringkasan Sapronak Masuk'
collapsible
defaultCollapsed={false}
>
<div className='w-full p-0'>
{isLoadingIncomingSapronakSummaries ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
/>
) : isResponseSuccess(incomingSapronakSummaries) &&
incomingSapronakSummaries.data.length === 0 ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Ringkasan Sapronak Masuk Tidak Ditemukan'
subtitle='Tidak ada ringkasan sapronak masuk untuk periode ini.'
/>
) : (
<Table<ClosingIncomingSapronakSummary>
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',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -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<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
{
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 (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
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 (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Sapronak Masuk</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
<div className='w-full pt-3'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Sapronak Masuk'
collapsible
defaultCollapsed={false}
>
<div className='flex flex-col gap-2 my-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Masuk'
value={tableFilterState.search}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
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',
}}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Masuk'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
</div>
</div>
{isLoadingIncomingSapronaks ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
/>
) : isResponseSuccess(incomingSapronaks) &&
incomingSapronaks.data.length === 0 ? (
<SapronakClosingSkeleton
type='incoming'
columns={incomingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Data Sapronak Masuk Tidak Ditemukan'
subtitle='Tidak ada data sapronak masuk untuk periode ini.'
/>
) : (
<Table<ClosingIncomingSapronak>
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',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -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<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
[
{
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 (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
accessorKey: 'total_qty',
@@ -78,10 +118,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (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 (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Ringkasan Sapronak Keluar</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
<div className='w-full'>
<Card
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Ringkasan Sapronak Keluar'
collapsible
defaultCollapsed={false}
>
<div className='w-full p-0'>
{isLoadingOutgoingSapronakSummaries ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
/>
) : isResponseSuccess(outgoingSapronakSummaries) &&
outgoingSapronakSummaries.data.length === 0 ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Ringkasan Sapronak Keluar Tidak Ditemukan'
subtitle='Tidak ada ringkasan sapronak keluar untuk periode ini.'
/>
) : (
<Table<ClosingOutgoingSapronakSummary>
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',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -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<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
{
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 (
<div className='flex flex-wrap gap-1 whitespace-nowrap'>
{visibleCategories.map((category, index) => (
<Badge
key={index}
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/5 whitespace-nowrap'
),
}}
title={category}
>
{category.length > 12
? `${category.slice(0, 12)}...`
: category}
</Badge>
))}
{remainingCount > 0 && (
<Badge
variant='soft'
className={{
badge: cn(
'px-2 py-1 flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content bg-base-content/20'
),
}}
title={categories.join(' ')}
>
+{remainingCount}
</Badge>
)}
</div>
);
},
},
{
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 (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Sapronak Keluar</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
<div className='w-full'>
<Card
className={{
wrapper: 'w-full rounded-lg',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
title='Sapronak Keluar'
collapsible
defaultCollapsed={false}
>
<div className='flex flex-col gap-2 my-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Keluar'
value={tableFilterState.search}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
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',
}}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Sapronak Keluar'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
</div>
</div>
{isLoadingOutgoingSapronaks ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
/>
) : isResponseSuccess(outgoingSapronaks) &&
outgoingSapronaks.data.length === 0 ? (
<SapronakClosingSkeleton
type='outgoing'
columns={outgoingSapronaksColumns}
iconName='heroicons:chart-bar'
title='Data Sapronak Keluar Tidak Ditemukan'
subtitle='Tidak ada data sapronak keluar untuk periode ini.'
/>
) : (
<Table<ClosingOutgoingSapronak>
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',
}}
/>
</div>
</Collapse>
</Card>
)}
</Card>
</div>
);
};
@@ -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 = ({
</div>
)}
</div>
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data Chickin yang Pending?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
onClick: (notes) => {
confirmationModalApproveClickHandler(notes);
},
isLoading: isApproveLoading,
}}
/>
</>
);
};
@@ -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 */}
<ConfirmationModalWithNotes
ref={chickinApproveModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data Chickin yang Pending?`}
className={{
modal: 'z-9999',
}}
secondaryButton={{
text: 'Tidak',
onClick: () => {
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,
}}
/>
</>
);
};
@@ -38,38 +38,26 @@ import { Color } from '@/types/theme';
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
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<string, Color> = {
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 (
<StatusBadge
@@ -42,6 +42,7 @@ export const generateDailyMarketingExcel = async (
{ header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 },
{ header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 },
{ header: 'HPP (Rp)', key: 'hppPrice', width: 15 },
{ header: 'HPP Amount (Rp)', key: 'hppAmount', width: 20 },
{ header: 'Total (Rp)', key: 'salesAmount', width: 20 },
];
@@ -67,6 +68,7 @@ export const generateDailyMarketingExcel = async (
totalWeight: formatNumber(item.total_weight_kg || 0),
salesPrice: formatCurrency(item.sales_price_per_kg || 0),
hppPrice: formatCurrency(item.hpp_price_per_kg || 0),
hppAmount: formatCurrency(item.hpp_amount || 0),
salesAmount: formatCurrency(item.sales_amount || 0),
});
});
@@ -75,21 +77,22 @@ export const generateDailyMarketingExcel = async (
if (params.summaryTotal) {
worksheet.addRow({
no: 'TOTAL',
soDate: 'ALL',
realizationDate: '-',
aging: '-',
warehouse: '-',
customer: '-',
doNumber: '-',
sales: '-',
vehicleNumber: '-',
marketingType: '-',
product: '-',
soDate: '',
realizationDate: '',
aging: '',
warehouse: '',
customer: '',
doNumber: '',
sales: '',
vehicleNumber: '',
marketingType: '',
product: '',
qty: formatNumber(params.summaryTotal.total_qty || 0),
averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0),
totalWeight: formatNumber(params.summaryTotal.total_weight_kg || 0),
salesPrice: formatNumber(params.summaryTotal.average_sales_price || 0),
salesPrice: formatCurrency(params.summaryTotal.average_sales_price || 0),
hppPrice: formatCurrency(params.summaryTotal.total_hpp_price_per_kg || 0),
hppAmount: formatCurrency(params.summaryTotal.total_hpp_amount || 0),
salesAmount: formatCurrency(params.summaryTotal.total_sales_amount || 0),
});
}
+21
View File
@@ -0,0 +1,21 @@
'use client';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import {
createClosingTabSlice,
ClosingTabSlice,
} from '@/stores/closing/slices/closing-tab.slice';
export type ClosingTabStore = ClosingTabSlice;
export const useClosingTabStore = create<ClosingTabStore>()(
devtools(
(...args) => ({
...createClosingTabSlice(...args),
}),
{
name: 'ClosingTabStore',
}
)
);
@@ -0,0 +1,37 @@
import { ReactNode } from 'react';
import { StateCreator } from 'zustand';
export type ClosingTabSlice = {
// State - actions per tab ID
tabActions: Record<string, ReactNode>;
// 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: {} }),
});
@@ -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<ChickinStore>()(
devtools(
(...args) => ({
...createChickinApprovalSlice(...args),
}),
{
name: 'ChickinStore',
}
)
);
@@ -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<void>) | null;
// Actions
openChickinApproveModal: (
data: ProjectFlockKandang,
callback: (notes?: string) => Promise<void>
) => 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,
}),
});