mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
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:
@@ -3,7 +3,7 @@
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
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 { ClosingApi } from '@/services/api/closing';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -34,33 +34,6 @@ const ClosingDetailPage = () => {
|
|||||||
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
() => 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) {
|
if (!closingId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
@@ -76,12 +49,7 @@ const ClosingDetailPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading =
|
const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang;
|
||||||
isLoadingClosing ||
|
|
||||||
isLoadingSales ||
|
|
||||||
isLoadingHppEkspedisi ||
|
|
||||||
isLoadingProject ||
|
|
||||||
isLoadingKandang;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
@@ -91,12 +59,6 @@ const ClosingDetailPage = () => {
|
|||||||
<ClosingDetail
|
<ClosingDetail
|
||||||
id={Number(closingId)}
|
id={Number(closingId)}
|
||||||
initialValue={closing.data}
|
initialValue={closing.data}
|
||||||
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
|
||||||
hppExpeditionData={
|
|
||||||
isResponseSuccess(hppEkspedisiData)
|
|
||||||
? hppEkspedisiData.data
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
projectData={
|
projectData={
|
||||||
isResponseSuccess(projectData) ? projectData.data : undefined
|
isResponseSuccess(projectData) ? projectData.data : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable';
|
|||||||
|
|
||||||
const Closing = () => {
|
const Closing = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='w-full p-3'>
|
||||||
<ClosingsTable />
|
<ClosingsTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
+29
-31
@@ -5,28 +5,23 @@ import { useMemo, useState } from 'react';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable';
|
||||||
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab';
|
||||||
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab';
|
||||||
|
|
||||||
import {
|
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
ClosingGeneralInformation,
|
import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab';
|
||||||
BaseClosingSales,
|
import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab';
|
||||||
ClosingHppExpedition,
|
import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab';
|
||||||
} from '@/types/api/closing';
|
import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab';
|
||||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab';
|
||||||
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 ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { useClosingTabStore } from '@/stores/closing/closing-tab.store';
|
||||||
interface ClosingDetailProps {
|
interface ClosingDetailProps {
|
||||||
id: number;
|
id: number;
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
salesData?: BaseClosingSales;
|
|
||||||
hppExpeditionData?: ClosingHppExpedition;
|
|
||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
@@ -34,25 +29,24 @@ interface ClosingDetailProps {
|
|||||||
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
salesData,
|
|
||||||
hppExpeditionData,
|
|
||||||
projectData,
|
projectData,
|
||||||
kandangData,
|
kandangData,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
const [activeTabId, setActiveTabId] = useState<string>('sapronak');
|
||||||
|
const tabActions = useClosingTabStore((state) => state.tabActions);
|
||||||
|
|
||||||
const closingDetailTabs = useMemo(() => {
|
const closingDetailTabs = useMemo(() => {
|
||||||
const validTabs = [
|
const validTabs = [
|
||||||
{
|
{
|
||||||
id: 'sapronak',
|
id: 'sapronak',
|
||||||
label: 'Sapronak',
|
label: 'Sapronak',
|
||||||
content: <ClosingSapronakTabContent projectFlockId={id} />,
|
content: <SapronakClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'perhitunganSapronak',
|
id: 'perhitunganSapronak',
|
||||||
label: 'Perhitungan Sapronak',
|
label: 'Perhitungan Sapronak',
|
||||||
content: (
|
content: (
|
||||||
<ClosingSapronakCalculationTabContent
|
<SapronakCalculationClosingTab
|
||||||
closingGeneralInformation={initialValue}
|
closingGeneralInformation={initialValue}
|
||||||
projectFlockId={id}
|
projectFlockId={id}
|
||||||
/>
|
/>
|
||||||
@@ -61,13 +55,13 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
{
|
{
|
||||||
id: 'penjualan',
|
id: 'penjualan',
|
||||||
label: 'Penjualan',
|
label: 'Penjualan',
|
||||||
content: <SalesReportTable initialValues={salesData} />,
|
content: <SalesClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'overhead',
|
id: 'overhead',
|
||||||
label: 'Overhead',
|
label: 'Overhead',
|
||||||
content: (
|
content: (
|
||||||
<ClosingOverheadTabContent
|
<OverheadClosingTab
|
||||||
projectFlockId={id}
|
projectFlockId={id}
|
||||||
generalInformation={initialValue}
|
generalInformation={initialValue}
|
||||||
kandangData={kandangData}
|
kandangData={kandangData}
|
||||||
@@ -77,26 +71,26 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
{
|
{
|
||||||
id: 'hppEkspedisi',
|
id: 'hppEkspedisi',
|
||||||
label: 'HPP Ekspedisi',
|
label: 'HPP Ekspedisi',
|
||||||
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
content: <HppExpeditionClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dataProduksi',
|
id: 'dataProduksi',
|
||||||
label: 'Data Produksi',
|
label: 'Data Produksi',
|
||||||
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
content: <ProductionDataClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'keuangan',
|
id: 'keuangan',
|
||||||
label: 'Keuangan',
|
label: 'Keuangan',
|
||||||
content: <ClosingFinanceTabContent projectFlockId={id} />,
|
content: <FinanceClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return validTabs;
|
return validTabs;
|
||||||
}, [initialValue]);
|
}, [initialValue, kandangData, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href={
|
href={
|
||||||
@@ -126,13 +120,17 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTabId={activeTab}
|
activeTabId={activeTabId}
|
||||||
onTabChange={setActiveTab}
|
onTabChange={setActiveTabId}
|
||||||
tabs={closingDetailTabs}
|
tabs={closingDetailTabs}
|
||||||
variant='lifted'
|
variant='boxed'
|
||||||
className={{
|
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>
|
</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;
|
projectData?: ProjectFlock;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
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='flex flex-col @sm:flex-row gap-4'>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
<h1 className='font-bold my-4'>Kandang</h1>
|
<h1 className='font-bold mb-3'>Kandang</h1>
|
||||||
<div className='flex flex-wrap gap-2 mb-4'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{projectData?.kandangs?.map((kandang) => (
|
{projectData?.kandangs?.map((kandang) => (
|
||||||
<Button
|
<Button
|
||||||
key={kandang.id}
|
key={kandang.id}
|
||||||
variant='outline'
|
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}`}
|
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
||||||
className='min-w-32'
|
|
||||||
>
|
>
|
||||||
{kandang.name}
|
{kandang.name}
|
||||||
</Button>
|
</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;
|
|
||||||
@@ -1,68 +1,116 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState, useMemo } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import SelectInput, {
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
OptionType,
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
useSelect,
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
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 { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { Closing } from '@/types/api/closing';
|
import { Closing } from '@/types/api/closing';
|
||||||
|
import { Color } from '@/types/theme';
|
||||||
const PROJECT_STATUS_OPTIONS = [
|
import {
|
||||||
{
|
ClosingFilterSchema,
|
||||||
value: 1,
|
ClosingFilterType,
|
||||||
label: 'Pengajuan',
|
} from '@/components/pages/closing/filter/ClosingFilter';
|
||||||
},
|
import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton';
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
label: 'Aktif',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
|
||||||
props,
|
props,
|
||||||
|
popoverPosition = 'bottom',
|
||||||
|
detailClickHandler,
|
||||||
}: {
|
}: {
|
||||||
type: 'dropdown' | 'collapse';
|
|
||||||
props: CellContext<Closing, unknown>;
|
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 (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<div className='relative'>
|
||||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
<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'>
|
<RequirePermission permissions='lti.closing.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/closing/detail/?closingId=${props.row.original.id}`}
|
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='primary'
|
color='none'
|
||||||
className='justify-start text-sm'
|
onClick={detailClickHandlerWrapper}
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||||
Detail
|
View Details
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</RowOptionsMenuWrapper>
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClosingsTable = () => {
|
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 {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -72,36 +120,67 @@ const ClosingsTable = () => {
|
|||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
nameSort: '',
|
// nameSort: '',
|
||||||
transactionDate: '',
|
// transactionDate: '',
|
||||||
realizationDate: '',
|
// realizationDate: '',
|
||||||
locationId: '',
|
location_id: '',
|
||||||
projectStatus: '',
|
project_status: '',
|
||||||
userId: '',
|
// userId: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
nameSort: 'sort_name',
|
// nameSort: 'sort_name',
|
||||||
transactionDate: 'transaction_date',
|
// transactionDate: 'transaction_date',
|
||||||
realizationDate: 'realization_date',
|
// realizationDate: 'realization_date',
|
||||||
locationId: 'location_id',
|
// locationId: 'location_id',
|
||||||
projectStatus: 'project_status',
|
// projectStatus: 'project_status',
|
||||||
userId: 'user_id',
|
// 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(
|
const { data: closings, isLoading: isLoadingClosings } = useSWR(
|
||||||
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
|
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
|
||||||
ClosingApi.getAllFetcher
|
ClosingApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(closings) ? (closings?.data as Closing[]) || [] : [],
|
||||||
|
[closings]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== PAGINATION & STATE =====
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// ===== TABLE COLUMNS =====
|
||||||
const closingsColumns: ColumnDef<Closing>[] = [
|
const closingsColumns: ColumnDef<Closing>[] = [
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,6 +212,19 @@ const ClosingsTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'project_status',
|
accessorKey: 'project_status',
|
||||||
header: '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',
|
header: 'Aksi',
|
||||||
@@ -142,27 +234,24 @@ const ClosingsTable = () => {
|
|||||||
const currentRowRelativeIndex =
|
const currentRowRelativeIndex =
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
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 (
|
return (
|
||||||
<>
|
<RowOptionsMenu
|
||||||
{currentPageSize > 3 && (
|
props={props}
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
detailClickHandler={detailClickHandler}
|
||||||
<RowOptionsMenu type='dropdown' props={props} />
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
</RowDropdownOptions>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 3 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu type='collapse' props={props} />
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ===== LOCATION OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
@@ -170,91 +259,140 @@ const ClosingsTable = () => {
|
|||||||
loadMore: loadMoreLocations,
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
// ===== PROJECT STATUS OPTIONS =====
|
||||||
null
|
const projectStatusOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: '1', label: 'Pengajuan' },
|
||||||
|
{ value: '2', label: 'Aktif' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
// ===== FILTER HELPERS =====
|
||||||
setSelectedLocation(val as OptionType);
|
const locationIdValue = useMemo(() => {
|
||||||
updateFilter(
|
if (!formik.values.location_id) return null;
|
||||||
'locationId',
|
return (
|
||||||
val ? ((val as OptionType).value as string) : ''
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
};
|
}, [formik.values.location_id, locationOptions]);
|
||||||
|
|
||||||
const [selectedProjectStatus, setSelectedProjectStatus] =
|
const projectStatusValue = useMemo(() => {
|
||||||
useState<OptionType | null>(null);
|
if (!formik.values.project_status) return null;
|
||||||
|
return (
|
||||||
const projectStatusChangeHandler = (
|
projectStatusOptions.find(
|
||||||
val: OptionType | OptionType[] | null
|
(opt) => opt.value === formik.values.project_status
|
||||||
) => {
|
) || null
|
||||||
setSelectedProjectStatus(val as OptionType);
|
|
||||||
updateFilter(
|
|
||||||
'projectStatus',
|
|
||||||
val ? ((val as OptionType).value as string) : ''
|
|
||||||
);
|
);
|
||||||
};
|
}, [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) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
if (!isNameSorted) {
|
if (!isNameSorted) {
|
||||||
updateFilter('nameSort', '');
|
// updateFilter('nameSort', '');
|
||||||
} else {
|
} else {
|
||||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
// updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
<div className='w-full'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col mb-4'>
|
||||||
<div className='flex flex-col gap-2 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='w-full flex flex-col sm:flex-row justify-end items-end sm:items-center gap-4'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari Closing'
|
placeholder='Search'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search ?? ''}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
startAdornment={
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:magnifying-glass'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
/>
|
/>
|
||||||
</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
|
|
||||||
className={{
|
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
|
<Button
|
||||||
label='Status Project'
|
variant='outline'
|
||||||
placeholder='Pilih Status'
|
color='none'
|
||||||
options={PROJECT_STATUS_OPTIONS}
|
onClick={handleFilterModalOpen}
|
||||||
value={selectedProjectStatus}
|
className={cn(
|
||||||
onChange={projectStatusChangeHandler}
|
'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',
|
||||||
isClearable
|
{
|
||||||
className={{
|
'border-primary-gradient text-primary': hasFilters,
|
||||||
wrapper: 'col-span-12 sm:col-span-6',
|
}
|
||||||
}}
|
)}
|
||||||
/>
|
>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
{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>
|
<Table<Closing>
|
||||||
data={isResponseSuccess(closings) ? closings?.data : []}
|
data={isResponseSuccess(closings) ? closings?.data : []}
|
||||||
columns={closingsColumns}
|
columns={closingsColumns}
|
||||||
@@ -272,13 +410,95 @@ const ClosingsTable = () => {
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn('mt-3', {
|
||||||
'w-full mb-20':
|
'w-full mb-0':
|
||||||
isResponseSuccess(closings) && closings?.data?.length === 0,
|
isResponseSuccess(closings) && closings?.data?.length === 0,
|
||||||
}),
|
}),
|
||||||
|
headerColumnClassName: 'text-nowrap',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</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;
|
||||||
+6
-6
@@ -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 { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
|
||||||
interface ClosingOverheadTabContentProps {
|
interface OverheadClosingTabProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
generalInformation?: ClosingGeneralInformation;
|
generalInformation?: ClosingGeneralInformation;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClosingOverheadTabContent = ({
|
const OverheadClosingTab = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
generalInformation,
|
generalInformation,
|
||||||
kandangData,
|
kandangData,
|
||||||
}: ClosingOverheadTabContentProps) => {
|
}: OverheadClosingTabProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
{projectFlockId && (
|
{projectFlockId && (
|
||||||
<ClosingOverheadTable
|
<OverheadClosingTable
|
||||||
projectFlockId={projectFlockId}
|
projectFlockId={projectFlockId}
|
||||||
generalInformation={generalInformation}
|
generalInformation={generalInformation}
|
||||||
kandangData={kandangData}
|
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;
|
||||||
+6
-6
@@ -1,22 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
|
import SapronakCalculationClosingTable from '@/components/pages/closing/table/SapronakCalculationClosingTable';
|
||||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
|
|
||||||
interface ClosingSapronakCalculationTabContentProps {
|
interface SapronakCalculationClosingTabProps {
|
||||||
projectFlockId?: number;
|
projectFlockId?: number;
|
||||||
closingGeneralInformation?: ClosingGeneralInformation;
|
closingGeneralInformation?: ClosingGeneralInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClosingSapronakCalculationTabContent = ({
|
const SapronakCalculationClosingTab = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
closingGeneralInformation,
|
closingGeneralInformation,
|
||||||
}: ClosingSapronakCalculationTabContentProps) => {
|
}: SapronakCalculationClosingTabProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
{projectFlockId && (
|
{projectFlockId && (
|
||||||
<>
|
<>
|
||||||
<ClosingSapronakCalculationTable
|
<SapronakCalculationClosingTable
|
||||||
closingGeneralInformation={closingGeneralInformation}
|
closingGeneralInformation={closingGeneralInformation}
|
||||||
projectFlockId={projectFlockId}
|
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;
|
||||||
+56
-13
@@ -14,18 +14,19 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton';
|
||||||
|
|
||||||
interface ClosingOverheadTableProps {
|
interface OverheadClosingTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
generalInformation?: ClosingGeneralInformation;
|
generalInformation?: ClosingGeneralInformation;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClosingOverheadTable = ({
|
const OverheadClosingTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
generalInformation,
|
generalInformation,
|
||||||
kandangData,
|
kandangData,
|
||||||
}: ClosingOverheadTableProps) => {
|
}: OverheadClosingTableProps) => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const kandangId = searchParams.get('kandangId');
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
@@ -208,16 +209,44 @@ const ClosingOverheadTable = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full pt-3'>
|
||||||
<Card
|
<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'
|
title='Pengeluaran Overhead'
|
||||||
collapsible
|
collapsible
|
||||||
defaultCollapsed={false}
|
defaultCollapsed={false}
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
body: 'p-4 shadow',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{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>
|
<Table<Overhead>
|
||||||
data={
|
data={
|
||||||
kandangId
|
kandangId
|
||||||
@@ -230,11 +259,24 @@ const ClosingOverheadTable = ({
|
|||||||
}
|
}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'my-4',
|
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(
|
headerColumnClassName: cn(
|
||||||
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||||
'whitespace-nowrap'
|
'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}
|
isLoading={isLoadingOverhead}
|
||||||
renderFooter={
|
renderFooter={
|
||||||
@@ -243,7 +285,8 @@ const ClosingOverheadTable = ({
|
|||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{kandangId && (
|
)}
|
||||||
|
{kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && (
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full',
|
||||||
@@ -298,8 +341,8 @@ const ClosingOverheadTable = ({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ClosingOverheadTable;
|
export default OverheadClosingTable;
|
||||||
+57
-24
@@ -5,28 +5,47 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
||||||
import {
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
BaseClosingSales,
|
import { BaseSales, ClosingSalesSummary } from '@/types/api/closing';
|
||||||
BaseSales,
|
|
||||||
ClosingSalesSummary,
|
|
||||||
} from '@/types/api/closing';
|
|
||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { Customer } from '@/types/api/master-data/customer';
|
import { Customer } from '@/types/api/master-data/customer';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
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 {
|
interface SalesClosingTableProps {
|
||||||
type?: 'detail';
|
projectFlockId: number;
|
||||||
initialValues?: BaseClosingSales;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(() => {
|
const salesData: BaseSales[] = useMemo(() => {
|
||||||
return initialValues?.sales || [];
|
if (isResponseSuccess(sales)) {
|
||||||
}, [initialValues]);
|
return sales.data.sales || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [sales]);
|
||||||
|
|
||||||
const summary: ClosingSalesSummary | undefined = useMemo(() => {
|
const summary: ClosingSalesSummary | undefined = useMemo(() => {
|
||||||
return initialValues?.summary;
|
if (isResponseSuccess(sales)) {
|
||||||
}, [initialValues]);
|
return sales.data.summary;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [sales]);
|
||||||
|
|
||||||
const totals = useMemo(() => {
|
const totals = useMemo(() => {
|
||||||
if (salesData.length === 0) {
|
if (salesData.length === 0) {
|
||||||
@@ -293,27 +312,42 @@ const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full pt-3'>
|
||||||
<section className='w-full'>
|
|
||||||
<div className='p-4'>
|
|
||||||
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
|
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full bg-base-100',
|
wrapper: 'w-full rounded-lg border-none',
|
||||||
body: 'p-0',
|
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
|
<Table
|
||||||
data={salesData}
|
data={salesData}
|
||||||
columns={salesColumns}
|
columns={salesColumns}
|
||||||
|
isLoading={isLoading}
|
||||||
renderFooter={salesData.length > 0}
|
renderFooter={salesData.length > 0}
|
||||||
className={{
|
className={{
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
containerClassName: 'w-full mb-0!',
|
||||||
|
tableWrapperClassName:
|
||||||
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||||
headerColumnClassName:
|
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',
|
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||||
bodyRowClassName:
|
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',
|
'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:
|
bodyColumnClassName:
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
tableFooterClassName:
|
tableFooterClassName:
|
||||||
@@ -323,11 +357,10 @@ const SalesReportTable = ({ initialValues }: SalesReportTableProps) => {
|
|||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
+81
-49
@@ -1,20 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Collapse from '@/components/Collapse';
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
|
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
|
||||||
|
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||||
|
|
||||||
interface ClosingIncomingSapronaksSummaryTableProps {
|
interface ClosingIncomingSapronaksSummaryTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -55,20 +55,60 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
|
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
header: 'Kategori',
|
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',
|
accessorKey: 'total_qty',
|
||||||
@@ -78,10 +118,6 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
@@ -93,44 +129,35 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setOpen(
|
|
||||||
isResponseSuccess(incomingSapronakSummaries)
|
|
||||||
? incomingSapronakSummaries.data.length > 0
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [incomingSapronakSummaries, isResponseSuccess]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full rounded-lg',
|
||||||
body: 'p-4 shadow',
|
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}
|
||||||
>
|
>
|
||||||
<Collapse
|
{isLoadingIncomingSapronakSummaries ? (
|
||||||
open={open}
|
<SapronakClosingSkeleton
|
||||||
onOpenChange={setOpen}
|
type='incoming'
|
||||||
title={
|
columns={incomingSapronaksColumns}
|
||||||
<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>
|
) : isResponseSuccess(incomingSapronakSummaries) &&
|
||||||
}
|
incomingSapronakSummaries.data.length === 0 ? (
|
||||||
className='w-full!'
|
<SapronakClosingSkeleton
|
||||||
titleClassName='w-full p-0!'
|
type='incoming'
|
||||||
>
|
columns={incomingSapronaksColumns}
|
||||||
<div className='w-full p-0'>
|
iconName='heroicons:chart-bar'
|
||||||
|
title='Ringkasan Sapronak Masuk Tidak Ditemukan'
|
||||||
|
subtitle='Tidak ada ringkasan sapronak masuk untuk periode ini.'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Table<ClosingIncomingSapronakSummary>
|
<Table<ClosingIncomingSapronakSummary>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(incomingSapronakSummaries)
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
@@ -158,16 +185,21 @@ const ClosingIncomingSapronaksSummaryTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: 'w-full mb-5!',
|
||||||
'w-full mb-20':
|
tableWrapperClassName:
|
||||||
isResponseSuccess(incomingSapronakSummaries) &&
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||||
incomingSapronakSummaries?.data?.length === 0,
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
+95
-46
@@ -9,13 +9,14 @@ import { Icon } from '@iconify/react';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Collapse from '@/components/Collapse';
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingIncomingSapronak } from '@/types/api/closing';
|
import { ClosingIncomingSapronak } from '@/types/api/closing';
|
||||||
|
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||||
|
|
||||||
interface ClosingIncomingSapronaksTableProps {
|
interface ClosingIncomingSapronaksTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -51,14 +52,12 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
ClosingApi.getAllIncomingSapronakFetcher
|
ClosingApi.getAllIncomingSapronakFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
|
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronak>[] = [
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,6 +80,48 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
{
|
{
|
||||||
accessorKey: 'product_category',
|
accessorKey: 'product_category',
|
||||||
header: 'Kategori Produk',
|
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',
|
accessorKey: 'source_warehouse',
|
||||||
@@ -117,56 +158,59 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setOpen(
|
|
||||||
isResponseSuccess(incomingSapronaks)
|
|
||||||
? incomingSapronaks.data.length > 0
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [incomingSapronaks, isResponseSuccess]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className='w-full pt-3'>
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full rounded-lg',
|
||||||
body: 'p-4 shadow',
|
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}
|
||||||
>
|
>
|
||||||
<Collapse
|
<div className='flex flex-col gap-2 my-4'>
|
||||||
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!'
|
|
||||||
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'>
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari Sapronak Masuk'
|
placeholder='Cari Sapronak Masuk'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
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>
|
</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>
|
<Table<ClosingIncomingSapronak>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(incomingSapronaks)
|
isResponseSuccess(incomingSapronaks)
|
||||||
@@ -194,16 +238,21 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: 'w-full mb-5!',
|
||||||
'w-full mb-20':
|
tableWrapperClassName:
|
||||||
isResponseSuccess(incomingSapronaks) &&
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||||
incomingSapronaks?.data?.length === 0,
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
+81
-49
@@ -1,20 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Collapse from '@/components/Collapse';
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
|
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
|
||||||
|
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||||
|
|
||||||
interface ClosingOutgoingSapronaksSummaryTableProps {
|
interface ClosingOutgoingSapronaksSummaryTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -55,20 +55,60 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
|
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'category',
|
accessorKey: 'category',
|
||||||
header: 'Kategori',
|
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',
|
accessorKey: 'total_qty',
|
||||||
@@ -78,10 +118,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
@@ -93,44 +129,35 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setOpen(
|
|
||||||
isResponseSuccess(outgoingSapronakSummaries)
|
|
||||||
? outgoingSapronakSummaries.data.length > 0
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [outgoingSapronakSummaries, isResponseSuccess]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full rounded-lg border-none',
|
||||||
body: 'p-4 shadow',
|
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}
|
||||||
>
|
>
|
||||||
<Collapse
|
{isLoadingOutgoingSapronakSummaries ? (
|
||||||
open={open}
|
<SapronakClosingSkeleton
|
||||||
onOpenChange={setOpen}
|
type='outgoing'
|
||||||
title={
|
columns={outgoingSapronaksColumns}
|
||||||
<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>
|
) : isResponseSuccess(outgoingSapronakSummaries) &&
|
||||||
}
|
outgoingSapronakSummaries.data.length === 0 ? (
|
||||||
className='w-full!'
|
<SapronakClosingSkeleton
|
||||||
titleClassName='w-full p-0!'
|
type='outgoing'
|
||||||
>
|
columns={outgoingSapronaksColumns}
|
||||||
<div className='w-full p-0'>
|
iconName='heroicons:chart-bar'
|
||||||
|
title='Ringkasan Sapronak Keluar Tidak Ditemukan'
|
||||||
|
subtitle='Tidak ada ringkasan sapronak keluar untuk periode ini.'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Table<ClosingOutgoingSapronakSummary>
|
<Table<ClosingOutgoingSapronakSummary>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(outgoingSapronakSummaries)
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
@@ -158,16 +185,21 @@ const ClosingOutgoingSapronaksSummaryTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: 'w-full mb-5!',
|
||||||
'w-full mb-20':
|
tableWrapperClassName:
|
||||||
isResponseSuccess(outgoingSapronakSummaries) &&
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||||
outgoingSapronakSummaries?.data?.length === 0,
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
+98
-47
@@ -9,13 +9,16 @@ import { Icon } from '@iconify/react';
|
|||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Card from '@/components/Card';
|
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 { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { ClosingOutgoingSapronak } from '@/types/api/closing';
|
import { ClosingOutgoingSapronak } from '@/types/api/closing';
|
||||||
|
import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton';
|
||||||
|
|
||||||
interface ClosingOutgoingSapronaksTableProps {
|
interface ClosingOutgoingSapronaksTableProps {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
@@ -51,14 +54,12 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
ClosingApi.getAllOutgoingSapronakFetcher
|
ClosingApi.getAllOutgoingSapronakFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
|
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -81,6 +82,48 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
{
|
{
|
||||||
accessorKey: 'product_category',
|
accessorKey: 'product_category',
|
||||||
header: 'Kategori Produk',
|
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',
|
accessorKey: 'source_warehouse',
|
||||||
@@ -117,56 +160,59 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setOpen(
|
|
||||||
isResponseSuccess(outgoingSapronaks)
|
|
||||||
? outgoingSapronaks.data.length > 0
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [outgoingSapronaks, isResponseSuccess]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full rounded-lg',
|
||||||
body: 'p-4 shadow',
|
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}
|
||||||
>
|
>
|
||||||
<Collapse
|
<div className='flex flex-col gap-2 my-4'>
|
||||||
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!'
|
|
||||||
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'>
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari Sapronak Keluar'
|
placeholder='Cari Sapronak Keluar'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
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>
|
</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>
|
<Table<ClosingOutgoingSapronak>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(outgoingSapronaks)
|
isResponseSuccess(outgoingSapronaks)
|
||||||
@@ -194,16 +240,21 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
setRowSelection={setRowSelection}
|
setRowSelection={setRowSelection}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: 'w-full mb-5!',
|
||||||
'w-full mb-20':
|
tableWrapperClassName:
|
||||||
isResponseSuccess(outgoingSapronaks) &&
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
||||||
outgoingSapronaks?.data?.length === 0,
|
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 Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
|
||||||
import PillBadge from '@/components/PillBadge';
|
import PillBadge from '@/components/PillBadge';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatDate, formatNumber } from '@/lib/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 { Icon } from '@iconify/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
|
||||||
|
|
||||||
const ChickinLogsView = ({
|
const ChickinLogsView = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -23,17 +22,12 @@ const ChickinLogsView = ({
|
|||||||
afterSubmit?: () => void;
|
afterSubmit?: () => void;
|
||||||
rawDataApprovals: BaseApproval[];
|
rawDataApprovals: BaseApproval[];
|
||||||
}) => {
|
}) => {
|
||||||
const confirmModal = useModal();
|
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
|
||||||
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
|
const [chickinErrorMessage, setChickinErrorMessage] = useState('');
|
||||||
|
const { openChickinApproveModal } = useChickinStore();
|
||||||
|
|
||||||
const handleClickApprove = () => {
|
const handleClickApprove = () => {
|
||||||
confirmModal.openModal();
|
openChickinApproveModal(initialValues, async (notes?: string) => {
|
||||||
};
|
|
||||||
|
|
||||||
const confirmationModalApproveClickHandler = async (notes?: string) => {
|
|
||||||
setChickinErrorMessage('');
|
setChickinErrorMessage('');
|
||||||
setIsApproveLoading(true);
|
|
||||||
const approveChickinRes = await ChickinApi.singleApproval(
|
const approveChickinRes = await ChickinApi.singleApproval(
|
||||||
initialValues?.id as number,
|
initialValues?.id as number,
|
||||||
'APPROVED',
|
'APPROVED',
|
||||||
@@ -46,9 +40,8 @@ const ChickinLogsView = ({
|
|||||||
toast.error(approveChickinRes?.message as string);
|
toast.error(approveChickinRes?.message as string);
|
||||||
setChickinErrorMessage(approveChickinRes?.message as string);
|
setChickinErrorMessage(approveChickinRes?.message as string);
|
||||||
}
|
}
|
||||||
confirmModal.closeModal();
|
|
||||||
setIsApproveLoading(false);
|
|
||||||
afterSubmit && afterSubmit();
|
afterSubmit && afterSubmit();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +76,7 @@ const ChickinLogsView = ({
|
|||||||
key={chickin.id || index}
|
key={chickin.id || index}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full mt-3',
|
||||||
body: 'p-3',
|
body: 'p-3',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -176,23 +169,6 @@ const ChickinLogsView = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal';
|
||||||
import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store';
|
import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store';
|
||||||
import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema';
|
import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema';
|
||||||
|
import { useChickinStore } from '@/stores/production/chickin/chickin.store';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -193,6 +194,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const confirmModal = useModal();
|
const confirmModal = useModal();
|
||||||
const successModal = useModal();
|
const successModal = useModal();
|
||||||
|
const chickinApproveModal = useModal();
|
||||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||||
'APPROVED'
|
'APPROVED'
|
||||||
);
|
);
|
||||||
@@ -200,6 +202,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const {
|
||||||
|
isChickinApproveModalOpen,
|
||||||
|
isChickinApproveLoading,
|
||||||
|
chickinApproveCallback,
|
||||||
|
closeChickinApproveModal,
|
||||||
|
setChickinApproveLoading,
|
||||||
|
} = useChickinStore();
|
||||||
|
|
||||||
// ===== Fetch Data =====
|
// ===== Fetch Data =====
|
||||||
const {
|
const {
|
||||||
@@ -271,7 +280,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isResponseSuccess(approveProjectFlockRes)) {
|
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();
|
confirmModal.closeModal();
|
||||||
}
|
}
|
||||||
if (isResponseError(approveProjectFlockRes)) {
|
if (isResponseError(approveProjectFlockRes)) {
|
||||||
@@ -288,6 +301,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
refreshProjectFlocks();
|
refreshProjectFlocks();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isChickinApproveModalOpen) {
|
||||||
|
chickinApproveModal.openModal();
|
||||||
|
} else {
|
||||||
|
chickinApproveModal.closeModal();
|
||||||
|
}
|
||||||
|
}, [isChickinApproveModalOpen, chickinApproveModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
successModal.openModal();
|
successModal.openModal();
|
||||||
@@ -970,6 +991,40 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
onClose={handleSuccessModalClose}
|
onClose={handleSuccessModalClose}
|
||||||
secondaryButton={undefined}
|
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 =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
APPROVED: 'Disetujui',
|
APPROVED: 'Disetujui',
|
||||||
Disetujui: 'Disetujui',
|
|
||||||
REJECTED: 'Ditolak',
|
REJECTED: 'Ditolak',
|
||||||
Ditolak: 'Ditolak',
|
CREATED: 'Pengajuan',
|
||||||
CREATED: 'Dibuat',
|
|
||||||
UPDATED: 'Diperbarui',
|
UPDATED: 'Diperbarui',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status: string): string => {
|
const getStatusText = (status: string): string => {
|
||||||
return statusTextMap[status] || status;
|
const normalizedStatus = status.toUpperCase();
|
||||||
|
return statusTextMap[normalizedStatus] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusBadgeColorMap: Record<string, Color> = {
|
const statusBadgeColorMap: Record<string, Color> = {
|
||||||
APPROVED: 'success',
|
APPROVED: 'success',
|
||||||
Disetujui: 'success',
|
|
||||||
approved: 'success',
|
|
||||||
disetujui: 'success',
|
|
||||||
REJECTED: 'error',
|
REJECTED: 'error',
|
||||||
Ditolak: 'error',
|
|
||||||
rejected: 'error',
|
|
||||||
ditolak: 'error',
|
|
||||||
CREATED: 'neutral',
|
CREATED: 'neutral',
|
||||||
Dibuat: 'neutral',
|
|
||||||
created: 'neutral',
|
|
||||||
dibuat: 'neutral',
|
|
||||||
UPDATED: 'warning',
|
UPDATED: 'warning',
|
||||||
Diperbarui: 'warning',
|
|
||||||
updated: 'warning',
|
|
||||||
diperbarui: 'warning',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadgeColor = (status: string): Color => {
|
const getStatusBadgeColor = (status: string): Color => {
|
||||||
return statusBadgeColorMap[status] || 'neutral';
|
const normalizedStatus = status.toUpperCase();
|
||||||
|
return statusBadgeColorMap[normalizedStatus] || 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
@@ -852,8 +840,7 @@ const RecordingTable = () => {
|
|||||||
|
|
||||||
const status = approval.action;
|
const status = approval.action;
|
||||||
const statusColor = getStatusBadgeColor(status);
|
const statusColor = getStatusBadgeColor(status);
|
||||||
|
const statusText = getStatusText(status);
|
||||||
const statusText = approval.step_name || getStatusText(status);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const generateDailyMarketingExcel = async (
|
|||||||
{ header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 },
|
{ header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 },
|
||||||
{ header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 },
|
{ header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 },
|
||||||
{ header: 'HPP (Rp)', key: 'hppPrice', width: 15 },
|
{ header: 'HPP (Rp)', key: 'hppPrice', width: 15 },
|
||||||
|
{ header: 'HPP Amount (Rp)', key: 'hppAmount', width: 20 },
|
||||||
{ header: 'Total (Rp)', key: 'salesAmount', width: 20 },
|
{ header: 'Total (Rp)', key: 'salesAmount', width: 20 },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export const generateDailyMarketingExcel = async (
|
|||||||
totalWeight: formatNumber(item.total_weight_kg || 0),
|
totalWeight: formatNumber(item.total_weight_kg || 0),
|
||||||
salesPrice: formatCurrency(item.sales_price_per_kg || 0),
|
salesPrice: formatCurrency(item.sales_price_per_kg || 0),
|
||||||
hppPrice: formatCurrency(item.hpp_price_per_kg || 0),
|
hppPrice: formatCurrency(item.hpp_price_per_kg || 0),
|
||||||
|
hppAmount: formatCurrency(item.hpp_amount || 0),
|
||||||
salesAmount: formatCurrency(item.sales_amount || 0),
|
salesAmount: formatCurrency(item.sales_amount || 0),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -75,21 +77,22 @@ export const generateDailyMarketingExcel = async (
|
|||||||
if (params.summaryTotal) {
|
if (params.summaryTotal) {
|
||||||
worksheet.addRow({
|
worksheet.addRow({
|
||||||
no: 'TOTAL',
|
no: 'TOTAL',
|
||||||
soDate: 'ALL',
|
soDate: '',
|
||||||
realizationDate: '-',
|
realizationDate: '',
|
||||||
aging: '-',
|
aging: '',
|
||||||
warehouse: '-',
|
warehouse: '',
|
||||||
customer: '-',
|
customer: '',
|
||||||
doNumber: '-',
|
doNumber: '',
|
||||||
sales: '-',
|
sales: '',
|
||||||
vehicleNumber: '-',
|
vehicleNumber: '',
|
||||||
marketingType: '-',
|
marketingType: '',
|
||||||
product: '-',
|
product: '',
|
||||||
qty: formatNumber(params.summaryTotal.total_qty || 0),
|
qty: formatNumber(params.summaryTotal.total_qty || 0),
|
||||||
averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0),
|
averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0),
|
||||||
totalWeight: formatNumber(params.summaryTotal.total_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),
|
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),
|
salesAmount: formatCurrency(params.summaryTotal.total_sales_amount || 0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user