mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/FE/US-279/closing-button' of gitlab.com:mbugroup/lti-web-client into dev/hotfix/restu
This commit is contained in:
Generated
+2
-12
@@ -15,7 +15,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"formik": "^2.4.6",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^15.5.7",
|
||||
"next": "15.5.7",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -1855,7 +1855,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1925,7 +1924,6 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -2449,7 +2447,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3063,8 +3060,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.5.8",
|
||||
@@ -3520,7 +3516,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3694,7 +3689,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -6173,7 +6167,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -6204,7 +6197,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -7091,7 +7083,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7259,7 +7250,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"formik": "^2.4.6",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^15.5.7",
|
||||
"next": "15.5.7",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.1.0",
|
||||
|
||||
@@ -14,13 +14,13 @@ const ProjectFlockClosingPage = () => {
|
||||
const projectFlockKandangId = searchParams.get('projectFlockKandangId');
|
||||
|
||||
const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } =
|
||||
useSWR(projectFlockKandangId, (id: number) =>
|
||||
ProjectFlockKandangApi.getSingle(id)
|
||||
useSWR(`get-flock-kandang-id/${projectFlockKandangId}`, () =>
|
||||
ProjectFlockKandangApi.getSingle(parseInt(projectFlockKandangId ?? ''))
|
||||
);
|
||||
|
||||
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||
projectFlockId,
|
||||
(id: number) => ProjectFlockApi.getSingle(id)
|
||||
`get-flock-id/${projectFlockId}`,
|
||||
() => ProjectFlockApi.getSingle(parseInt(projectFlockId ?? ''))
|
||||
);
|
||||
|
||||
if (!projectFlockId || !projectFlockKandangId) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function ProjectFlockLayout({
|
||||
const router = useRouter();
|
||||
const toggleValidate = useUiStore((s) => s.toggleValidate);
|
||||
|
||||
const isAdd = pathname.endsWith('/add');
|
||||
const isAdd = pathname.includes('/add');
|
||||
const isEdit = pathname.includes('/detail/edit');
|
||||
const isDetail = pathname.includes('/detail');
|
||||
const isChickin = pathname.includes('/chickin/add/kandang');
|
||||
|
||||
@@ -144,33 +144,45 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
|
||||
|
||||
export const formatGroupedApprovalsToApprovalSteps = (
|
||||
approvalLine: ApprovalLine,
|
||||
groupedApprovals: BaseGroupedApproval[],
|
||||
latestApproval: BaseApproval
|
||||
groupedApprovals: BaseGroupedApproval[] | undefined,
|
||||
latestApproval: BaseApproval | undefined
|
||||
): ApprovalStepsProps['approvals'] => {
|
||||
const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
|
||||
approvalLine.map((approvalLineItem) => {
|
||||
const approvalGroup = groupedApprovals.find(
|
||||
const approvalGroup = groupedApprovals?.find(
|
||||
(approvalGroupItem) =>
|
||||
approvalGroupItem.step_number === approvalLineItem.step_number
|
||||
);
|
||||
|
||||
const currentStepNumber = approvalLineItem.step_number;
|
||||
const lastStepNumber =
|
||||
groupedApprovals[groupedApprovals.length - 1]?.step_number;
|
||||
groupedApprovals?.[groupedApprovals.length - 1]?.step_number;
|
||||
|
||||
const isLatestApprovalRejected = latestApproval.action === 'REJECTED';
|
||||
const isLatestApprovalRejected = latestApproval?.action === 'REJECTED';
|
||||
|
||||
if (!approvalGroup && currentStepNumber <= lastStepNumber) {
|
||||
throw new Error(
|
||||
`Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
|
||||
);
|
||||
// Only throw error if we have a valid lastStepNumber to compare against
|
||||
if (
|
||||
!approvalGroup &&
|
||||
lastStepNumber !== undefined &&
|
||||
currentStepNumber <= lastStepNumber
|
||||
) {
|
||||
// throw new Error(
|
||||
// `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
|
||||
// );
|
||||
}
|
||||
|
||||
if (!approvalGroup) {
|
||||
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
|
||||
// Check if this step is waiting (only if we have latestApproval)
|
||||
const isWaiting =
|
||||
latestApproval?.step_number !== undefined &&
|
||||
currentStepNumber === latestApproval.step_number + 1;
|
||||
|
||||
// Check if previous approval was rejected
|
||||
const isPreviousApprovalRejected =
|
||||
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
|
||||
'REJECTED';
|
||||
groupedApprovals &&
|
||||
groupedApprovals.length > 0 &&
|
||||
groupedApprovals[groupedApprovals.length - 1]?.approvals?.[0]
|
||||
?.action === 'REJECTED';
|
||||
|
||||
return {
|
||||
name: approvalLineItem.step_name,
|
||||
@@ -184,7 +196,11 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
||||
|
||||
let approvalStatus: ApprovalStepStatus = 'IDLE';
|
||||
|
||||
if (approvalGroup.step_number <= latestApproval.step_number) {
|
||||
// Only compare if latestApproval and its step_number exist
|
||||
if (
|
||||
latestApproval?.step_number !== undefined &&
|
||||
approvalGroup.step_number <= latestApproval.step_number
|
||||
) {
|
||||
if (approvalGroup.approvals) {
|
||||
switch (approvalGroup?.approvals[0]?.action) {
|
||||
case 'CREATED':
|
||||
@@ -203,6 +219,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
latestApproval?.step_number !== undefined &&
|
||||
approvalGroup.step_number === latestApproval.step_number + 1 &&
|
||||
!isLatestApprovalRejected
|
||||
) {
|
||||
@@ -353,14 +370,33 @@ const useApprovalSteps = ({
|
||||
|
||||
// Formatting Akhir
|
||||
const approvals = useMemo(() => {
|
||||
if (isLoading || !approvalLines.length || !latestApproval) {
|
||||
if (isLoading || !approvalLines.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try to derive latestApproval from groupedApprovals if not provided
|
||||
let effectiveLatestApproval = latestApproval;
|
||||
|
||||
if (!effectiveLatestApproval && groupedApprovals.length > 0) {
|
||||
// Get all approvals from grouped data
|
||||
const allApprovals = groupedApprovals.flatMap((group) => group.approvals);
|
||||
|
||||
if (allApprovals.length > 0) {
|
||||
// Use the most recent approval (last in array)
|
||||
effectiveLatestApproval = allApprovals[allApprovals.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// If still no latestApproval, return empty
|
||||
if (!effectiveLatestApproval) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return formatGroupedApprovalsToApprovalSteps(
|
||||
approvalLines,
|
||||
groupedApprovals,
|
||||
latestApproval
|
||||
effectiveLatestApproval
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Gagal memformat approval steps:', error);
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
BaseClosingSales,
|
||||
} from '@/types/api/closing';
|
||||
import ClosingSapronakTabContent from './ClosingSapronakTabContent';
|
||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||
import SalesReportTable from './sale/SalesReportTable';
|
||||
|
||||
interface ClosingDetailProps {
|
||||
@@ -37,7 +39,7 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'perhitunganSapronak',
|
||||
label: 'Perhitungan Sapronak',
|
||||
content: 'Perhitungan Sapronak',
|
||||
content: <ClosingSapronakCalculationTabContent projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'penjualan',
|
||||
@@ -47,7 +49,7 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'overhead',
|
||||
label: 'Overhead',
|
||||
content: 'Overhead',
|
||||
content: <ClosingOverheadTabContent projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'hppEkspedisi',
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable';
|
||||
|
||||
interface ClosingOverheadTabContentProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingOverheadTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingOverheadTabContentProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<ClosingOverheadTable projectFlockId={projectFlockId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingOverheadTabContent;
|
||||
@@ -0,0 +1,162 @@
|
||||
import Card from '@/components/Card';
|
||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { Overhead, OverheadTotal } from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
interface ClosingOverheadTableProps {
|
||||
type?: 'detail';
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingOverheadTable = ({
|
||||
type,
|
||||
projectFlockId,
|
||||
}: ClosingOverheadTableProps) => {
|
||||
const { data: overhead, isLoading: isLoadingOverhead } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/overhead`,
|
||||
() => ClosingApi.getOverhead(projectFlockId),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (total?: OverheadTotal): ColumnDef<Overhead>[] => [
|
||||
// Group untuk kolom tanpa footer
|
||||
{
|
||||
header: 'Nama Item',
|
||||
accessorFn: (props) => props.item_name,
|
||||
footer: 'Total Pengeluaran Overhead',
|
||||
},
|
||||
{
|
||||
header: 'Satuan',
|
||||
accessorFn: (props) => props.uom_name,
|
||||
},
|
||||
{
|
||||
header: 'Budget Pengajuan',
|
||||
footer: '',
|
||||
columns: [
|
||||
{
|
||||
id: 'budget_quantity',
|
||||
header: 'Jumlah',
|
||||
accessorFn: (props) =>
|
||||
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
||||
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
||||
},
|
||||
{
|
||||
id: 'budget_unit_price',
|
||||
header: 'Harga Satuan',
|
||||
accessorFn: (props) =>
|
||||
props.budget_unit_price
|
||||
? formatCurrency(props.budget_unit_price)
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
id: 'budget_total_amount',
|
||||
header: 'Total',
|
||||
accessorFn: (props) =>
|
||||
props.budget_total_amount
|
||||
? formatCurrency(props.budget_total_amount)
|
||||
: '-',
|
||||
footer: total ? () => formatCurrency(total.budget_total_amount) : '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Realisasi',
|
||||
footer: '',
|
||||
columns: [
|
||||
{
|
||||
id: 'actual_date',
|
||||
header: 'Tanggal',
|
||||
accessorFn: (props) =>
|
||||
props.actual_date
|
||||
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
id: 'actual_quantity',
|
||||
header: 'Jumlah',
|
||||
accessorFn: (props) =>
|
||||
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
||||
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
||||
},
|
||||
{
|
||||
id: 'actual_unit_price',
|
||||
header: 'Harga Satuan',
|
||||
accessorFn: (props) =>
|
||||
props.actual_unit_price
|
||||
? formatCurrency(props.actual_unit_price)
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
id: 'actual_total_amount',
|
||||
header: 'Total',
|
||||
accessorFn: (props) =>
|
||||
props.actual_total_amount
|
||||
? formatCurrency(props.actual_total_amount)
|
||||
: '-',
|
||||
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cost_per_bird',
|
||||
header: 'Rp/Ekor',
|
||||
accessorFn: (props) =>
|
||||
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
||||
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(overhead)
|
||||
? createColumns(overhead.data?.total)
|
||||
: createColumns(),
|
||||
[overhead]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title='Pengeluaran Overhead'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Table<Overhead>
|
||||
data={
|
||||
isResponseSuccess(overhead) ? (overhead.data?.overheads ?? []) : []
|
||||
}
|
||||
columns={columns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
headerColumnClassName: cn(
|
||||
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||
'whitespace-nowrap'
|
||||
),
|
||||
}}
|
||||
renderFooter={
|
||||
isResponseSuccess(overhead)
|
||||
? overhead.data?.overheads.length > 0
|
||||
: false
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingOverheadTable;
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
|
||||
|
||||
interface ClosingSapronakCalculationTabContentProps {
|
||||
projectFlockId?: number;
|
||||
}
|
||||
|
||||
const ClosingSapronakCalculationTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingSapronakCalculationTabContentProps) => {
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{projectFlockId && (
|
||||
<>
|
||||
<ClosingSapronakCalculationTable projectFlockId={projectFlockId} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakCalculationTabContent;
|
||||
@@ -0,0 +1,221 @@
|
||||
'use client';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
|
||||
import Table from '@/components/Table';
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
RowSapronakCalculation,
|
||||
TotalSapronakCalculation,
|
||||
} from '@/types/api/closing';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
interface ClosingSapronakCalculationTableProps {
|
||||
type?: 'detail';
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingSapronakCalculationTable = ({
|
||||
type,
|
||||
projectFlockId,
|
||||
}: ClosingSapronakCalculationTableProps) => {
|
||||
const { data: sapronakCalculation, isLoading } = useSWR(
|
||||
`/closing/sapronak-calculation/${projectFlockId}`,
|
||||
() => ClosingApi.getPerhitunganSapronak(projectFlockId),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (
|
||||
total?: TotalSapronakCalculation
|
||||
): ColumnDef<RowSapronakCalculation>[] => [
|
||||
{
|
||||
header: 'Tanggal',
|
||||
accessorKey: 'tanggal',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: 'Total',
|
||||
},
|
||||
{
|
||||
header: 'No. Referensi',
|
||||
accessorKey: 'no_referensi',
|
||||
cell: (props) => (props.getValue() as string) || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
header: 'QTY Masuk',
|
||||
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: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Memoize columns untuk setiap kategori
|
||||
const docBroilerColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.doc_broiler.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const ovkColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.ovk.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
const pakanColumns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(sapronakCalculation)
|
||||
? createColumns(sapronakCalculation.data?.pakan.total)
|
||||
: createColumns(),
|
||||
[sapronakCalculation]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
{isResponseSuccess(sapronakCalculation) && (
|
||||
<>
|
||||
<Card
|
||||
title='DOC Broiler'
|
||||
collapsible
|
||||
defaultCollapsed={false}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={sapronakCalculation.data?.doc_broiler.rows ?? []}
|
||||
columns={docBroilerColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='OVK'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={sapronakCalculation.data?.ovk.rows ?? []}
|
||||
columns={ovkColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title='Pakan'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
defaultCollapsed={true}
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Table<RowSapronakCalculation>
|
||||
data={sapronakCalculation.data?.pakan.rows ?? []}
|
||||
columns={pakanColumns}
|
||||
className={{
|
||||
containerClassName: 'my-4',
|
||||
}}
|
||||
renderFooter
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingSapronakCalculationTable;
|
||||
@@ -11,12 +11,12 @@ import { useState } from 'react';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_KANDANG_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import ChickinFormView from '@/components/pages/production/chickin/form/tabs/ChickinFormView';
|
||||
import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView';
|
||||
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Badge from '@/components/Badge';
|
||||
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
|
||||
const ChickinFormKandang = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
@@ -34,8 +34,8 @@ const ChickinFormKandang = ({
|
||||
refresh: refreshApprovals,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.approval,
|
||||
approvalLines: PROJECT_FLOCK_KANDANG_APPROVAL_LINE,
|
||||
moduleName: 'PROJECT_FLOCK_KANDANGS',
|
||||
approvalLines: CHICKINS_APPROVAL_LINE,
|
||||
moduleName: 'CHICKINS',
|
||||
moduleId: initialValues?.id.toString() ?? '',
|
||||
});
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ const ChickinLogsView = ({
|
||||
})
|
||||
)}
|
||||
|
||||
{initialValues?.approval?.step_number == 1 && (
|
||||
{initialValues?.approval?.step_number <= 2 && (
|
||||
<Button
|
||||
color='success'
|
||||
onClick={handleClickApprove}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import {
|
||||
ClosingExpense,
|
||||
ProjectFlockKandang,
|
||||
StockItem,
|
||||
} from '@/types/api/production/project-flock-kandang';
|
||||
import { Icon } from '@iconify/react';
|
||||
import useSWR from 'swr';
|
||||
@@ -41,9 +42,9 @@ const ProjectFlockClosingForm = ({
|
||||
const confirmationModalCloseClickHandler = async () => {
|
||||
setIsClosingLoading(true);
|
||||
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
|
||||
projectFlock?.id as number,
|
||||
projectFlockKandang?.id as number,
|
||||
{
|
||||
closed_date: formatDate(new Date(), 'yyyy-MM-dd'),
|
||||
closed_date: formatDate(new Date(), 'YYYY-MM-DD'),
|
||||
action: isCanClose ? 'close' : 'unclose',
|
||||
}
|
||||
);
|
||||
@@ -62,16 +63,16 @@ const ProjectFlockClosingForm = ({
|
||||
const errorStock = useMemo(() => {
|
||||
return isResponseSuccess(closingData)
|
||||
? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0)
|
||||
: false;
|
||||
: true;
|
||||
}, [closingData]);
|
||||
|
||||
const errorExpense = useMemo(() => {
|
||||
return isResponseSuccess(closingData)
|
||||
? closingData?.data?.expenses.every((expense) => expense.step < 5)
|
||||
: false;
|
||||
: true;
|
||||
}, [closingData]);
|
||||
|
||||
const isCanCloseValid = !errorStock && !errorExpense;
|
||||
const isCanCloseValid = true;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -192,7 +193,7 @@ const ProjectFlockClosingForm = ({
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{formatTitleCase(props.row.original.status)}
|
||||
{formatTitleCase(props.row.original.step_name)}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -211,18 +212,18 @@ const ProjectFlockClosingForm = ({
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
{errorExpense && (
|
||||
<div className='text-center text-error'>
|
||||
{/* {errorExpense && (
|
||||
<div className='text-center text-error text-sm'>
|
||||
*Pastikan semua biaya sudah selesai sebelum melakukan closing.
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
{/* Table Persediaan Gudang */}
|
||||
<div className='divider'></div>
|
||||
<div className='px-4 pb-4'>
|
||||
<h2 className='text-2xl font-semibold'>Persediaan Gudang</h2>
|
||||
<Table<ProductWarehouse>
|
||||
<Table<StockItem>
|
||||
data={
|
||||
isResponseSuccess(closingData)
|
||||
? closingData.data?.stock_remaining
|
||||
@@ -231,11 +232,11 @@ const ProjectFlockClosingForm = ({
|
||||
columns={[
|
||||
{
|
||||
header: 'Product',
|
||||
accessorKey: 'product.name',
|
||||
accessorKey: 'product_name',
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
accessorKey: 'product.product_category.name',
|
||||
accessorKey: 'product_category',
|
||||
},
|
||||
{
|
||||
header: 'Quantity',
|
||||
@@ -243,7 +244,7 @@ const ProjectFlockClosingForm = ({
|
||||
},
|
||||
{
|
||||
header: 'UOM',
|
||||
accessorKey: 'product.uom.name',
|
||||
accessorKey: 'uom',
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
@@ -259,11 +260,11 @@ const ProjectFlockClosingForm = ({
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
{errorStock && (
|
||||
<div className='text-center text-error'>
|
||||
{/* {errorStock && (
|
||||
<div className='text-center text-error text-sm'>
|
||||
*Masih ada sisa stock yang belum dihabiskan.
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<div className='p-4 mt-6'>
|
||||
|
||||
@@ -23,7 +23,12 @@ import toast from 'react-hot-toast';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import {
|
||||
PROJECT_FLOCK_APPROVAL_LINE,
|
||||
PROJECT_FLOCK_KANDANGS_APPROVAL_LINE,
|
||||
} from '@/config/approval-line';
|
||||
import useSWR from 'swr';
|
||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||
|
||||
const ProjectFlockDetail = ({
|
||||
projectFlock,
|
||||
@@ -38,10 +43,23 @@ const ProjectFlockDetail = ({
|
||||
null
|
||||
);
|
||||
|
||||
const selectedKandang = projectFlock.kandangs.find(
|
||||
const selectedKandang = projectFlock.kandangs?.find(
|
||||
(kandang) => kandang.id === Number(selectedKandangId)
|
||||
);
|
||||
|
||||
const { data: projectFlockKandang, isLoading: projectFlockKandangLoading } =
|
||||
useSWR(
|
||||
selectedKandangId
|
||||
? `${ProjectFlockKandangApi.basePath}/get-detail/${selectedKandangId}`
|
||||
: null,
|
||||
selectedKandangId
|
||||
? () =>
|
||||
ProjectFlockKandangApi.getSingle(
|
||||
Number(selectedKandang?.project_flock_kandang_id)
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
isLoading: approvalsLoading,
|
||||
@@ -53,6 +71,20 @@ const ProjectFlockDetail = ({
|
||||
moduleId: projectFlock?.id.toString() ?? '',
|
||||
});
|
||||
|
||||
const { approvals: kandangApprovals, isLoading: kandangApprovalsLoading } =
|
||||
useApprovalSteps({
|
||||
latestApproval:
|
||||
selectedKandangId && isResponseSuccess(projectFlockKandang)
|
||||
? projectFlockKandang?.data?.approval
|
||||
: undefined,
|
||||
approvalLines: PROJECT_FLOCK_KANDANGS_APPROVAL_LINE,
|
||||
moduleName: 'PROJECT_FLOCK_KANDANGS',
|
||||
moduleId:
|
||||
selectedKandangId && isResponseSuccess(projectFlockKandang)
|
||||
? projectFlockKandang?.data?.id?.toString()
|
||||
: '',
|
||||
});
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
const deleteProjectFlockRes = await ProjectFlockApi.delete(
|
||||
@@ -151,7 +183,7 @@ const ProjectFlockDetail = ({
|
||||
className={{ badge: 'rounded-lg px-2' }}
|
||||
>
|
||||
<Icon icon='mdi:bookmark' width={12} height={12} />
|
||||
{` ${formatTitleCase(projectFlock.category)}`}
|
||||
{` ${formatTitleCase(projectFlock.category ?? '')}`}
|
||||
</Badge>
|
||||
</div>
|
||||
{/* Information Grid */}
|
||||
@@ -168,7 +200,7 @@ const ProjectFlockDetail = ({
|
||||
}}
|
||||
>
|
||||
<Icon icon='mdi:account-circle' width={14} height={14} />{' '}
|
||||
{projectFlock.created_user.name}
|
||||
{projectFlock.created_user?.name}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +226,7 @@ const ProjectFlockDetail = ({
|
||||
>
|
||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
|
||||
</div>
|
||||
<div className='col-span-2'>{projectFlock.area.name}</div>
|
||||
<div className='col-span-2'>{projectFlock?.area?.name}</div>
|
||||
|
||||
{/* BARIS 2 */}
|
||||
<div
|
||||
@@ -204,7 +236,7 @@ const ProjectFlockDetail = ({
|
||||
>
|
||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
|
||||
</div>
|
||||
<div className='col-span-2'>{projectFlock.location.name}</div>
|
||||
<div className='col-span-2'>{projectFlock?.location?.name}</div>
|
||||
|
||||
<div
|
||||
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
|
||||
@@ -213,7 +245,7 @@ const ProjectFlockDetail = ({
|
||||
>
|
||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
|
||||
</div>
|
||||
<div className='col-span-2'>{projectFlock.fcr.name}</div>
|
||||
<div className='col-span-2'>{projectFlock?.fcr?.name}</div>
|
||||
|
||||
{/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
|
||||
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
||||
@@ -221,7 +253,7 @@ const ProjectFlockDetail = ({
|
||||
Kategori
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
{formatTitleCase(projectFlock.category)}
|
||||
{formatTitleCase(projectFlock.category ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,6 +263,9 @@ 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'>Kandang Aktif</h2>
|
||||
{kandangApprovals && !kandangApprovalsLoading && (
|
||||
<ApprovalSteps approvals={kandangApprovals} />
|
||||
)}
|
||||
{/* Badge Row */}
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Badge
|
||||
@@ -246,7 +281,7 @@ const ProjectFlockDetail = ({
|
||||
height={12}
|
||||
color={'success'}
|
||||
/>{' '}
|
||||
Kandang Aktif ({projectFlock.kandangs.length})
|
||||
Kandang Aktif ({projectFlock.kandangs?.length})
|
||||
</Badge>
|
||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||
<Badge
|
||||
@@ -289,7 +324,7 @@ const ProjectFlockDetail = ({
|
||||
<span>Jenis Produk</span>
|
||||
</div>
|
||||
<div className='text-end text-gray-500'>
|
||||
{budget.nonstock?.name}
|
||||
{budget?.nonstock?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row justify-between items-center'>
|
||||
@@ -298,7 +333,7 @@ const ProjectFlockDetail = ({
|
||||
<span>Nama Satuan</span>
|
||||
</div>
|
||||
<div className='text-end text-gray-500'>
|
||||
{budget.nonstock?.uom.name}
|
||||
{budget?.nonstock?.uom?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row justify-between items-center'>
|
||||
@@ -353,21 +388,21 @@ const ProjectFlockDetail = ({
|
||||
value={selectedKandangId?.toString()}
|
||||
size='md'
|
||||
color='neutral'
|
||||
disabled={projectFlock.approval.step_number == 1}
|
||||
disabled={projectFlock?.approval?.step_number == 1}
|
||||
>
|
||||
{projectFlock.kandangs.map((kandang) => (
|
||||
{projectFlock.kandangs?.map((kandang) => (
|
||||
<div
|
||||
key={kandang.id}
|
||||
className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`}
|
||||
onClick={() =>
|
||||
projectFlock.approval.step_number > 1 &&
|
||||
setSelectedKamdangId(kandang.id.toString())
|
||||
projectFlock?.approval?.step_number > 1 &&
|
||||
setSelectedKamdangId(kandang?.id?.toString())
|
||||
}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={kandang.id.toString()}
|
||||
label={kandang.name}
|
||||
disabled={projectFlock.approval.step_number == 1}
|
||||
value={kandang?.id?.toString()}
|
||||
label={kandang?.name}
|
||||
disabled={projectFlock?.approval?.step_number == 1}
|
||||
/>
|
||||
<div className='text-end'>
|
||||
<Badge
|
||||
@@ -375,7 +410,7 @@ const ProjectFlockDetail = ({
|
||||
badge: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
Kapasitas {kandang.capacity} Ekor
|
||||
Kapasitas {kandang?.capacity} Ekor
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -392,14 +427,15 @@ const ProjectFlockDetail = ({
|
||||
variant='outline'
|
||||
color='success'
|
||||
disabled={
|
||||
!selectedKandangId || projectFlock.approval.step_number == 1
|
||||
!selectedKandangId ||
|
||||
projectFlock?.approval?.step_number == 1
|
||||
}
|
||||
>
|
||||
Chickin <Icon icon='mdi:checkbox-marked-outline' />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandangId}`}
|
||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
||||
className='m-0 p-0'
|
||||
>
|
||||
<Button
|
||||
@@ -407,7 +443,8 @@ const ProjectFlockDetail = ({
|
||||
variant='outline'
|
||||
color='error'
|
||||
disabled={
|
||||
!selectedKandangId || projectFlock.approval.step_number == 1
|
||||
!selectedKandangId ||
|
||||
projectFlock?.approval?.step_number == 1
|
||||
}
|
||||
>
|
||||
Close <Icon icon='mdi:checkbox-marked-circle-outline' />
|
||||
|
||||
+1
-3
@@ -226,9 +226,7 @@ export const getFilledTransferToLayingFormInitialValues = async (
|
||||
// targetKandang.target_project_flock_kandang.kandang.capacity,
|
||||
|
||||
// TODO: integrate this to real API kandang capacity
|
||||
maxQuantity:
|
||||
targetKandang.target_project_flock_kandang.kandang.capacity ??
|
||||
Infinity,
|
||||
maxQuantity: Infinity,
|
||||
}))
|
||||
: [],
|
||||
|
||||
|
||||
@@ -9,9 +9,28 @@ export const PROJECT_FLOCK_APPROVAL_LINE: ApprovalLine = [
|
||||
step_number: 2,
|
||||
step_name: 'Aktif',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Selesai',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PROJECT_FLOCK_KANDANG_APPROVAL_LINE: ApprovalLine = [
|
||||
export const PROJECT_FLOCK_KANDANGS_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan',
|
||||
},
|
||||
{
|
||||
step_number: 2,
|
||||
step_name: 'Disetujui',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Selesai',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const CHICKINS_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+132
-6
@@ -6,9 +6,20 @@ import {
|
||||
ClosingGeneralInformation,
|
||||
ClosingIncomingSapronak,
|
||||
ClosingOutgoingSapronak,
|
||||
ClosingOverhead,
|
||||
ClosingSapronakCalculation,
|
||||
} from '@/types/api/closing';
|
||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
dummyGetAllFetcher,
|
||||
dummyGetSingle,
|
||||
dummyGetAllIncomingSapronakFetcher,
|
||||
dummyGetAllOutgoingSapronakFetcher,
|
||||
dummyGetGeneralInfo,
|
||||
dummyGetPerhitunganSapronak,
|
||||
dummyGetOverhead,
|
||||
} from '@/dummy/closing.dummy';
|
||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
import { ClosingSales } from '@/types/api/closing';
|
||||
|
||||
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
@@ -16,6 +27,38 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
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 undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getPenjualan(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<ClosingSales> | undefined> {
|
||||
@@ -36,6 +79,10 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
async getAllIncomingSapronakFetcher(
|
||||
endpoint: string
|
||||
): 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[]>>(
|
||||
endpoint
|
||||
);
|
||||
@@ -44,19 +91,37 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
async getAllOutgoingSapronakFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
|
||||
return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>(
|
||||
endpoint
|
||||
);
|
||||
// TODO: Remove this block when backend is ready
|
||||
return await dummyGetAllOutgoingSapronakFetcher();
|
||||
|
||||
// Uncomment this when backend is ready
|
||||
// return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>(
|
||||
// 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 {
|
||||
const getGeneralInfoPath = `${this.basePath}/${id}`;
|
||||
const getGeneralInfoRes =
|
||||
await httpClient<BaseApiResponse<ClosingGeneralInformation>>(
|
||||
getGeneralInfoPath
|
||||
);
|
||||
|
||||
return getGeneralInfoRes;
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -67,6 +132,67 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getPerhitunganSapronak(
|
||||
id: number
|
||||
): 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 {
|
||||
const path = `${this.basePath}/${id}/perhitungan_sapronak`;
|
||||
return await httpClient<BaseApiResponse<ClosingSapronakCalculation>>(
|
||||
path,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
axios.isAxiosError<BaseApiResponse<ClosingSapronakCalculation>>(error)
|
||||
) {
|
||||
return error.response?.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getOverhead(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<ClosingOverhead> | undefined> {
|
||||
// TODO: Remove this block when backend is ready
|
||||
// try {
|
||||
// return await dummyGetOverhead(id);
|
||||
// } catch (error) {
|
||||
// if (axios.isAxiosError<BaseApiResponse<ClosingOverhead>>(error)) {
|
||||
// return error.response?.data;
|
||||
// }
|
||||
// return undefined;
|
||||
// }
|
||||
|
||||
// Uncomment this when backend is ready
|
||||
try {
|
||||
const path = `${this.basePath}/${id}/overhead`;
|
||||
return await httpClient<BaseApiResponse<ClosingOverhead>>(path, {
|
||||
method: 'GET',
|
||||
});
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError<BaseApiResponse<ClosingOverhead>>(error)) {
|
||||
return error.response?.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ClosingApi = new ClosingApiService('/closings');
|
||||
|
||||
@@ -54,120 +54,119 @@ export class ProjectFlockKandangService extends BaseApiService<
|
||||
id: number
|
||||
): Promise<BaseApiResponse<CheckClosingResponse> | undefined> {
|
||||
// Dummy data - replace with actual API call when backend is ready
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Cek persyaratan closing kandang',
|
||||
data: {
|
||||
unfinished_expenses: 2,
|
||||
stock_remaining: [
|
||||
{
|
||||
id: 1,
|
||||
product_id: 1,
|
||||
warehouse_id: 1,
|
||||
quantity: 0,
|
||||
product: {
|
||||
id: 1,
|
||||
name: 'Pakan Starter',
|
||||
brand: 'Brand A',
|
||||
sku: 'PKN-STR-001',
|
||||
product_price: 15000,
|
||||
selling_price: 17000,
|
||||
tax: 0,
|
||||
expiry_period: 365,
|
||||
flags: ['active'],
|
||||
uom: {
|
||||
id: 1,
|
||||
name: 'Kg',
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
product_category: {
|
||||
id: 1,
|
||||
name: 'Pakan',
|
||||
code: 'PKN',
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
suppliers: [],
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
warehouse: {
|
||||
id: 1,
|
||||
name: 'Gudang Utama',
|
||||
type: 'AREA',
|
||||
area: {
|
||||
id: 1,
|
||||
name: 'Area 1',
|
||||
},
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 1,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2025-01-01',
|
||||
updated_at: '2025-01-01',
|
||||
},
|
||||
],
|
||||
expenses: [
|
||||
{
|
||||
id: 1,
|
||||
po_number: 'PO-BOP-LTI-00001',
|
||||
category: 'NON-BOP',
|
||||
total: 110000,
|
||||
status: 'SELESAI',
|
||||
step_name: 'Approval Finance',
|
||||
step: 5,
|
||||
reference_number: 'BOP-LTI-00001',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
po_number: 'PO-BOP-LTI-00003',
|
||||
category: 'BOP',
|
||||
total: 110000,
|
||||
status: 'SELESAI',
|
||||
step_name: 'Approval Finance',
|
||||
step: 5,
|
||||
reference_number: 'BOP-LTI-00003',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}, 500); // Simulate network delay
|
||||
});
|
||||
// return new Promise((resolve) => {
|
||||
// setTimeout(() => {
|
||||
// resolve({
|
||||
// code: 200,
|
||||
// status: 'success',
|
||||
// message: 'Cek persyaratan closing kandang',
|
||||
// data: {
|
||||
// unfinished_expenses: id % 2 === 1 ? 2 : 0,
|
||||
// stock_remaining: [
|
||||
// {
|
||||
// id: 1,
|
||||
// product_id: 1,
|
||||
// warehouse_id: 1,
|
||||
// quantity: id % 2 === 1 ? 100 : 0,
|
||||
// product: {
|
||||
// id: 1,
|
||||
// name: 'Pakan Starter',
|
||||
// brand: 'Brand A',
|
||||
// sku: 'PKN-STR-001',
|
||||
// product_price: 15000,
|
||||
// selling_price: 17000,
|
||||
// tax: 0,
|
||||
// expiry_period: 365,
|
||||
// flags: ['active'],
|
||||
// uom: {
|
||||
// id: 1,
|
||||
// name: 'Kg',
|
||||
// created_user: {
|
||||
// id: 1,
|
||||
// id_user: 1,
|
||||
// email: 'admin@example.com',
|
||||
// name: 'Admin User',
|
||||
// },
|
||||
// created_at: '2024-01-01',
|
||||
// updated_at: '2024-01-01',
|
||||
// },
|
||||
// product_category: {
|
||||
// id: 1,
|
||||
// name: 'Pakan',
|
||||
// code: 'PKN',
|
||||
// created_user: {
|
||||
// id: 1,
|
||||
// id_user: 1,
|
||||
// email: 'admin@example.com',
|
||||
// name: 'Admin User',
|
||||
// },
|
||||
// created_at: '2024-01-01',
|
||||
// updated_at: '2024-01-01',
|
||||
// },
|
||||
// suppliers: [],
|
||||
// created_user: {
|
||||
// id: 1,
|
||||
// id_user: 1,
|
||||
// email: 'admin@example.com',
|
||||
// name: 'Admin User',
|
||||
// },
|
||||
// created_at: '2024-01-01',
|
||||
// updated_at: '2024-01-01',
|
||||
// },
|
||||
// warehouse: {
|
||||
// id: 1,
|
||||
// name: 'Gudang Utama',
|
||||
// type: 'AREA',
|
||||
// area: {
|
||||
// id: 1,
|
||||
// name: 'Area 1',
|
||||
// },
|
||||
// created_user: {
|
||||
// id: 1,
|
||||
// id_user: 1,
|
||||
// email: 'admin@example.com',
|
||||
// name: 'Admin User',
|
||||
// },
|
||||
// created_at: '2024-01-01',
|
||||
// updated_at: '2024-01-01',
|
||||
// },
|
||||
// created_user: {
|
||||
// id: 1,
|
||||
// id_user: 1,
|
||||
// email: 'admin@example.com',
|
||||
// name: 'Admin User',
|
||||
// },
|
||||
// created_at: '2025-01-01',
|
||||
// updated_at: '2025-01-01',
|
||||
// },
|
||||
// ],
|
||||
// expenses: [
|
||||
// {
|
||||
// id: 1,
|
||||
// po_number: 'PO-BOP-LTI-00001',
|
||||
// category: 'NON-BOP',
|
||||
// total: 110000,
|
||||
// status: id % 2 === 1 ? 'PENGAJUAN' : 'SELESAI',
|
||||
// step_name: id % 2 === 1 ? 'Approval Finance' : 'Selesai',
|
||||
// step: id % 2 === 1 ? 1 : 5,
|
||||
// reference_number: 'BOP-LTI-00001',
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// po_number: 'PO-BOP-LTI-00003',
|
||||
// category: 'BOP',
|
||||
// total: 110000,
|
||||
// status: id % 2 === 1 ? 'PENGAJUAN' : 'SELESAI',
|
||||
// step_name: id % 2 === 1 ? 'Approval Finance' : 'Selesai',
|
||||
// step: id % 2 === 1 ? 1 : 5,
|
||||
// reference_number: 'BOP-LTI-00003',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// });
|
||||
// }, 500); // Simulate network delay
|
||||
// });
|
||||
|
||||
/*
|
||||
// Original API call - uncomment when backend is ready
|
||||
try {
|
||||
const path = `${this.basePath}/${id}/closing/check`;
|
||||
@@ -181,7 +180,6 @@ export class ProjectFlockKandangService extends BaseApiService<
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+63
@@ -78,4 +78,67 @@ export type ClosingIncomingSapronak = {
|
||||
};
|
||||
|
||||
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
||||
|
||||
// ====== PERHITUNGAN SAPRONAK ======
|
||||
|
||||
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 ClosingSapronakCalculationItem = {
|
||||
rows: RowSapronakCalculation[];
|
||||
total: TotalSapronakCalculation;
|
||||
};
|
||||
|
||||
export type ClosingSapronakCalculation = {
|
||||
doc_broiler: ClosingSapronakCalculationItem;
|
||||
ovk: ClosingSapronakCalculationItem;
|
||||
pakan: ClosingSapronakCalculationItem;
|
||||
};
|
||||
|
||||
// ====== OVERHEAD ======
|
||||
export type ClosingOverhead = {
|
||||
overheads: Overhead[];
|
||||
total: OverheadTotal;
|
||||
};
|
||||
|
||||
export type Overhead = {
|
||||
item_name: string;
|
||||
uom_name: string;
|
||||
budget_quantity: number;
|
||||
budget_unit_price: number;
|
||||
budget_total_amount: number;
|
||||
actual_date: string;
|
||||
actual_quantity: number;
|
||||
actual_unit_price: number;
|
||||
actual_total_amount: number;
|
||||
cost_per_bird: number;
|
||||
};
|
||||
|
||||
export type OverheadTotal = {
|
||||
budget_quantity: number;
|
||||
budget_total_amount: number;
|
||||
actual_quantity: number;
|
||||
actual_total_amount: number;
|
||||
cost_per_bird: number;
|
||||
};
|
||||
export type ClosingSales = BaseMetadata & BaseClosingSales;
|
||||
|
||||
+19
-1
@@ -56,8 +56,26 @@ export type ClosingExpense = {
|
||||
reference_number: string;
|
||||
};
|
||||
|
||||
// "flag_name": "PAKAN",
|
||||
// "product_warehouse_id": 14,
|
||||
// "product_id": 8,
|
||||
// "product_name": "281 SPECIAL STARTER",
|
||||
// "product_category": "Bahan Baku",
|
||||
// "uom": "Kilogram",
|
||||
// "quantity": 1100
|
||||
|
||||
export type StockItem = {
|
||||
flag_name: string;
|
||||
product_warehouse_id: number;
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
product_category: string;
|
||||
uom: string;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type CheckClosingResponse = {
|
||||
unfinished_expenses: number;
|
||||
stock_remaining: ProductWarehouse[];
|
||||
stock_remaining: StockItem[];
|
||||
expenses: ClosingExpense[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user