mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-284): Refactor table component support for nesting header
This commit is contained in:
+103
-88
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user