mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
feat(FE): adding temporary perhitungan sapronak
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import SapronakCalculationTable from '@/components/pages/us-284/SapronakCalculationTable';
|
||||
|
||||
const PerhitunganSapronak = () => {
|
||||
return (
|
||||
<div className='size-full p-4'>
|
||||
<SapronakCalculationTable projectFlockId={1} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerhitunganSapronak;
|
||||
@@ -20,6 +20,10 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import toast from 'react-hot-toast';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
|
||||
const ProjectFlockDetail = ({
|
||||
projectFlock,
|
||||
@@ -38,6 +42,17 @@ const ProjectFlockDetail = ({
|
||||
(kandang) => kandang.id === Number(selectedKandangId)
|
||||
);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
isLoading: approvalsLoading,
|
||||
refresh: refreshApprovals,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: projectFlock?.approval,
|
||||
approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
|
||||
moduleName: 'PROJECT_FLOCKS',
|
||||
moduleId: projectFlock?.id.toString() ?? '',
|
||||
});
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
const deleteProjectFlockRes = await ProjectFlockApi.delete(
|
||||
@@ -90,6 +105,12 @@ const ProjectFlockDetail = ({
|
||||
<div className='border-t-1 border-gray-300'>
|
||||
<div className='p-4 flex flex-col gap-4'>
|
||||
<h2 className='text-2xl font-semibold'>Informasi Umum</h2>
|
||||
{/* Status Approval */}
|
||||
{approvals && !approvalsLoading && (
|
||||
<div className='text-sm my-3'>
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
</div>
|
||||
)}
|
||||
{/* Badge Row */}
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Badge
|
||||
@@ -151,7 +172,7 @@ const ProjectFlockDetail = ({
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
||||
{/* <div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
||||
<Icon width={14} height={14} icon={'mdi:clock'} /> History
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
@@ -163,7 +184,7 @@ const ProjectFlockDetail = ({
|
||||
height={11}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* BARIS 1 */}
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
export type RowSapronakCalculation = {
|
||||
id: number;
|
||||
tanggal: string;
|
||||
no_referensi: string;
|
||||
qty_masuk: number;
|
||||
qty_keluar: number;
|
||||
qty_pakai: number;
|
||||
uraian: string;
|
||||
kategori_produk: string;
|
||||
harga_beli_per_qty: number;
|
||||
total_harga: number;
|
||||
keterangan: string;
|
||||
};
|
||||
|
||||
export type TotalSapronakCalculation = {
|
||||
label: string;
|
||||
qty_masuk: number;
|
||||
qty_keluar: number;
|
||||
qty_pakai: number;
|
||||
harga_beli_per_qty: number;
|
||||
total_harga: number;
|
||||
};
|
||||
|
||||
export type SapronakCalculationItem = {
|
||||
rows: RowSapronakCalculation[];
|
||||
total: TotalSapronakCalculation;
|
||||
};
|
||||
|
||||
export type SapronakCalculation = {
|
||||
doc_broiler: SapronakCalculationItem;
|
||||
ovk: SapronakCalculationItem;
|
||||
pakan: SapronakCalculationItem;
|
||||
};
|
||||
|
||||
// Dummy data
|
||||
const DUMMY_SAPRONAK_CALCULATION: SapronakCalculation = {
|
||||
doc_broiler: {
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
tanggal: '11-Sep-2025',
|
||||
no_referensi: 'PO-PULLET-388',
|
||||
qty_masuk: 32800,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 32800,
|
||||
uraian: 'PULLET LOHMANN (16 MINGGU)',
|
||||
kategori_produk: 'PULLET LAYER',
|
||||
harga_beli_per_qty: 60136,
|
||||
total_harga: 1972556800,
|
||||
keterangan: '-',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tanggal: '24-Sep-2025',
|
||||
no_referensi: 'PO-PULLET-410',
|
||||
qty_masuk: 14758,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 14758,
|
||||
uraian: 'PULLET HY-LINE (17 MINGGU)',
|
||||
kategori_produk: 'PULLET LAYER',
|
||||
harga_beli_per_qty: 65421,
|
||||
total_harga: 965908998,
|
||||
keterangan: '-',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tanggal: '29-Sep-2025',
|
||||
no_referensi: 'PO-PULLET-196',
|
||||
qty_masuk: 35439,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 35439,
|
||||
uraian: 'PULLET ISA BROWN (15 MINGGU)',
|
||||
kategori_produk: 'PULLET LAYER',
|
||||
harga_beli_per_qty: 55909,
|
||||
total_harga: 1981297351,
|
||||
keterangan: '-',
|
||||
},
|
||||
],
|
||||
total: {
|
||||
label: 'TOTAL PULLET',
|
||||
qty_masuk: 82997,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 82997,
|
||||
harga_beli_per_qty: 59274.65,
|
||||
total_harga: 4919963149,
|
||||
},
|
||||
},
|
||||
ovk: {
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
tanggal: '28-Sep-2025',
|
||||
no_referensi: 'PO-OVK-276',
|
||||
qty_masuk: 52,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 52,
|
||||
uraian: 'ND-IB VACCINE',
|
||||
kategori_produk: 'OVK VAKSIN',
|
||||
harga_beli_per_qty: 204652,
|
||||
total_harga: 10641904,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tanggal: '26-Sep-2025',
|
||||
no_referensi: 'PO-OVK-811',
|
||||
qty_masuk: 43,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 43,
|
||||
uraian: 'GUMBORO VACCINE',
|
||||
kategori_produk: 'OVK VAKSIN',
|
||||
harga_beli_per_qty: 298379,
|
||||
total_harga: 12830297,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tanggal: '28-Sep-2025',
|
||||
no_referensi: 'PO-OVK-879',
|
||||
qty_masuk: 21,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 21,
|
||||
uraian: 'AMOXITIN SOLUBLE',
|
||||
kategori_produk: 'OVK OBAT',
|
||||
harga_beli_per_qty: 145952,
|
||||
total_harga: 3064992,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
tanggal: '11-Okt-2025',
|
||||
no_referensi: 'PO-OVK-340',
|
||||
qty_masuk: 38,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 38,
|
||||
uraian: 'TILOXIN SOLUBLE',
|
||||
kategori_produk: 'OVK OBAT',
|
||||
harga_beli_per_qty: 200424,
|
||||
total_harga: 7616112,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
tanggal: '27-Sep-2025',
|
||||
no_referensi: 'PO-OVK-364',
|
||||
qty_masuk: 7,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 7,
|
||||
uraian: 'EGG STIMULANT',
|
||||
kategori_produk: 'OVK VITAMIN',
|
||||
harga_beli_per_qty: 115024,
|
||||
total_harga: 805168,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
tanggal: '16-Sep-2025',
|
||||
no_referensi: 'PO-OVK-982',
|
||||
qty_masuk: 57,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 57,
|
||||
uraian: 'MULTIVIT-AMINO',
|
||||
kategori_produk: 'OVK VITAMIN',
|
||||
harga_beli_per_qty: 65123,
|
||||
total_harga: 3712011,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
tanggal: '04-Okt-2025',
|
||||
no_referensi: 'PO-OVK-876',
|
||||
qty_masuk: 4,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 4,
|
||||
uraian: 'BKC DESINFEKTAN',
|
||||
kategori_produk: 'OVK KIMIA',
|
||||
harga_beli_per_qty: 105677,
|
||||
total_harga: 422708,
|
||||
keterangan: 'Program kesehatan & biosecurity',
|
||||
},
|
||||
],
|
||||
total: {
|
||||
label: 'TOTAL OVK',
|
||||
qty_masuk: 222,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 222,
|
||||
harga_beli_per_qty: 172965.92,
|
||||
total_harga: 38481094,
|
||||
},
|
||||
},
|
||||
pakan: {
|
||||
rows: [
|
||||
{
|
||||
id: 1,
|
||||
tanggal: '13-Ags-2025',
|
||||
no_referensi: 'PO-FEED-730',
|
||||
qty_masuk: 4833,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 4833,
|
||||
uraian: 'FEED PRE-LAY',
|
||||
kategori_produk: 'PAKAN PRE-LAY',
|
||||
harga_beli_per_qty: 7578,
|
||||
total_harga: 36625874,
|
||||
keterangan: 'Konsumsi pakan kandang layer',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
tanggal: '28-Jul-2025',
|
||||
no_referensi: 'PO-FEED-555',
|
||||
qty_masuk: 6500,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 6500,
|
||||
uraian: 'FEED LAYER PHASE 1',
|
||||
kategori_produk: 'PAKAN LAYER',
|
||||
harga_beli_per_qty: 8116,
|
||||
total_harga: 52754000,
|
||||
keterangan: 'Konsumsi pakan kandang layer',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
tanggal: '24-Agu-2025',
|
||||
no_referensi: 'PO-FEED-683',
|
||||
qty_masuk: 8802,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 8802,
|
||||
uraian: 'FEED LAYER PHASE 2',
|
||||
kategori_produk: 'PAKAN LAYER',
|
||||
harga_beli_per_qty: 8801,
|
||||
total_harga: 77465402,
|
||||
keterangan: 'Konsumsi pakan kandang layer',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
tanggal: '02-Sep-2025',
|
||||
no_referensi: 'PO-FEED-448',
|
||||
qty_masuk: 2185,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 2185,
|
||||
uraian: 'JAGUNG GILING',
|
||||
kategori_produk: 'PAKAN MIX',
|
||||
harga_beli_per_qty: 5573,
|
||||
total_harga: 12187705,
|
||||
keterangan: 'Konsumsi pakan kandang layer',
|
||||
},
|
||||
],
|
||||
total: {
|
||||
label: 'TOTAL PAKAN',
|
||||
qty_masuk: 22320,
|
||||
qty_keluar: 0,
|
||||
qty_pakai: 22320,
|
||||
harga_beli_per_qty: 8092.39,
|
||||
total_harga: 179032981,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class ClosingService extends BaseApiService<unknown, unknown, unknown> {
|
||||
constructor(basePath: string = '') {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
async getPerhitunganSapronak(
|
||||
projectFlockId: number
|
||||
): Promise<BaseApiResponse<SapronakCalculation> | undefined> {
|
||||
// Dummy implementation - simulate API call with delay
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Retrieved sapronak calculation successfully',
|
||||
data: DUMMY_SAPRONAK_CALCULATION,
|
||||
});
|
||||
}, 500); // Simulate 500ms network delay
|
||||
});
|
||||
|
||||
/*
|
||||
// Real API implementation - uncomment when backend is ready
|
||||
try {
|
||||
const path = `${this.basePath}/project-flock/${projectFlockId}/sapronak-calculation`;
|
||||
|
||||
return await httpClient<BaseApiResponse<SapronakCalculation>>(path, {
|
||||
method: 'GET',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError<BaseApiResponse<SapronakCalculation>>(error)) {
|
||||
return error.response?.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
export const ClosingApi = new ClosingService(`/closing`);
|
||||
@@ -0,0 +1,204 @@
|
||||
'use client';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import {
|
||||
ClosingApi,
|
||||
RowSapronakCalculation,
|
||||
} from '@/components/pages/us-284/DummyDataSapronakCalculation';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const SapronakCalculationTable = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
const { data: sapronakCalculation, isLoading } = useSWR(
|
||||
`/sapronak-calculation`,
|
||||
() => ClosingApi.getPerhitunganSapronak(projectFlockId)
|
||||
);
|
||||
|
||||
const columns: ColumnDef<RowSapronakCalculation>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: 'Tanggal',
|
||||
accessorKey: 'tanggal',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as string;
|
||||
// Data already in DD-MMM-YYYY format, just display it
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'no_referensi',
|
||||
},
|
||||
{
|
||||
header: 'QTY Masuk',
|
||||
accessorKey: 'qty_masuk',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return formatNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'QTY Keluar',
|
||||
accessorKey: 'qty_keluar',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return formatNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'QTY Pakai',
|
||||
accessorKey: 'qty_pakai',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return formatNumber(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Uraian',
|
||||
accessorKey: 'uraian',
|
||||
},
|
||||
{
|
||||
header: 'Kategori Produk',
|
||||
accessorKey: 'kategori_produk',
|
||||
},
|
||||
{
|
||||
header: 'Harga Beli/Qty (Rp)',
|
||||
accessorKey: 'harga_beli_per_qty',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return formatCurrency(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Harga (Rp)',
|
||||
accessorKey: 'total_harga',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return formatCurrency(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Keterangan',
|
||||
accessorKey: 'keterangan',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as string;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{isLoading && (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
)}
|
||||
{isResponseSuccess(sapronakCalculation) && (
|
||||
<>
|
||||
<Card
|
||||
title='DOC Broiler'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={sapronakCalculation.data.doc_broiler.rows}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='OVK'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={sapronakCalculation.data.ovk.rows}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='Pakan'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={sapronakCalculation.data.pakan.rows}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
isResponseSuccess(sapronakCalculation) &&
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SapronakCalculationTable;
|
||||
@@ -58,6 +58,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
|
||||
icon: 'uil:wallet',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Perhitungan Sapronak',
|
||||
link: '/us-284',
|
||||
icon: 'uil:calculator',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Persediaan',
|
||||
link: '/inventory',
|
||||
|
||||
Reference in New Issue
Block a user