feat(FE-284): Refactor table component support for nesting header

This commit is contained in:
randy-ar
2025-12-09 17:57:46 +07:00
parent 3569955e7f
commit f9dfe7b27f
5 changed files with 1446 additions and 351 deletions
+103 -88
View File
@@ -14,6 +14,7 @@ import {
SortingState, SortingState,
OnChangeFn, OnChangeFn,
Row, Row,
HeaderContext,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils'; import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -57,8 +58,6 @@ export interface TableProps<TData extends object> {
setRowSelection?: OnChangeFn<Record<string, boolean>>; setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean); enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
renderFooter?: boolean; renderFooter?: boolean;
footerContent?: ReactNode;
footerData?: TData[];
withCheckbox?: boolean; withCheckbox?: boolean;
rowOptions?: number[]; rowOptions?: number[];
} }
@@ -73,22 +72,22 @@ const emptyContentDefaultValue = (
</div> </div>
); );
const TABLE_DEFAULT_STYLING = { export const TABLE_DEFAULT_STYLING = {
containerClassName: 'w-full mb-20', containerClassName: 'w-full mb-20',
tableWrapperClassName: tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-lg', 'overflow-x-auto border border-solid border-base-content/10 rounded-lg',
tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableClassName: 'font-inter w-full table-auto text-sm font-medium',
tableHeaderClassName: '', tableHeaderClassName: '',
headerRowClassName: '', headerRowClassName: '',
headerColumnClassName: 'px-4 py-3 text-base-content/50', headerColumnClassName:
'px-4 py-3 border-base-content/10 text-base-content/50',
tableBodyClassName: '', tableBodyClassName: '',
bodyRowClassName: 'border-t border-t-base-content/10', bodyRowClassName: 'border-t border-base-content/10',
bodyColumnClassName: 'px-4 py-3 text-base-content', bodyColumnClassName: 'px-4 py-3 text-base-content',
paginationClassName: '', paginationClassName: '',
tableFooterClassName: 'font-semibold border-base-content/10',
tableFooterClassName: '', footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10',
footerRowClassName: '', footerColumnClassName: 'p-4 text-base-content whitespace-nowrap',
footerColumnClassName: '',
}; };
const Table = <TData extends object>({ const Table = <TData extends object>({
@@ -111,8 +110,6 @@ const Table = <TData extends object>({
setRowSelection, setRowSelection,
enableRowSelection, enableRowSelection,
renderFooter = false, renderFooter = false,
footerContent,
footerData = [],
withCheckbox = false, withCheckbox = false,
rowOptions = [10, 20, 50, 100], rowOptions = [10, 20, 50, 100],
}: TableProps<TData>) => { }: TableProps<TData>) => {
@@ -187,14 +184,6 @@ const Table = <TData extends object>({
const table = useReactTable(tableOptions); const table = useReactTable(tableOptions);
const { setPageSize } = table; const { setPageSize } = table;
const footerTableOptions: TableOptions<TData> = {
columns,
data: footerData,
getCoreRowModel: getCoreRowModel(),
};
const footerTable = useReactTable(footerTableOptions);
const prevPageClickHandler = () => { const prevPageClickHandler = () => {
table.previousPage(); table.previousPage();
@@ -235,58 +224,82 @@ const Table = <TData extends object>({
key={headerGroup.id} key={headerGroup.id}
className={tableClassNames.headerRowClassName} className={tableClassNames.headerRowClassName}
> >
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => {
<th const columnRelativeDepth =
key={header.id} header.depth - header.column.depth;
colSpan={header.colSpan} if (
onClick={header.column.getToggleSortingHandler()} !header.isPlaceholder &&
className={cn( columnRelativeDepth > 1 &&
header.column.getCanSort() header.id === header.column.id
? 'cursor-pointer select-none' ) {
: '', return null;
{ }
'first:w-9 first:pr-0': withCheckbox, let rowSpan = 1;
}, if (header.isPlaceholder) {
tableClassNames.headerColumnClassName const leafs = header.getLeafHeaders();
)} rowSpan = leafs[leafs.length - 1].depth - header.depth;
> }
<div className='flex items-center gap-1'> return (
{flexRender( <th
header.column.columnDef.header, key={header.id}
header.getContext() colSpan={header.colSpan}
rowSpan={rowSpan}
onClick={header.column.getToggleSortingHandler()}
className={cn(
header.column.getCanSort()
? 'cursor-pointer select-none'
: '',
{
'first:w-9 first:pr-0': withCheckbox,
},
{
'border-b': header.colSpan > 1,
},
tableClassNames.headerColumnClassName
)} )}
>
<div
className={cn('flex items-center gap-1 min-h-full', {
'justify-center': header.colSpan > 1,
})}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getCanSort() && ( {header.column.getCanSort() && (
<div className='w-4 h-4 relative flex flex-col items-center'> <div className='w-4 h-4 relative flex flex-col items-center'>
<Icon <Icon
icon='heroicons:chevron-up-16-solid' icon='heroicons:chevron-up-16-solid'
width={18} width={18}
height={18} height={18}
className={cn( className={cn(
'absolute -top-1', 'absolute -top-1',
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'asc' header.column.getIsSorted() === 'asc'
? 'text-black' ? 'text-black'
: 'text-black/30' : 'text-black/30'
)} )}
/> />
<Icon <Icon
icon='heroicons:chevron-down-16-solid' icon='heroicons:chevron-down-16-solid'
width={18} width={18}
height={18} height={18}
className={cn( className={cn(
'absolute -bottom-1.5', 'absolute -bottom-1.5',
'transition-all ease-in-out duration-200', 'transition-all ease-in-out duration-200',
header.column.getIsSorted() === 'desc' header.column.getIsSorted() === 'desc'
? 'text-black' ? 'text-black'
: 'text-black/30' : 'text-black/30'
)} )}
/> />
</div> </div>
)} )}
</div> </div>
</th> </th>
))} );
})}
</tr> </tr>
))} ))}
</thead> </thead>
@@ -311,25 +324,27 @@ const Table = <TData extends object>({
</tr> </tr>
))} ))}
</tbody> </tbody>
<tfoot className={cn(className.tableFooterClassName)}> <tfoot className={cn(tableClassNames.tableFooterClassName)}>
{renderFooter && {renderFooter && (
(footerData && footerData.length > 0 <tr className={cn(tableClassNames.footerRowClassName)}>
? footerTable.getRowModel().rows.map((row) => ( {table.getAllLeafColumns().map((column) => (
<tr key={row.id} className={className.footerRowClassName}> <td
{row.getVisibleCells().map((cell) => ( key={column.id}
<td className={cn(
key={cell.id} { 'first:w-9 first:pr-0': withCheckbox },
className={className.footerColumnClassName} tableClassNames.footerColumnClassName
> )}
{flexRender( >
cell.column.columnDef.cell, {column.columnDef.footer &&
cell.getContext() flexRender(column.columnDef.footer, {
)} column,
</td> header: column.columnDef,
))} table,
</tr> } as HeaderContext<TData, unknown>)}
)) </td>
: footerContent)} ))}
</tr>
)}
</tfoot> </tfoot>
</table> </table>
</div> </div>
+166 -33
View File
@@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; import { GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
// TODO: delete this later, DONT HARDCODE USER DATA
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps { interface RequireAuthProps {
children?: ReactNode; children?: ReactNode;
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter(); const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth(); const { setUser, setIsLoadingUser } = useAuth();
const { const { data: userResponse, isLoading: isLoadingUserResponse } =
data: userResponse, useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
isLoading: isLoadingUserResponse, '/auth/sso/userinfo',
error: userErrorResponse, httpClientFetcher,
} = useSWRImmutable< {
GetMeResponse & { ok?: boolean }, shouldRetryOnError: false,
AxiosError<BaseApiResponse>, revalidateOnFocus: false,
SWRHttpKey revalidateOnReconnect: false,
>('/sso/userinfo', httpClientFetcher, { refreshInterval: 0,
shouldRetryOnError: false, }
revalidateOnFocus: false, );
revalidateOnReconnect: false,
refreshInterval: 0,
});
useEffect(() => { useEffect(() => {
setIsLoadingUser(isLoadingUserResponse); setIsLoadingUser(isLoadingUserResponse);
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); setUser(userResponse.data);
} else if ( } else {
isResponseError(userErrorResponse?.response?.data) && // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
typeof window !== 'undefined' // TODO: remove this later, DONT HARDCODE USER DATA
) { setUser(DUMMY_USER);
router.replace(
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
);
} }
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); }, [userResponse, setIsLoadingUser, setUser]);
if (isLoadingUserResponse && !userResponse && !userErrorResponse) { // TODO: uncomment this later
return ( // if (isLoadingUserResponse && !userResponse) {
<div className='w-full flex flex-row justify-center items-center p-4'> // return (
<span className='loading loading-spinner loading-xl' /> // <div className='w-full flex flex-row justify-center items-center p-4'>
</div> // <span className='loading loading-spinner loading-xl' />
); // </div>
} // );
// }
return <>{isResponseSuccess(userResponse) && children}</>; return <>{children}</>;
}; };
export default RequireAuth; export default RequireAuth;
@@ -5,7 +5,6 @@ import Card from '@/components/Card';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { cn, formatCurrency, formatNumber } from '@/lib/helper';
import { import {
ClosingSapronakCalculation,
RowSapronakCalculation, RowSapronakCalculation,
TotalSapronakCalculation, TotalSapronakCalculation,
} from '@/types/api/closing'; } from '@/types/api/closing';
@@ -20,10 +19,6 @@ interface ClosingSapronakCalculationTableProps {
projectFlockId: number; projectFlockId: number;
} }
interface FooterSapronakCalculationRow extends RowSapronakCalculation {
_isFooter: true;
}
const ClosingSapronakCalculationTable = ({ const ClosingSapronakCalculationTable = ({
type, type,
projectFlockId, projectFlockId,
@@ -33,176 +28,124 @@ const ClosingSapronakCalculationTable = ({
() => ClosingApi.getPerhitunganSapronak(projectFlockId) () => ClosingApi.getPerhitunganSapronak(projectFlockId)
); );
const columns: ColumnDef<RowSapronakCalculation>[] = useMemo( // Helper function to create columns with footer support
() => [ const createColumns = (
{
header: 'Tanggal',
accessorKey: 'tanggal',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
{
header: 'No. Referensi',
accessorKey: 'no_referensi',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
const value = props.getValue() as string;
if (isFooter) {
return (
<div className='font-semibold text-gray-900 col-span-2'>
{value}
</div>
);
}
return value || '-';
},
},
{
header: 'QTY Masuk',
accessorKey: 'qty_masuk',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
header: 'QTY Keluar',
accessorKey: 'qty_keluar',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
header: 'QTY Pakai',
accessorKey: 'qty_pakai',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
header: 'Uraian',
accessorKey: 'uraian',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
{
header: 'Kategori Produk',
accessorKey: 'kategori_produk',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'harga_beli_per_qty',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_harga',
cell: (props) => {
const value = props.getValue() as number;
const isFooter = '_isFooter' in props.row.original;
return (
<div className={isFooter ? 'font-semibold text-gray-900' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
header: 'Keterangan',
accessorKey: 'keterangan',
cell: (props) => {
const isFooter = '_isFooter' in props.row.original;
if (isFooter) return null;
const value = props.getValue() as string;
return value || '-';
},
},
],
[]
);
const createFooterRow = (
total?: TotalSapronakCalculation total?: TotalSapronakCalculation
): FooterSapronakCalculationRow[] => { ): ColumnDef<RowSapronakCalculation>[] => [
if (!total) return []; {
return [ header: 'Tanggal',
{ accessorKey: 'tanggal',
id: -999, cell: (props) => (props.getValue() as string) || '-',
tanggal: '', footer: 'Total',
no_referensi: total.label, },
qty_masuk: total.qty_masuk, {
qty_keluar: total.qty_keluar, header: 'No. Referensi',
qty_pakai: total.qty_pakai, accessorKey: 'no_referensi',
uraian: '', cell: (props) => (props.getValue() as string) || '-',
kategori_produk: '', footer: '',
harga_beli_per_qty: total.harga_beli_per_qty, },
total_harga: total.total_harga, {
keterangan: '', header: 'QTY Masuk',
_isFooter: true, accessorKey: 'qty_masuk',
}, cell: (props) => formatNumber(props.getValue() as number),
]; footer: total
}; ? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_masuk)}
</div>
)
: '',
},
{
header: 'QTY Keluar',
accessorKey: 'qty_keluar',
cell: (props) => formatNumber(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_keluar)}
</div>
)
: '',
},
{
header: 'QTY Pakai',
accessorKey: 'qty_pakai',
cell: (props) => formatNumber(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_pakai)}
</div>
)
: '',
},
{
header: 'Uraian',
accessorKey: 'uraian',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
{
header: 'Kategori Produk',
accessorKey: 'kategori_produk',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
{
header: 'Harga Beli/Qty (Rp)',
accessorKey: 'harga_beli_per_qty',
cell: (props) => formatCurrency(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatCurrency(total.harga_beli_per_qty)}
</div>
)
: '',
},
{
header: 'Total Harga (Rp)',
accessorKey: 'total_harga',
cell: (props) => formatCurrency(props.getValue() as number),
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatCurrency(total.total_harga)}
</div>
)
: '',
},
{
header: 'Keterangan',
accessorKey: 'keterangan',
cell: (props) => (props.getValue() as string) || '-',
footer: '',
},
];
const docBroilerFooter = useMemo( // Memoize columns untuk setiap kategori
const docBroilerColumns = useMemo(
() => () =>
isResponseSuccess(sapronakCalculation) isResponseSuccess(sapronakCalculation)
? createFooterRow(sapronakCalculation.data?.doc_broiler.total) ? createColumns(sapronakCalculation.data?.doc_broiler.total)
: [], : createColumns(),
[sapronakCalculation] [sapronakCalculation]
); );
const ovkFooter = useMemo( const ovkColumns = useMemo(
() => () =>
isResponseSuccess(sapronakCalculation) isResponseSuccess(sapronakCalculation)
? createFooterRow(sapronakCalculation.data?.ovk.total) ? createColumns(sapronakCalculation.data?.ovk.total)
: [], : createColumns(),
[sapronakCalculation] [sapronakCalculation]
); );
const pakanFooter = useMemo( const pakanColumns = useMemo(
() => () =>
isResponseSuccess(sapronakCalculation) isResponseSuccess(sapronakCalculation)
? createFooterRow(sapronakCalculation.data?.pakan.total) ? createColumns(sapronakCalculation.data?.pakan.total)
: [], : createColumns(),
[sapronakCalculation] [sapronakCalculation]
); );
@@ -212,39 +155,20 @@ const ClosingSapronakCalculationTable = ({
<> <>
<Card <Card
title='DOC Broiler' title='DOC Broiler'
variant='bordered'
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
className={{ className={{
wrapper: 'w-full', wrapper: 'w-full',
body: 'p-4 shadow',
}} }}
> >
<Table<RowSapronakCalculation> <Table<RowSapronakCalculation>
data={sapronakCalculation.data?.doc_broiler.rows ?? []} data={sapronakCalculation.data?.doc_broiler.rows ?? []}
columns={columns} columns={docBroilerColumns}
footerData={docBroilerFooter}
renderFooter={
(sapronakCalculation.data?.doc_broiler.rows.length ?? 0) > 0 &&
!!sapronakCalculation.data?.doc_broiler.total
}
className={{ className={{
containerClassName: cn({ containerClassName: 'my-4',
'mb-20':
sapronakCalculation.data?.doc_broiler.rows.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
}} }}
renderFooter
/> />
</Card> </Card>
@@ -259,29 +183,11 @@ const ClosingSapronakCalculationTable = ({
> >
<Table<RowSapronakCalculation> <Table<RowSapronakCalculation>
data={sapronakCalculation.data?.ovk.rows ?? []} data={sapronakCalculation.data?.ovk.rows ?? []}
columns={columns} columns={ovkColumns}
footerData={ovkFooter}
renderFooter={
(sapronakCalculation.data?.ovk.rows.length ?? 0) > 0 &&
!!sapronakCalculation.data?.ovk.total
}
className={{ className={{
containerClassName: cn({ containerClassName: 'my-4',
'mb-20': sapronakCalculation.data?.ovk.rows.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
}} }}
renderFooter
/> />
</Card> </Card>
@@ -296,29 +202,11 @@ const ClosingSapronakCalculationTable = ({
> >
<Table<RowSapronakCalculation> <Table<RowSapronakCalculation>
data={sapronakCalculation.data?.pakan.rows ?? []} data={sapronakCalculation.data?.pakan.rows ?? []}
columns={columns} columns={pakanColumns}
footerData={pakanFooter}
renderFooter={
(sapronakCalculation.data?.pakan.rows.length ?? 0) > 0 &&
!!sapronakCalculation.data?.pakan.total
}
className={{ className={{
containerClassName: cn({ containerClassName: 'my-4',
'mb-20': sapronakCalculation.data?.pakan.rows.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
tableFooterClassName:
'bg-gray-100 font-semibold border border-gray-200',
footerRowClassName: 'border-t-2 border-gray-300',
footerColumnClassName: 'px-6 py-3 text-xs text-gray-900',
}} }}
renderFooter
/> />
</Card> </Card>
</> </>
+984
View File
@@ -0,0 +1,984 @@
/**
* Dummy Data untuk Closing API
*
* File ini berisi dummy data untuk testing API Closing sebelum backend siap.
*
* Struktur data mengikuti tipe yang didefinisikan di @/types/api/closing.d.ts
*
* @example
* // 1. Menggunakan getAllFetcher dengan SWR:
* import useSWR from 'swr';
* import { ClosingApi } from '@/services/api/closing';
*
* const { data, error, isLoading } = useSWR(
* '/closings',
* ClosingApi.getAllFetcher.bind(ClosingApi)
* );
*
* if (data?.status === 'success') {
* console.log(data.data); // Array of Closing objects
* }
*
* @example
* // 2. Menggunakan getSingle:
* import { ClosingApi } from '@/services/api/closing';
*
* const response = await ClosingApi.getSingle(1);
* if (response?.status === 'success') {
* console.log(response.data); // Single Closing object
* } else if (response?.status === 'error') {
* console.error(response.message); // Error message
* }
*
* @example
* // 3. Menggunakan getGeneralInfo dengan SWR:
* import useSWR from 'swr';
* import { ClosingApi } from '@/services/api/closing';
*
* const closingId = 1;
* const { data, error, isLoading } = useSWR(
* closingId,
* (id: number) => ClosingApi.getGeneralInfo(id)
* );
*
* if (data?.status === 'success') {
* console.log(data.data); // ClosingGeneralInformation object
* }
*
* @example
* // 4. Menggunakan getAllIncomingSapronakFetcher dengan SWR:
* import useSWR from 'swr';
* import { ClosingApi } from '@/services/api/closing';
*
* const { data, error, isLoading } = useSWR(
* `${ClosingApi.basePath}/1/sapronak/incoming`,
* ClosingApi.getAllIncomingSapronakFetcher.bind(ClosingApi)
* );
*
* if (data?.status === 'success') {
* console.log(data.data); // Array of ClosingIncomingSapronak
* }
*
* @example
* // 5. Menggunakan getAllOutgoingSapronakFetcher dengan SWR:
* import useSWR from 'swr';
* import { ClosingApi } from '@/services/api/closing';
*
* const { data, error, isLoading } = useSWR(
* `${ClosingApi.basePath}/1/sapronak/outgoing`,
* ClosingApi.getAllOutgoingSapronakFetcher.bind(ClosingApi)
* );
*
* if (data?.status === 'success') {
* console.log(data.data); // Array of ClosingOutgoingSapronak
* }
*
* @see {@link /home/sweetpotet/Documents/projects/lti-web-client/src/types/api/closing.d.ts}
*/
import { format } from 'date-fns';
import {
Closing,
ClosingGeneralInformation,
ClosingIncomingSapronak,
ClosingOutgoingSapronak,
ClosingSapronakCalculation,
} from '@/types/api/closing';
import { CreatedUser, BaseApiResponse } from '@/types/api/api-general';
// Waktu saat ini untuk created_at/updated_at
const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
const today = format(new Date(), 'yyyy-MM-dd');
const yesterday = format(
new Date().setDate(new Date().getDate() - 1),
'yyyy-MM-dd'
);
const lastWeek = format(
new Date().setDate(new Date().getDate() - 7),
'yyyy-MM-dd'
);
const lastMonth = format(
new Date().setMonth(new Date().getMonth() - 1),
'yyyy-MM-dd'
);
// ======================
// 👤 Created User
// ======================
export const createdUser: CreatedUser = {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin Utama',
};
// ======================
// 📊 Closing Dummy Data
// ======================
export const dummyClosings: Closing[] = [
// 1. Closing dengan status Pengajuan - GROWING
{
id: 1,
location_id: 1,
location_name: 'Farm Sukajadi',
project_category: 'GROWING',
period: 1,
closing_date: today,
shed_label: 'Kandang A1, A2, A3',
shed_count: 3,
sales_paid_amount: 150000000,
sales_remaining_amount: 50000000,
sales_payment_status: 'Sebagian Lunas',
project_status: 'Pengajuan',
created_user: createdUser,
created_at: now,
updated_at: now,
},
// 2. Closing dengan status Aktif - LAYING
{
id: 2,
location_id: 2,
location_name: 'Farm Cihampelas',
project_category: 'LAYING',
period: 2,
closing_date: yesterday,
shed_label: 'Kandang B1, B2',
shed_count: 2,
sales_paid_amount: 200000000,
sales_remaining_amount: 0,
sales_payment_status: 'Lunas',
project_status: 'Aktif',
created_user: createdUser,
created_at: lastWeek,
updated_at: yesterday,
},
// 3. Closing dengan status Selesai - GROWING
{
id: 3,
location_id: 3,
location_name: 'Farm Pasteur',
project_category: 'GROWING',
period: 3,
closing_date: lastWeek,
shed_label: 'Kandang C1, C2, C3, C4',
shed_count: 4,
sales_paid_amount: 300000000,
sales_remaining_amount: 25000000,
sales_payment_status: 'Sebagian Lunas',
project_status: 'Selesai',
created_user: createdUser,
created_at: lastMonth,
updated_at: lastWeek,
},
// 4. Closing dengan status Aktif - LAYING
{
id: 4,
location_id: 4,
location_name: 'Farm Setiabudi',
project_category: 'LAYING',
period: 1,
closing_date: today,
shed_label: 'Kandang D1',
shed_count: 1,
sales_paid_amount: 75000000,
sales_remaining_amount: 75000000,
sales_payment_status: 'Belum Lunas',
project_status: 'Aktif',
created_user: createdUser,
created_at: yesterday,
updated_at: now,
},
// 5. Closing dengan status Selesai - GROWING
{
id: 5,
location_id: 5,
location_name: 'Farm Dago',
project_category: 'GROWING',
period: 4,
closing_date: lastMonth,
shed_label: 'Kandang E1, E2, E3, E4, E5',
shed_count: 5,
sales_paid_amount: 500000000,
sales_remaining_amount: 0,
sales_payment_status: 'Lunas',
project_status: 'Selesai',
created_user: createdUser,
created_at: lastMonth,
updated_at: lastMonth,
},
// 6. Closing dengan status Pengajuan - LAYING
{
id: 6,
location_id: 6,
location_name: 'Farm Lembang',
project_category: 'LAYING',
period: 2,
closing_date: undefined, // Belum ada tanggal closing
shed_label: 'Kandang F1, F2',
shed_count: 2,
sales_paid_amount: 0,
sales_remaining_amount: 180000000,
sales_payment_status: 'Belum Lunas',
project_status: 'Pengajuan',
created_user: createdUser,
created_at: now,
updated_at: now,
},
// 7. Closing dengan status Aktif - GROWING
{
id: 7,
location_id: 7,
location_name: 'Farm Ciwidey',
project_category: 'GROWING',
period: 1,
closing_date: yesterday,
shed_label: 'Kandang G1, G2, G3',
shed_count: 3,
sales_paid_amount: 120000000,
sales_remaining_amount: 30000000,
sales_payment_status: 'Sebagian Lunas',
project_status: 'Aktif',
created_user: createdUser,
created_at: lastWeek,
updated_at: yesterday,
},
// 8. Closing dengan status Selesai - LAYING
{
id: 8,
location_id: 8,
location_name: 'Farm Bandung Timur',
project_category: 'LAYING',
period: 3,
closing_date: lastMonth,
shed_label: 'Kandang H1, H2, H3, H4, H5, H6',
shed_count: 6,
sales_paid_amount: 600000000,
sales_remaining_amount: 0,
sales_payment_status: 'Lunas',
project_status: 'Selesai',
created_user: createdUser,
created_at: lastMonth,
updated_at: lastMonth,
},
];
// ======================
// 📊 Closing General Information Dummy Data
// ======================
export const dummyClosingGeneralInformations: ClosingGeneralInformation[] = [
// 1. General Info - GROWING - Pengajuan
{
id: 1,
location_id: 1,
location_name: 'Farm Sukajadi',
project_category: 'GROWING',
period: 1,
closing_date: today,
shed_label: 'Kandang A1, A2, A3',
shed_count: 3,
sales_paid_amount: 150000000,
sales_remaining_amount: 50000000,
sales_payment_status: 'Sebagian Lunas',
project_status: 'Pengajuan',
flock_id: 101,
project_type: 'GROWING',
population: 15000,
active_house_count: 3,
closing_status: 'Draft',
created_user: createdUser,
created_at: now,
updated_at: now,
},
// 2. General Info - LAYING - Aktif
{
id: 2,
location_id: 2,
location_name: 'Farm Cihampelas',
project_category: 'LAYING',
period: 2,
closing_date: yesterday,
shed_label: 'Kandang B1, B2',
shed_count: 2,
sales_paid_amount: 200000000,
sales_remaining_amount: 0,
sales_payment_status: 'Lunas',
project_status: 'Aktif',
flock_id: 102,
project_type: 'LAYING',
population: 10000,
active_house_count: 2,
closing_status: 'In Progress',
created_user: createdUser,
created_at: lastWeek,
updated_at: yesterday,
},
// 3. General Info - GROWING - Selesai
{
id: 3,
location_id: 3,
location_name: 'Farm Pasteur',
project_category: 'GROWING',
period: 3,
closing_date: lastWeek,
shed_label: 'Kandang C1, C2, C3, C4',
shed_count: 4,
sales_paid_amount: 300000000,
sales_remaining_amount: 25000000,
sales_payment_status: 'Sebagian Lunas',
project_status: 'Selesai',
flock_id: 103,
project_type: 'GROWING',
population: 20000,
active_house_count: 4,
closing_status: 'Completed',
created_user: createdUser,
created_at: lastMonth,
updated_at: lastWeek,
},
// 4. General Info - LAYING - Aktif
{
id: 4,
location_id: 4,
location_name: 'Farm Setiabudi',
project_category: 'LAYING',
period: 1,
closing_date: today,
shed_label: 'Kandang D1',
shed_count: 1,
sales_paid_amount: 75000000,
sales_remaining_amount: 75000000,
sales_payment_status: 'Belum Lunas',
project_status: 'Aktif',
flock_id: 104,
project_type: 'LAYING',
population: 5000,
active_house_count: 1,
closing_status: 'In Progress',
created_user: createdUser,
created_at: yesterday,
updated_at: now,
},
// 5. General Info - GROWING - Selesai
{
id: 5,
location_id: 5,
location_name: 'Farm Dago',
project_category: 'GROWING',
period: 4,
closing_date: lastMonth,
shed_label: 'Kandang E1, E2, E3, E4, E5',
shed_count: 5,
sales_paid_amount: 500000000,
sales_remaining_amount: 0,
sales_payment_status: 'Lunas',
project_status: 'Selesai',
flock_id: 105,
project_type: 'GROWING',
population: 25000,
active_house_count: 5,
closing_status: 'Completed',
created_user: createdUser,
created_at: lastMonth,
updated_at: lastMonth,
},
// 6. General Info - LAYING - Pengajuan
{
id: 6,
location_id: 6,
location_name: 'Farm Lembang',
project_category: 'LAYING',
period: 2,
closing_date: undefined,
shed_label: 'Kandang F1, F2',
shed_count: 2,
sales_paid_amount: 0,
sales_remaining_amount: 180000000,
sales_payment_status: 'Belum Lunas',
project_status: 'Pengajuan',
flock_id: 106,
project_type: 'LAYING',
population: 12000,
active_house_count: 2,
closing_status: 'Draft',
created_user: createdUser,
created_at: now,
updated_at: now,
},
// 7. General Info - GROWING - Aktif
{
id: 7,
location_id: 7,
location_name: 'Farm Ciwidey',
project_category: 'GROWING',
period: 1,
closing_date: yesterday,
shed_label: 'Kandang G1, G2, G3',
shed_count: 3,
sales_paid_amount: 120000000,
sales_remaining_amount: 30000000,
sales_payment_status: 'Sebagian Lunas',
project_status: 'Aktif',
flock_id: 107,
project_type: 'GROWING',
population: 18000,
active_house_count: 3,
closing_status: 'In Progress',
created_user: createdUser,
created_at: lastWeek,
updated_at: yesterday,
},
// 8. General Info - LAYING - Selesai
{
id: 8,
location_id: 8,
location_name: 'Farm Bandung Timur',
project_category: 'LAYING',
period: 3,
closing_date: lastMonth,
shed_label: 'Kandang H1, H2, H3, H4, H5, H6',
shed_count: 6,
sales_paid_amount: 600000000,
sales_remaining_amount: 0,
sales_payment_status: 'Lunas',
project_status: 'Selesai',
flock_id: 108,
project_type: 'LAYING',
population: 30000,
active_house_count: 6,
closing_status: 'Completed',
created_user: createdUser,
created_at: lastMonth,
updated_at: lastMonth,
},
];
// ======================
// 📦 Incoming Sapronak Dummy Data
// ======================
export const dummyIncomingSapronaks: ClosingIncomingSapronak[] = [
{
id: 1,
date: today,
reference_number: 'IN-2025-001',
transaction_type: 'Pembelian',
product_name: 'DOC Broiler Cobb 500',
product_category: 'DOC',
product_sub_category: 'DOC Broiler',
source_warehouse: 'Gudang Pusat',
destination_warehouse: 'Kandang A1',
quantity: 5000,
unit: 'Ekor',
formatted_quantity: '5,000 Ekor',
notes: 'DOC berkualitas tinggi dari supplier terpercaya',
},
{
id: 2,
date: yesterday,
reference_number: 'IN-2025-002',
transaction_type: 'Transfer Masuk',
product_name: 'Pakan Starter BR-1',
product_category: 'Pakan',
product_sub_category: 'Starter',
source_warehouse: 'Gudang Area Bandung',
destination_warehouse: 'Kandang B1',
quantity: 100,
unit: 'Sak',
formatted_quantity: '100 Sak (5,000 Kg)',
notes: 'Pakan starter untuk periode awal',
},
{
id: 3,
date: lastWeek,
reference_number: 'IN-2025-003',
transaction_type: 'Pembelian',
product_name: 'Vitamin B Complex',
product_category: 'OVK',
product_sub_category: 'Vitamin',
source_warehouse: 'Supplier Medion',
destination_warehouse: 'Gudang Farmasi',
quantity: 50,
unit: 'Botol',
formatted_quantity: '50 Botol',
notes: 'Vitamin untuk meningkatkan daya tahan tubuh',
},
{
id: 4,
date: today,
reference_number: 'IN-2025-004',
transaction_type: 'Pembelian',
product_name: 'Pakan Finisher BR-2',
product_category: 'Pakan',
product_sub_category: 'Finisher',
source_warehouse: 'Gudang Pusat',
destination_warehouse: 'Kandang C1',
quantity: 200,
unit: 'Sak',
formatted_quantity: '200 Sak (10,000 Kg)',
notes: 'Pakan finisher untuk periode akhir',
},
{
id: 5,
date: yesterday,
reference_number: 'IN-2025-005',
transaction_type: 'Transfer Masuk',
product_name: 'Antibiotik Enrofloxacin',
product_category: 'OVK',
product_sub_category: 'Obat',
source_warehouse: 'Gudang Area Jakarta',
destination_warehouse: 'Gudang Farmasi',
quantity: 30,
unit: 'Box',
formatted_quantity: '30 Box',
notes: 'Antibiotik untuk pencegahan penyakit',
},
];
// ======================
// 📤 Outgoing Sapronak Dummy Data
// ======================
export const dummyOutgoingSapronaks: ClosingOutgoingSapronak[] = [
{
id: 1,
date: today,
reference_number: 'OUT-2025-001',
transaction_type: 'Pemakaian',
product_name: 'Pakan Starter BR-1',
product_category: 'Pakan',
product_sub_category: 'Starter',
source_warehouse: 'Kandang A1',
destination_warehouse: 'Konsumsi Kandang A1',
quantity: 50,
unit: 'Sak',
formatted_quantity: '50 Sak (2,500 Kg)',
notes: 'Pemakaian pakan harian periode starter',
},
{
id: 2,
date: yesterday,
reference_number: 'OUT-2025-002',
transaction_type: 'Transfer Keluar',
product_name: 'DOC Broiler Cobb 500',
product_category: 'DOC',
product_sub_category: 'DOC Broiler',
source_warehouse: 'Kandang B1',
destination_warehouse: 'Kandang B2',
quantity: 1000,
unit: 'Ekor',
formatted_quantity: '1,000 Ekor',
notes: 'Transfer DOC ke kandang baru',
},
{
id: 3,
date: lastWeek,
reference_number: 'OUT-2025-003',
transaction_type: 'Pemakaian',
product_name: 'Vitamin B Complex',
product_category: 'OVK',
product_sub_category: 'Vitamin',
source_warehouse: 'Gudang Farmasi',
destination_warehouse: 'Konsumsi Kandang C1',
quantity: 10,
unit: 'Botol',
formatted_quantity: '10 Botol',
notes: 'Pemberian vitamin untuk meningkatkan kesehatan',
},
{
id: 4,
date: today,
reference_number: 'OUT-2025-004',
transaction_type: 'Pemakaian',
product_name: 'Pakan Finisher BR-2',
product_category: 'Pakan',
product_sub_category: 'Finisher',
source_warehouse: 'Kandang C1',
destination_warehouse: 'Konsumsi Kandang C1',
quantity: 80,
unit: 'Sak',
formatted_quantity: '80 Sak (4,000 Kg)',
notes: 'Pemakaian pakan harian periode finisher',
},
{
id: 5,
date: yesterday,
reference_number: 'OUT-2025-005',
transaction_type: 'Pemakaian',
product_name: 'Antibiotik Enrofloxacin',
product_category: 'OVK',
product_sub_category: 'Obat',
source_warehouse: 'Gudang Farmasi',
destination_warehouse: 'Konsumsi Kandang D1',
quantity: 5,
unit: 'Box',
formatted_quantity: '5 Box',
notes: 'Pengobatan untuk ayam yang sakit',
},
{
id: 6,
date: lastWeek,
reference_number: 'OUT-2025-006',
transaction_type: 'Transfer Keluar',
product_name: 'Pakan Starter BR-1',
product_category: 'Pakan',
product_sub_category: 'Starter',
source_warehouse: 'Kandang E1',
destination_warehouse: 'Kandang E2',
quantity: 30,
unit: 'Sak',
formatted_quantity: '30 Sak (1,500 Kg)',
notes: 'Transfer pakan antar kandang',
},
];
// ======================
// 📊 Perhitungan Sapronak Dummy Data
// ======================
export const dummySapronakCalculation: ClosingSapronakCalculation = {
// DOC Broiler Calculation
doc_broiler: {
rows: [
{
id: 1,
tanggal: today,
no_referensi: 'IN-2025-001',
qty_masuk: 5000,
qty_keluar: 0,
qty_pakai: 0,
uraian: 'DOC Broiler Cobb 500',
kategori_produk: 'DOC Broiler',
harga_beli_per_qty: 8000,
total_harga: 40000000,
keterangan: 'Pembelian DOC dari supplier',
},
{
id: 2,
tanggal: yesterday,
no_referensi: 'OUT-2025-002',
qty_masuk: 0,
qty_keluar: 1000,
qty_pakai: 0,
uraian: 'DOC Broiler Cobb 500',
kategori_produk: 'DOC Broiler',
harga_beli_per_qty: 8000,
total_harga: 8000000,
keterangan: 'Transfer DOC ke kandang lain',
},
{
id: 3,
tanggal: lastWeek,
no_referensi: 'USE-2025-001',
qty_masuk: 0,
qty_keluar: 0,
qty_pakai: 50,
uraian: 'DOC Broiler Cobb 500',
kategori_produk: 'DOC Broiler',
harga_beli_per_qty: 8000,
total_harga: 400000,
keterangan: 'Mortalitas DOC',
},
],
total: {
label: 'Total DOC Broiler',
qty_masuk: 5000,
qty_keluar: 1000,
qty_pakai: 50,
harga_beli_per_qty: 8000,
total_harga: 48400000,
},
},
// OVK Calculation
ovk: {
rows: [
{
id: 1,
tanggal: today,
no_referensi: 'IN-2025-003',
qty_masuk: 50,
qty_keluar: 0,
qty_pakai: 0,
uraian: 'Vitamin B Complex',
kategori_produk: 'Vitamin',
harga_beli_per_qty: 150000,
total_harga: 7500000,
keterangan: 'Pembelian vitamin',
},
{
id: 2,
tanggal: yesterday,
no_referensi: 'IN-2025-005',
qty_masuk: 30,
qty_keluar: 0,
qty_pakai: 0,
uraian: 'Antibiotik Enrofloxacin',
kategori_produk: 'Obat',
harga_beli_per_qty: 250000,
total_harga: 7500000,
keterangan: 'Pembelian antibiotik',
},
{
id: 3,
tanggal: lastWeek,
no_referensi: 'OUT-2025-003',
qty_masuk: 0,
qty_keluar: 0,
qty_pakai: 10,
uraian: 'Vitamin B Complex',
kategori_produk: 'Vitamin',
harga_beli_per_qty: 150000,
total_harga: 1500000,
keterangan: 'Pemakaian vitamin',
},
{
id: 4,
tanggal: yesterday,
no_referensi: 'OUT-2025-005',
qty_masuk: 0,
qty_keluar: 0,
qty_pakai: 5,
uraian: 'Antibiotik Enrofloxacin',
kategori_produk: 'Obat',
harga_beli_per_qty: 250000,
total_harga: 1250000,
keterangan: 'Pemakaian antibiotik',
},
],
total: {
label: 'Total OVK',
qty_masuk: 80,
qty_keluar: 0,
qty_pakai: 15,
harga_beli_per_qty: 200000,
total_harga: 17750000,
},
},
// Pakan Calculation
pakan: {
rows: [
{
id: 1,
tanggal: yesterday,
no_referensi: 'IN-2025-002',
qty_masuk: 100,
qty_keluar: 0,
qty_pakai: 0,
uraian: 'Pakan Starter BR-1',
kategori_produk: 'Starter',
harga_beli_per_qty: 450000,
total_harga: 45000000,
keterangan: 'Pembelian pakan starter',
},
{
id: 2,
tanggal: today,
no_referensi: 'IN-2025-004',
qty_masuk: 200,
qty_keluar: 0,
qty_pakai: 0,
uraian: 'Pakan Finisher BR-2',
kategori_produk: 'Finisher',
harga_beli_per_qty: 480000,
total_harga: 96000000,
keterangan: 'Pembelian pakan finisher',
},
{
id: 3,
tanggal: today,
no_referensi: 'OUT-2025-001',
qty_masuk: 0,
qty_keluar: 0,
qty_pakai: 50,
uraian: 'Pakan Starter BR-1',
kategori_produk: 'Starter',
harga_beli_per_qty: 450000,
total_harga: 22500000,
keterangan: 'Pemakaian pakan starter',
},
{
id: 4,
tanggal: today,
no_referensi: 'OUT-2025-004',
qty_masuk: 0,
qty_keluar: 0,
qty_pakai: 80,
uraian: 'Pakan Finisher BR-2',
kategori_produk: 'Finisher',
harga_beli_per_qty: 480000,
total_harga: 38400000,
keterangan: 'Pemakaian pakan finisher',
},
{
id: 5,
tanggal: lastWeek,
no_referensi: 'OUT-2025-006',
qty_masuk: 0,
qty_keluar: 30,
qty_pakai: 0,
uraian: 'Pakan Starter BR-1',
kategori_produk: 'Starter',
harga_beli_per_qty: 450000,
total_harga: 13500000,
keterangan: 'Transfer pakan ke kandang lain',
},
],
total: {
label: 'Total Pakan',
qty_masuk: 300,
qty_keluar: 30,
qty_pakai: 130,
harga_beli_per_qty: 465000,
total_harga: 215400000,
},
},
};
// ======================
// 🔧 Dummy API Response Functions
// ======================
/**
* Dummy implementation for getAllFetcher
* Returns all closing records
*/
export const dummyGetAllFetcher = async (): Promise<{
code: number;
status: 'success';
message: string;
data: Closing[];
}> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
code: 200,
status: 'success',
message: 'Data closing berhasil diambil',
data: dummyClosings,
};
};
/**
* Dummy implementation for getSingle
* Returns a single closing by ID
*/
export const dummyGetSingle = async (
id: number
): Promise<BaseApiResponse<Closing> | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 300));
const closing = dummyClosings.find((c) => c.id === id);
if (!closing) {
return {
code: 404,
status: 'error',
message: `Closing dengan ID ${id} tidak ditemukan`,
};
}
return {
code: 200,
status: 'success',
message: 'Data closing berhasil diambil',
data: closing,
};
};
/**
* Dummy implementation for getAllIncomingSapronakFetcher
* Returns all incoming sapronak records
*/
export const dummyGetAllIncomingSapronakFetcher = async (): Promise<{
code: number;
status: 'success';
message: string;
data: ClosingIncomingSapronak[];
}> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return {
code: 200,
status: 'success',
message: 'Data sapronak masuk berhasil diambil',
data: dummyIncomingSapronaks,
};
};
/**
* Dummy implementation for getAllOutgoingSapronakFetcher
* Returns all outgoing sapronak records
*/
export const dummyGetAllOutgoingSapronakFetcher = async (): Promise<{
code: number;
status: 'success';
message: string;
data: ClosingOutgoingSapronak[];
}> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return {
code: 200,
status: 'success',
message: 'Data sapronak keluar berhasil diambil',
data: dummyOutgoingSapronaks,
};
};
/**
* Dummy implementation for getGeneralInfo
* Returns closing general information by ID
*/
export const dummyGetGeneralInfo = async (
id: number
): Promise<BaseApiResponse<ClosingGeneralInformation> | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 300));
const closingInfo = dummyClosingGeneralInformations.find((c) => c.id == id);
if (!closingInfo) {
return {
code: 404,
status: 'error',
message: `Closing general information dengan ID ${id} tidak ditemukan`,
};
}
return {
code: 200,
status: 'success',
message: 'Data closing general information berhasil diambil',
data: closingInfo,
};
};
/**
* Dummy implementation for getPerhitunganSapronak
* Returns sapronak calculation data
*/
export const dummyGetPerhitunganSapronak = async (
id: number
): Promise<
| {
code: number;
status: 'success';
message: string;
data: ClosingSapronakCalculation;
}
| undefined
> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return {
code: 200,
status: 'success',
message: 'Data perhitungan sapronak berhasil diambil',
data: dummySapronakCalculation,
};
};
+79 -4
View File
@@ -8,17 +8,62 @@ import {
ClosingOutgoingSapronak, ClosingOutgoingSapronak,
ClosingSapronakCalculation, ClosingSapronakCalculation,
} from '@/types/api/closing'; } from '@/types/api/closing';
import { httpClient, httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import {
dummyGetAllFetcher,
dummyGetSingle,
dummyGetAllIncomingSapronakFetcher,
dummyGetAllOutgoingSapronakFetcher,
dummyGetGeneralInfo,
dummyGetPerhitunganSapronak,
} from '@/dummy/closing.dummy';
import { httpClient, httpClientFetcher } from '@/services/http/client';
export class ClosingApiService extends BaseApiService<Closing, null, null> { export class ClosingApiService extends BaseApiService<Closing, null, null> {
constructor(basePath: string) { constructor(basePath: string) {
super(basePath); super(basePath);
} }
async getAllFetcher(endpoint: string): Promise<BaseApiResponse<Closing[]>> {
// TODO: Remove this block when backend is ready
// return await dummyGetAllFetcher();
// Uncomment this when backend is ready
return await httpClientFetcher<BaseApiResponse<Closing[]>>(endpoint);
}
async getSingle(id: number): Promise<BaseApiResponse<Closing> | undefined> {
// TODO: Remove this block when backend is ready
// try {
// return await dummyGetSingle(id);
// } catch (error) {
// if (axios.isAxiosError<BaseApiResponse<Closing>>(error)) {
// return error.response?.data;
// }
// return undefined;
// }
// Uncomment this when backend is ready
try {
const getSinglePath = `${this.basePath}/${id}`;
const getSingleRes =
await httpClient<BaseApiResponse<Closing>>(getSinglePath);
return getSingleRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse<Closing>>(error)) {
return error.response?.data;
}
return undefined;
}
}
async getAllIncomingSapronakFetcher( async getAllIncomingSapronakFetcher(
endpoint: string endpoint: string
): Promise<BaseApiResponse<ClosingIncomingSapronak[]>> { ): Promise<BaseApiResponse<ClosingIncomingSapronak[]>> {
// TODO: Remove this block when backend is ready
// return await dummyGetAllIncomingSapronakFetcher();
// Uncomment this when backend is ready
return await httpClientFetcher<BaseApiResponse<ClosingIncomingSapronak[]>>( return await httpClientFetcher<BaseApiResponse<ClosingIncomingSapronak[]>>(
endpoint endpoint
); );
@@ -27,19 +72,37 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
async getAllOutgoingSapronakFetcher( async getAllOutgoingSapronakFetcher(
endpoint: string endpoint: string
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> { ): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
// TODO: Remove this block when backend is ready
// return await dummyGetAllOutgoingSapronakFetcher();
// Uncomment this when backend is ready
return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>( return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>(
endpoint endpoint
); );
} }
async getGeneralInfo(id: number) { async getGeneralInfo(
id: number
): Promise<BaseApiResponse<ClosingGeneralInformation> | undefined> {
// TODO: Remove this block when backend is ready
// try {
// return await dummyGetGeneralInfo(id);
// } catch (error) {
// if (
// axios.isAxiosError<BaseApiResponse<ClosingGeneralInformation>>(error)
// ) {
// return error.response?.data;
// }
// return undefined;
// }
// Uncomment this when backend is ready
try { try {
const getGeneralInfoPath = `${this.basePath}/${id}`; const getGeneralInfoPath = `${this.basePath}/${id}`;
const getGeneralInfoRes = const getGeneralInfoRes =
await httpClient<BaseApiResponse<ClosingGeneralInformation>>( await httpClient<BaseApiResponse<ClosingGeneralInformation>>(
getGeneralInfoPath getGeneralInfoPath
); );
return getGeneralInfoRes; return getGeneralInfoRes;
} catch (error) { } catch (error) {
if ( if (
@@ -54,9 +117,21 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
async getPerhitunganSapronak( async getPerhitunganSapronak(
id: number id: number
): Promise<BaseApiResponse<ClosingSapronakCalculation> | undefined> { ): Promise<BaseApiResponse<ClosingSapronakCalculation> | undefined> {
// TODO: Remove this block when backend is ready
// try {
// return await dummyGetPerhitunganSapronak(id);
// } catch (error) {
// if (
// axios.isAxiosError<BaseApiResponse<ClosingSapronakCalculation>>(error)
// ) {
// return error.response?.data;
// }
// return undefined;
// }
// Uncomment this when backend is ready
try { try {
const path = `${this.basePath}/${id}/perhitungan_sapronak`; const path = `${this.basePath}/${id}/perhitungan_sapronak`;
return await httpClient<BaseApiResponse<ClosingSapronakCalculation>>( return await httpClient<BaseApiResponse<ClosingSapronakCalculation>>(
path, path,
{ {