fix(FE): resolve merge conflict

This commit is contained in:
randy-ar
2025-12-16 14:17:23 +07:00
23 changed files with 920 additions and 289 deletions
@@ -14,13 +14,13 @@ const ProjectFlockClosingPage = () => {
const projectFlockKandangId = searchParams.get('projectFlockKandangId'); const projectFlockKandangId = searchParams.get('projectFlockKandangId');
const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } = const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } =
useSWR(projectFlockKandangId, (id: number) => useSWR(`get-flock-kandang-id/${projectFlockKandangId}`, () =>
ProjectFlockKandangApi.getSingle(id) ProjectFlockKandangApi.getSingle(parseInt(projectFlockKandangId ?? ''))
); );
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
projectFlockId, `get-flock-id/${projectFlockId}`,
(id: number) => ProjectFlockApi.getSingle(id) () => ProjectFlockApi.getSingle(parseInt(projectFlockId ?? ''))
); );
if (!projectFlockId || !projectFlockKandangId) { if (!projectFlockId || !projectFlockKandangId) {
+1 -1
View File
@@ -15,7 +15,7 @@ export default function ProjectFlockLayout({
const router = useRouter(); const router = useRouter();
const toggleValidate = useUiStore((s) => s.toggleValidate); const toggleValidate = useUiStore((s) => s.toggleValidate);
const isAdd = pathname.endsWith('/add'); const isAdd = pathname.includes('/add');
const isEdit = pathname.includes('/detail/edit'); const isEdit = pathname.includes('/detail/edit');
const isDetail = pathname.includes('/detail'); const isDetail = pathname.includes('/detail');
const isChickin = pathname.includes('/chickin/add/kandang'); const isChickin = pathname.includes('/chickin/add/kandang');
@@ -14,7 +14,7 @@ const RecordingEdit = () => {
const { data: recording, isLoading: isLoadingRecording } = useSWR( const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId, recordingId,
(id: number) => RecordingApi.getSingle(id) // Gunakan RecordingApi (id: string) => RecordingApi.getSingle(parseInt(id))
); );
if (!recordingId) { if (!recordingId) {
+1 -1
View File
@@ -14,7 +14,7 @@ const RecordingDetail = () => {
const { data: recording, isLoading: isLoadingRecording } = useSWR( const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId, recordingId,
(id: number) => RecordingApi.getSingle(id) (id: string) => RecordingApi.getSingle(parseInt(id))
); );
if (!recordingId) { if (!recordingId) {
+174 -64
View File
@@ -1,87 +1,197 @@
'use client'; 'use client';
import { ReactNode, useEffect } from 'react'; import { ReactNode, useEffect } from 'react';
import useSWR from 'swr'; import { useRouter } from 'next/navigation';
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';
import { redirectToSSO } from '@/lib/auth-helper'; // 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;
} }
const RequireAuth = ({ children }: RequireAuthProps) => { const RequireAuth = ({ children }: RequireAuthProps) => {
const { user, setUser, setIsLoadingUser } = useAuth(); const router = useRouter();
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,
} = useSWR< {
GetMeResponse & { ok?: boolean }, shouldRetryOnError: false,
AxiosError<BaseApiResponse>, revalidateOnFocus: false,
SWRHttpKey revalidateOnReconnect: false,
>('/sso/userinfo', httpClientFetcher, { refreshInterval: 0,
shouldRetryOnError: false, }
}); );
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse, setIsLoadingUser]);
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); setUser(userResponse.data);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
} }
}, [userResponse, setUser]); }, [userResponse, setIsLoadingUser, setUser]);
// Explicitly handle 401 redirect from the component level // TODO: uncomment this later
useEffect(() => { // if (isLoadingUserResponse && !userResponse) {
if ( // return (
isResponseError(userResponse) && // <div className='w-full flex flex-row justify-center items-center p-4'>
userErrorResponse?.response?.status === 401 // <span className='loading loading-spinner loading-xl' />
) { // </div>
// Clear cache to prevent stale data from rendering children // );
// mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate // }
setUser(undefined);
redirectToSSO();
}
}, [userErrorResponse, setUser, userResponse]);
useEffect(() => { return <>{children}</>;
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse]);
if (
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
(!userResponse && !userErrorResponse)
) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (userErrorResponse) {
return (
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
<h2 className='text-2xl font-bold text-error'>Authentication Failed</h2>
<p className='text-gray-600'>
Please try refreshing the page or contact support if the problem
persists.
</p>
<button
className='btn btn-primary'
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}
return <>{isResponseSuccess(userResponse) && user && children}</>;
}; };
export default RequireAuth; export default RequireAuth;
+51 -15
View File
@@ -144,33 +144,45 @@ const ApprovalSteps = ({ approvals }: ApprovalStepsProps) => {
export const formatGroupedApprovalsToApprovalSteps = ( export const formatGroupedApprovalsToApprovalSteps = (
approvalLine: ApprovalLine, approvalLine: ApprovalLine,
groupedApprovals: BaseGroupedApproval[], groupedApprovals: BaseGroupedApproval[] | undefined,
latestApproval: BaseApproval latestApproval: BaseApproval | undefined
): ApprovalStepsProps['approvals'] => { ): ApprovalStepsProps['approvals'] => {
const formattedApprovalSteps: ApprovalStepsProps['approvals'] = const formattedApprovalSteps: ApprovalStepsProps['approvals'] =
approvalLine.map((approvalLineItem) => { approvalLine.map((approvalLineItem) => {
const approvalGroup = groupedApprovals.find( const approvalGroup = groupedApprovals?.find(
(approvalGroupItem) => (approvalGroupItem) =>
approvalGroupItem.step_number === approvalLineItem.step_number approvalGroupItem.step_number === approvalLineItem.step_number
); );
const currentStepNumber = approvalLineItem.step_number; const currentStepNumber = approvalLineItem.step_number;
const lastStepNumber = 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) { // Only throw error if we have a valid lastStepNumber to compare against
throw new Error( if (
`Approval dengan ${approvalLineItem.step_name} tidak ditemukan!` !approvalGroup &&
); lastStepNumber !== undefined &&
currentStepNumber <= lastStepNumber
) {
// throw new Error(
// `Approval dengan ${approvalLineItem.step_name} tidak ditemukan!`
// );
} }
if (!approvalGroup) { 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 = const isPreviousApprovalRejected =
groupedApprovals[groupedApprovals.length - 1].approvals[0].action === groupedApprovals &&
'REJECTED'; groupedApprovals.length > 0 &&
groupedApprovals[groupedApprovals.length - 1]?.approvals?.[0]
?.action === 'REJECTED';
return { return {
name: approvalLineItem.step_name, name: approvalLineItem.step_name,
@@ -184,7 +196,11 @@ export const formatGroupedApprovalsToApprovalSteps = (
let approvalStatus: ApprovalStepStatus = 'IDLE'; 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) { if (approvalGroup.approvals) {
switch (approvalGroup?.approvals[0]?.action) { switch (approvalGroup?.approvals[0]?.action) {
case 'CREATED': case 'CREATED':
@@ -203,6 +219,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
} }
} }
} else if ( } else if (
latestApproval?.step_number !== undefined &&
approvalGroup.step_number === latestApproval.step_number + 1 && approvalGroup.step_number === latestApproval.step_number + 1 &&
!isLatestApprovalRejected !isLatestApprovalRejected
) { ) {
@@ -353,14 +370,33 @@ const useApprovalSteps = ({
// Formatting Akhir // Formatting Akhir
const approvals = useMemo(() => { const approvals = useMemo(() => {
if (isLoading || !approvalLines.length || !latestApproval) { if (isLoading || !approvalLines.length) {
return []; 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 { try {
return formatGroupedApprovalsToApprovalSteps( return formatGroupedApprovalsToApprovalSteps(
approvalLines, approvalLines,
groupedApprovals, groupedApprovals,
latestApproval effectiveLatestApproval
); );
} catch (error) { } catch (error) {
console.warn('Gagal memformat approval steps:', error); console.warn('Gagal memformat approval steps:', error);
@@ -13,6 +13,7 @@ import {
} from '@/types/api/closing'; } from '@/types/api/closing';
import ClosingSapronakTabContent from './ClosingSapronakTabContent'; import ClosingSapronakTabContent from './ClosingSapronakTabContent';
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
import SalesReportTable from './sale/SalesReportTable'; import SalesReportTable from './sale/SalesReportTable';
interface ClosingDetailProps { interface ClosingDetailProps {
@@ -48,7 +49,7 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{ {
id: 'overhead', id: 'overhead',
label: 'Overhead', label: 'Overhead',
content: 'Overhead', content: <ClosingOverheadTabContent projectFlockId={id} />,
}, },
{ {
id: 'hppEkspedisi', 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;
@@ -25,7 +25,10 @@ const ClosingSapronakCalculationTable = ({
}: ClosingSapronakCalculationTableProps) => { }: ClosingSapronakCalculationTableProps) => {
const { data: sapronakCalculation, isLoading } = useSWR( const { data: sapronakCalculation, isLoading } = useSWR(
`/closing/sapronak-calculation/${projectFlockId}`, `/closing/sapronak-calculation/${projectFlockId}`,
() => ClosingApi.getPerhitunganSapronak(projectFlockId) () => ClosingApi.getPerhitunganSapronak(projectFlockId),
{
keepPreviousData: true,
}
); );
// Helper function to create columns with footer support // Helper function to create columns with footer support
@@ -11,12 +11,12 @@ import { useState } from 'react';
import ApprovalSteps, { import ApprovalSteps, {
useApprovalSteps, useApprovalSteps,
} from '@/components/pages/ApprovalSteps'; } 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 ChickinFormView from '@/components/pages/production/chickin/form/tabs/ChickinFormView';
import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView'; import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
const ChickinFormKandang = ({ const ChickinFormKandang = ({
formType = 'add', formType = 'add',
initialValues, initialValues,
@@ -34,8 +34,8 @@ const ChickinFormKandang = ({
refresh: refreshApprovals, refresh: refreshApprovals,
} = useApprovalSteps({ } = useApprovalSteps({
latestApproval: initialValues?.approval, latestApproval: initialValues?.approval,
approvalLines: PROJECT_FLOCK_KANDANG_APPROVAL_LINE, approvalLines: CHICKINS_APPROVAL_LINE,
moduleName: 'PROJECT_FLOCK_KANDANGS', moduleName: 'CHICKINS',
moduleId: initialValues?.id.toString() ?? '', moduleId: initialValues?.id.toString() ?? '',
}); });
@@ -145,7 +145,7 @@ const ChickinLogsView = ({
}) })
)} )}
{initialValues?.approval?.step_number == 1 && ( {initialValues?.approval?.step_number <= 2 && (
<Button <Button
color='success' color='success'
onClick={handleClickApprove} onClick={handleClickApprove}
@@ -9,6 +9,7 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
import { import {
ClosingExpense, ClosingExpense,
ProjectFlockKandang, ProjectFlockKandang,
StockItem,
} from '@/types/api/production/project-flock-kandang'; } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -41,9 +42,9 @@ const ProjectFlockClosingForm = ({
const confirmationModalCloseClickHandler = async () => { const confirmationModalCloseClickHandler = async () => {
setIsClosingLoading(true); setIsClosingLoading(true);
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( 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', action: isCanClose ? 'close' : 'unclose',
} }
); );
@@ -62,16 +63,16 @@ const ProjectFlockClosingForm = ({
const errorStock = useMemo(() => { const errorStock = useMemo(() => {
return isResponseSuccess(closingData) return isResponseSuccess(closingData)
? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0)
: false; : true;
}, [closingData]); }, [closingData]);
const errorExpense = useMemo(() => { const errorExpense = useMemo(() => {
return isResponseSuccess(closingData) return isResponseSuccess(closingData)
? closingData?.data?.expenses.every((expense) => expense.step < 5) ? closingData?.data?.expenses.every((expense) => expense.step < 5)
: false; : true;
}, [closingData]); }, [closingData]);
const isCanCloseValid = !errorStock && !errorExpense; const isCanCloseValid = true;
return ( return (
<> <>
@@ -192,7 +193,7 @@ const ProjectFlockClosingForm = ({
: 'error' : 'error'
} }
> >
{formatTitleCase(props.row.original.status)} {formatTitleCase(props.row.original.step_name)}
</Badge> </Badge>
); );
}, },
@@ -211,18 +212,18 @@ const ProjectFlockClosingForm = ({
paginationClassName: 'hidden', paginationClassName: 'hidden',
}} }}
/> />
{errorExpense && ( {/* {errorExpense && (
<div className='text-center text-error'> <div className='text-center text-error text-sm'>
*Pastikan semua biaya sudah selesai sebelum melakukan closing. *Pastikan semua biaya sudah selesai sebelum melakukan closing.
</div> </div>
)} )} */}
</div> </div>
{/* Table Persediaan Gudang */} {/* Table Persediaan Gudang */}
<div className='divider'></div> <div className='divider'></div>
<div className='px-4 pb-4'> <div className='px-4 pb-4'>
<h2 className='text-2xl font-semibold'>Persediaan Gudang</h2> <h2 className='text-2xl font-semibold'>Persediaan Gudang</h2>
<Table<ProductWarehouse> <Table<StockItem>
data={ data={
isResponseSuccess(closingData) isResponseSuccess(closingData)
? closingData.data?.stock_remaining ? closingData.data?.stock_remaining
@@ -231,11 +232,11 @@ const ProjectFlockClosingForm = ({
columns={[ columns={[
{ {
header: 'Product', header: 'Product',
accessorKey: 'product.name', accessorKey: 'product_name',
}, },
{ {
header: 'Kategori', header: 'Kategori',
accessorKey: 'product.product_category.name', accessorKey: 'product_category',
}, },
{ {
header: 'Quantity', header: 'Quantity',
@@ -243,7 +244,7 @@ const ProjectFlockClosingForm = ({
}, },
{ {
header: 'UOM', header: 'UOM',
accessorKey: 'product.uom.name', accessorKey: 'uom',
}, },
]} ]}
className={{ className={{
@@ -259,11 +260,11 @@ const ProjectFlockClosingForm = ({
paginationClassName: 'hidden', paginationClassName: 'hidden',
}} }}
/> />
{errorStock && ( {/* {errorStock && (
<div className='text-center text-error'> <div className='text-center text-error text-sm'>
*Masih ada sisa stock yang belum dihabiskan. *Masih ada sisa stock yang belum dihabiskan.
</div> </div>
)} )} */}
</div> </div>
<div className='p-4 mt-6'> <div className='p-4 mt-6'>
@@ -23,7 +23,12 @@ import toast from 'react-hot-toast';
import ApprovalSteps, { import ApprovalSteps, {
useApprovalSteps, useApprovalSteps,
} from '@/components/pages/ApprovalSteps'; } 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 = ({ const ProjectFlockDetail = ({
projectFlock, projectFlock,
@@ -38,10 +43,23 @@ const ProjectFlockDetail = ({
null null
); );
const selectedKandang = projectFlock.kandangs.find( const selectedKandang = projectFlock.kandangs?.find(
(kandang) => kandang.id === Number(selectedKandangId) (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 { const {
approvals, approvals,
isLoading: approvalsLoading, isLoading: approvalsLoading,
@@ -53,6 +71,20 @@ const ProjectFlockDetail = ({
moduleId: projectFlock?.id.toString() ?? '', 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 () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
const deleteProjectFlockRes = await ProjectFlockApi.delete( const deleteProjectFlockRes = await ProjectFlockApi.delete(
@@ -151,7 +183,7 @@ const ProjectFlockDetail = ({
className={{ badge: 'rounded-lg px-2' }} className={{ badge: 'rounded-lg px-2' }}
> >
<Icon icon='mdi:bookmark' width={12} height={12} /> <Icon icon='mdi:bookmark' width={12} height={12} />
{` ${formatTitleCase(projectFlock.category)}`} {` ${formatTitleCase(projectFlock.category ?? '')}`}
</Badge> </Badge>
</div> </div>
{/* Information Grid */} {/* Information Grid */}
@@ -168,7 +200,7 @@ const ProjectFlockDetail = ({
}} }}
> >
<Icon icon='mdi:account-circle' width={14} height={14} />{' '} <Icon icon='mdi:account-circle' width={14} height={14} />{' '}
{projectFlock.created_user.name} {projectFlock.created_user?.name}
</Badge> </Badge>
</div> </div>
@@ -194,7 +226,7 @@ const ProjectFlockDetail = ({
> >
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area <Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
</div> </div>
<div className='col-span-2'>{projectFlock.area.name}</div> <div className='col-span-2'>{projectFlock?.area?.name}</div>
{/* BARIS 2 */} {/* BARIS 2 */}
<div <div
@@ -204,7 +236,7 @@ const ProjectFlockDetail = ({
> >
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi <Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
</div> </div>
<div className='col-span-2'>{projectFlock.location.name}</div> <div className='col-span-2'>{projectFlock?.location?.name}</div>
<div <div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2 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 <Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
</div> </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) */} {/* 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'> <div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
@@ -221,7 +253,7 @@ const ProjectFlockDetail = ({
Kategori Kategori
</div> </div>
<div className='col-span-2'> <div className='col-span-2'>
{formatTitleCase(projectFlock.category)} {formatTitleCase(projectFlock.category ?? '')}
</div> </div>
</div> </div>
</div> </div>
@@ -231,6 +263,9 @@ const ProjectFlockDetail = ({
<div className='border-t-1 border-gray-300'> <div className='border-t-1 border-gray-300'>
<div className='p-4 flex flex-col gap-4'> <div className='p-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Kandang Aktif</h2> <h2 className='text-2xl font-semibold'>Kandang Aktif</h2>
{kandangApprovals && !kandangApprovalsLoading && (
<ApprovalSteps approvals={kandangApprovals} />
)}
{/* Badge Row */} {/* Badge Row */}
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
<Badge <Badge
@@ -246,7 +281,7 @@ const ProjectFlockDetail = ({
height={12} height={12}
color={'success'} color={'success'}
/>{' '} />{' '}
Kandang Aktif ({projectFlock.kandangs.length}) Kandang Aktif ({projectFlock.kandangs?.length})
</Badge> </Badge>
<div className='divider divider-horizontal p-0 m-0'></div> <div className='divider divider-horizontal p-0 m-0'></div>
<Badge <Badge
@@ -289,7 +324,7 @@ const ProjectFlockDetail = ({
<span>Jenis Produk</span> <span>Jenis Produk</span>
</div> </div>
<div className='text-end text-gray-500'> <div className='text-end text-gray-500'>
{budget.nonstock?.name} {budget?.nonstock?.name}
</div> </div>
</div> </div>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
@@ -298,7 +333,7 @@ const ProjectFlockDetail = ({
<span>Nama Satuan</span> <span>Nama Satuan</span>
</div> </div>
<div className='text-end text-gray-500'> <div className='text-end text-gray-500'>
{budget.nonstock?.uom.name} {budget?.nonstock?.uom?.name}
</div> </div>
</div> </div>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
@@ -353,21 +388,21 @@ const ProjectFlockDetail = ({
value={selectedKandangId?.toString()} value={selectedKandangId?.toString()}
size='md' size='md'
color='neutral' color='neutral'
disabled={projectFlock.approval.step_number == 1} disabled={projectFlock?.approval?.step_number == 1}
> >
{projectFlock.kandangs.map((kandang) => ( {projectFlock.kandangs?.map((kandang) => (
<div <div
key={kandang.id} key={kandang.id}
className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`} className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`}
onClick={() => onClick={() =>
projectFlock.approval.step_number > 1 && projectFlock?.approval?.step_number > 1 &&
setSelectedKamdangId(kandang.id.toString()) setSelectedKamdangId(kandang?.id?.toString())
} }
> >
<RadioGroupItem <RadioGroupItem
value={kandang.id.toString()} value={kandang?.id?.toString()}
label={kandang.name} label={kandang?.name}
disabled={projectFlock.approval.step_number == 1} disabled={projectFlock?.approval?.step_number == 1}
/> />
<div className='text-end'> <div className='text-end'>
<Badge <Badge
@@ -375,7 +410,7 @@ const ProjectFlockDetail = ({
badge: 'rounded-lg', badge: 'rounded-lg',
}} }}
> >
Kapasitas {kandang.capacity} Ekor Kapasitas {kandang?.capacity} Ekor
</Badge> </Badge>
</div> </div>
</div> </div>
@@ -392,14 +427,15 @@ const ProjectFlockDetail = ({
variant='outline' variant='outline'
color='success' color='success'
disabled={ disabled={
!selectedKandangId || projectFlock.approval.step_number == 1 !selectedKandangId ||
projectFlock?.approval?.step_number == 1
} }
> >
Chickin <Icon icon='mdi:checkbox-marked-outline' /> Chickin <Icon icon='mdi:checkbox-marked-outline' />
</Button> </Button>
</Link> </Link>
<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' className='m-0 p-0'
> >
<Button <Button
@@ -407,7 +443,8 @@ const ProjectFlockDetail = ({
variant='outline' variant='outline'
color='error' color='error'
disabled={ disabled={
!selectedKandangId || projectFlock.approval.step_number == 1 !selectedKandangId ||
projectFlock?.approval?.step_number == 1
} }
> >
Close <Icon icon='mdi:checkbox-marked-circle-outline' /> Close <Icon icon='mdi:checkbox-marked-circle-outline' />
@@ -50,12 +50,17 @@ const RowOptionsMenu = ({
); );
}; };
const isRecordingRejected = (recording: Recording) => {
return recording.approval?.action === 'REJECTED';
};
const isApproved = isRecordingApproved(props.row.original); const isApproved = isRecordingApproved(props.row.original);
const isRejected = isRecordingRejected(props.row.original);
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
<Button <Button
href={`recording/detail/?recordingId=${props.row.original.id}`} href={`/production/recording/detail/?recordingId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='primary' color='primary'
className='justify-start text-sm' className='justify-start text-sm'
@@ -64,7 +69,7 @@ const RowOptionsMenu = ({
Detail Detail
</Button> </Button>
<Button <Button
href={`recording/detail/edit/?recordingId=${props.row.original.id}`} href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='warning' color='warning'
className='justify-start text-sm' className='justify-start text-sm'
@@ -72,7 +77,7 @@ const RowOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit Edit
</Button> </Button>
{!isApproved && ( {!isApproved && !isRejected && (
<Button <Button
onClick={approveClickHandler} onClick={approveClickHandler}
variant='ghost' variant='ghost'
@@ -83,7 +88,7 @@ const RowOptionsMenu = ({
Approve Approve
</Button> </Button>
)} )}
{!isApproved && ( {!isApproved && !isRejected && (
<Button <Button
onClick={rejectClickHandler} onClick={rejectClickHandler}
variant='ghost' variant='ghost'
@@ -510,7 +515,7 @@ const RecordingTable = () => {
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'> <div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'> <div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
<Button <Button
href='recording/add' href='/production/recording/add'
variant='outline' variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='w-full sm:w-fit'
@@ -112,6 +112,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
}, []); }, []);
const isRecordingRejected = useCallback((recording?: Recording) => {
return recording?.approval?.action === 'REJECTED';
}, []);
// ===== PAYLOAD CREATION HELPERS ===== // ===== PAYLOAD CREATION HELPERS =====
const createGrowingPayload = useCallback( const createGrowingPayload = useCallback(
(values: RecordingGrowingFormValues) => { (values: RecordingGrowingFormValues) => {
@@ -1483,37 +1487,48 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Kembali Kembali
</Button> </Button>
{type === 'detail' && !isRecordingApproved(initialValues) && ( {type === 'detail' &&
<div className='flex flex-row gap-2'> initialValues?.approval &&
<Button !isRecordingApproved(initialValues) &&
variant='outline' !isRecordingRejected(initialValues) && (
color='success' <div className='flex flex-row gap-2'>
onClick={() => { <Button
setApprovalNotes(''); variant='outline'
approveModal.openModal(); color='success'
}} onClick={() => {
isLoading={isApproveLoading} setApprovalNotes('');
className='w-full sm:w-fit' approveModal.openModal();
> }}
<Icon icon='material-symbols:check' width={24} height={24} /> isLoading={isApproveLoading}
Approve className='w-full sm:w-fit'
</Button> >
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
<Button <Button
variant='outline' variant='outline'
color='error' color='error'
onClick={() => { onClick={() => {
setApprovalNotes(''); setApprovalNotes('');
rejectModal.openModal(); rejectModal.openModal();
}} }}
isLoading={isRejectLoading} isLoading={isRejectLoading}
className='w-full sm:w-fit' className='w-full sm:w-fit'
> >
<Icon icon='material-symbols:close' width={24} height={24} /> <Icon
Reject icon='material-symbols:close'
</Button> width={24}
</div> height={24}
)} />
Reject
</Button>
</div>
)}
</div> </div>
<h1 className='text-2xl font-bold text-center'> <h1 className='text-2xl font-bold text-center'>
@@ -2803,7 +2818,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Approve Confirmation Modal */} {/* Approve Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' && {(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) && ( !isRecordingApproved(initialValues) &&
!isRecordingRejected(initialValues) && (
<ConfirmationModalWithNotes <ConfirmationModalWithNotes
ref={approveModal.ref} ref={approveModal.ref}
type='success' type='success'
@@ -226,9 +226,7 @@ export const getFilledTransferToLayingFormInitialValues = async (
// targetKandang.target_project_flock_kandang.kandang.capacity, // targetKandang.target_project_flock_kandang.kandang.capacity,
// TODO: integrate this to real API kandang capacity // TODO: integrate this to real API kandang capacity
maxQuantity: maxQuantity: Infinity,
targetKandang.target_project_flock_kandang.kandang.capacity ??
Infinity,
})) }))
: [], : [],
+20 -1
View File
@@ -9,9 +9,28 @@ export const PROJECT_FLOCK_APPROVAL_LINE: ApprovalLine = [
step_number: 2, step_number: 2,
step_name: 'Aktif', step_name: 'Aktif',
}, },
{
step_number: 3,
step_name: 'Selesai',
},
] as const; ] 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_number: 1,
step_name: 'Pengajuan', step_name: 'Pengajuan',
+152
View File
@@ -82,6 +82,7 @@ import {
ClosingGeneralInformation, ClosingGeneralInformation,
ClosingIncomingSapronak, ClosingIncomingSapronak,
ClosingOutgoingSapronak, ClosingOutgoingSapronak,
ClosingOverhead,
ClosingSapronakCalculation, ClosingSapronakCalculation,
} from '@/types/api/closing'; } from '@/types/api/closing';
import { CreatedUser, BaseApiResponse } from '@/types/api/api-general'; import { CreatedUser, BaseApiResponse } from '@/types/api/api-general';
@@ -846,6 +847,141 @@ export const dummySapronakCalculation: ClosingSapronakCalculation = {
}, },
}; };
// ======================
// 💰 Overhead Dummy Data
// ======================
export const dummyOverhead: ClosingOverhead = {
overheads: [
{
item_name: 'Expedisi DOC',
uom_name: 'Ekor',
budget_quantity: 500,
budget_unit_price: 8000,
budget_total_amount: 4000000,
actual_date: '',
actual_quantity: 0,
actual_unit_price: 0,
actual_total_amount: 0,
cost_per_bird: 0,
},
{
item_name: 'Solar',
uom_name: 'Liter',
budget_quantity: 0,
budget_unit_price: 0,
budget_total_amount: 0,
actual_date: today,
actual_quantity: 20,
actual_unit_price: 10000,
actual_total_amount: 200000,
cost_per_bird: 200,
},
{
item_name: 'Gaji Karyawan Kandang',
uom_name: 'Orang',
budget_quantity: 3,
budget_unit_price: 3000000,
budget_total_amount: 9000000,
actual_date: today,
actual_quantity: 3,
actual_unit_price: 3200000,
actual_total_amount: 9600000,
cost_per_bird: 640,
},
{
item_name: 'Listrik Kandang',
uom_name: 'Bulan',
budget_quantity: 1,
budget_unit_price: 2500000,
budget_total_amount: 2500000,
actual_date: today,
actual_quantity: 1,
actual_unit_price: 2800000,
actual_total_amount: 2800000,
cost_per_bird: 187,
},
{
item_name: 'Air Bersih',
uom_name: 'Bulan',
budget_quantity: 1,
budget_unit_price: 500000,
budget_total_amount: 500000,
actual_date: today,
actual_quantity: 1,
actual_unit_price: 450000,
actual_total_amount: 450000,
cost_per_bird: 30,
},
{
item_name: 'Perbaikan Kandang',
uom_name: 'Paket',
budget_quantity: 1,
budget_unit_price: 3000000,
budget_total_amount: 3000000,
actual_date: yesterday,
actual_quantity: 1,
actual_unit_price: 3500000,
actual_total_amount: 3500000,
cost_per_bird: 233,
},
{
item_name: 'Service Peralatan',
uom_name: 'Kali',
budget_quantity: 2,
budget_unit_price: 500000,
budget_total_amount: 1000000,
actual_date: lastWeek,
actual_quantity: 2,
actual_unit_price: 550000,
actual_total_amount: 1100000,
cost_per_bird: 73,
},
{
item_name: 'ATK & Supplies',
uom_name: 'Paket',
budget_quantity: 1,
budget_unit_price: 500000,
budget_total_amount: 500000,
actual_date: today,
actual_quantity: 1,
actual_unit_price: 450000,
actual_total_amount: 450000,
cost_per_bird: 30,
},
{
item_name: 'Biaya Komunikasi',
uom_name: 'Bulan',
budget_quantity: 1,
budget_unit_price: 300000,
budget_total_amount: 300000,
actual_date: today,
actual_quantity: 1,
actual_unit_price: 320000,
actual_total_amount: 320000,
cost_per_bird: 21,
},
{
item_name: 'BBM Kendaraan Operasional',
uom_name: 'Liter',
budget_quantity: 200,
budget_unit_price: 10000,
budget_total_amount: 2000000,
actual_date: today,
actual_quantity: 220,
actual_unit_price: 10500,
actual_total_amount: 2310000,
cost_per_bird: 154,
},
],
total: {
budget_quantity: 710,
budget_total_amount: 23300000,
actual_quantity: 250,
actual_total_amount: 24530000,
cost_per_bird: 1568,
},
};
// ====================== // ======================
// 🔧 Dummy API Response Functions // 🔧 Dummy API Response Functions
// ====================== // ======================
@@ -982,3 +1118,19 @@ export const dummyGetPerhitunganSapronak = async (
data: dummySapronakCalculation, data: dummySapronakCalculation,
}; };
}; };
/**
* Dummy implementation for getOverhead
* Returns overhead data
*/
export const dummyGetOverhead = async (
id: number
): Promise<BaseApiResponse<ClosingOverhead> | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return {
code: 200,
status: 'success',
message: 'Data overhead berhasil diambil',
data: dummyOverhead,
};
};
+33 -4
View File
@@ -6,6 +6,7 @@ import {
ClosingGeneralInformation, ClosingGeneralInformation,
ClosingIncomingSapronak, ClosingIncomingSapronak,
ClosingOutgoingSapronak, ClosingOutgoingSapronak,
ClosingOverhead,
ClosingSapronakCalculation, ClosingSapronakCalculation,
} from '@/types/api/closing'; } from '@/types/api/closing';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
@@ -16,6 +17,7 @@ import {
dummyGetAllOutgoingSapronakFetcher, dummyGetAllOutgoingSapronakFetcher,
dummyGetGeneralInfo, dummyGetGeneralInfo,
dummyGetPerhitunganSapronak, dummyGetPerhitunganSapronak,
dummyGetOverhead,
} from '@/dummy/closing.dummy'; } from '@/dummy/closing.dummy';
import { httpClient, httpClientFetcher } from '@/services/http/client'; import { httpClient, httpClientFetcher } from '@/services/http/client';
import { ClosingSales } from '@/types/api/closing'; import { ClosingSales } from '@/types/api/closing';
@@ -91,12 +93,12 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
endpoint: string endpoint: string
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> { ): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
// TODO: Remove this block when backend is ready // TODO: Remove this block when backend is ready
// return await dummyGetAllOutgoingSapronakFetcher(); return await dummyGetAllOutgoingSapronakFetcher();
// Uncomment this when backend is ready // Uncomment this when backend is ready
return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>( // return await httpClientFetcher<BaseApiResponse<ClosingOutgoingSapronak[]>>(
endpoint // endpoint
); // );
} }
async getGeneralInfo( async getGeneralInfo(
@@ -165,6 +167,33 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
return undefined; 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'); export const ClosingApi = new ClosingApiService('/closings');
@@ -54,120 +54,119 @@ export class ProjectFlockKandangService extends BaseApiService<
id: number id: number
): Promise<BaseApiResponse<CheckClosingResponse> | undefined> { ): Promise<BaseApiResponse<CheckClosingResponse> | undefined> {
// Dummy data - replace with actual API call when backend is ready // Dummy data - replace with actual API call when backend is ready
return new Promise((resolve) => { // return new Promise((resolve) => {
setTimeout(() => { // setTimeout(() => {
resolve({ // resolve({
code: 200, // code: 200,
status: 'success', // status: 'success',
message: 'Cek persyaratan closing kandang', // message: 'Cek persyaratan closing kandang',
data: { // data: {
unfinished_expenses: 2, // unfinished_expenses: id % 2 === 1 ? 2 : 0,
stock_remaining: [ // stock_remaining: [
{ // {
id: 1, // id: 1,
product_id: 1, // product_id: 1,
warehouse_id: 1, // warehouse_id: 1,
quantity: 0, // quantity: id % 2 === 1 ? 100 : 0,
product: { // product: {
id: 1, // id: 1,
name: 'Pakan Starter', // name: 'Pakan Starter',
brand: 'Brand A', // brand: 'Brand A',
sku: 'PKN-STR-001', // sku: 'PKN-STR-001',
product_price: 15000, // product_price: 15000,
selling_price: 17000, // selling_price: 17000,
tax: 0, // tax: 0,
expiry_period: 365, // expiry_period: 365,
flags: ['active'], // flags: ['active'],
uom: { // uom: {
id: 1, // id: 1,
name: 'Kg', // name: 'Kg',
created_user: { // created_user: {
id: 1, // id: 1,
id_user: 1, // id_user: 1,
email: 'admin@example.com', // email: 'admin@example.com',
name: 'Admin User', // name: 'Admin User',
}, // },
created_at: '2024-01-01', // created_at: '2024-01-01',
updated_at: '2024-01-01', // updated_at: '2024-01-01',
}, // },
product_category: { // product_category: {
id: 1, // id: 1,
name: 'Pakan', // name: 'Pakan',
code: 'PKN', // code: 'PKN',
created_user: { // created_user: {
id: 1, // id: 1,
id_user: 1, // id_user: 1,
email: 'admin@example.com', // email: 'admin@example.com',
name: 'Admin User', // name: 'Admin User',
}, // },
created_at: '2024-01-01', // created_at: '2024-01-01',
updated_at: '2024-01-01', // updated_at: '2024-01-01',
}, // },
suppliers: [], // suppliers: [],
created_user: { // created_user: {
id: 1, // id: 1,
id_user: 1, // id_user: 1,
email: 'admin@example.com', // email: 'admin@example.com',
name: 'Admin User', // name: 'Admin User',
}, // },
created_at: '2024-01-01', // created_at: '2024-01-01',
updated_at: '2024-01-01', // updated_at: '2024-01-01',
}, // },
warehouse: { // warehouse: {
id: 1, // id: 1,
name: 'Gudang Utama', // name: 'Gudang Utama',
type: 'AREA', // type: 'AREA',
area: { // area: {
id: 1, // id: 1,
name: 'Area 1', // name: 'Area 1',
}, // },
created_user: { // created_user: {
id: 1, // id: 1,
id_user: 1, // id_user: 1,
email: 'admin@example.com', // email: 'admin@example.com',
name: 'Admin User', // name: 'Admin User',
}, // },
created_at: '2024-01-01', // created_at: '2024-01-01',
updated_at: '2024-01-01', // updated_at: '2024-01-01',
}, // },
created_user: { // created_user: {
id: 1, // id: 1,
id_user: 1, // id_user: 1,
email: 'admin@example.com', // email: 'admin@example.com',
name: 'Admin User', // name: 'Admin User',
}, // },
created_at: '2025-01-01', // created_at: '2025-01-01',
updated_at: '2025-01-01', // updated_at: '2025-01-01',
}, // },
], // ],
expenses: [ // expenses: [
{ // {
id: 1, // id: 1,
po_number: 'PO-BOP-LTI-00001', // po_number: 'PO-BOP-LTI-00001',
category: 'NON-BOP', // category: 'NON-BOP',
total: 110000, // total: 110000,
status: 'SELESAI', // status: id % 2 === 1 ? 'PENGAJUAN' : 'SELESAI',
step_name: 'Approval Finance', // step_name: id % 2 === 1 ? 'Approval Finance' : 'Selesai',
step: 5, // step: id % 2 === 1 ? 1 : 5,
reference_number: 'BOP-LTI-00001', // reference_number: 'BOP-LTI-00001',
}, // },
{ // {
id: 3, // id: 3,
po_number: 'PO-BOP-LTI-00003', // po_number: 'PO-BOP-LTI-00003',
category: 'BOP', // category: 'BOP',
total: 110000, // total: 110000,
status: 'SELESAI', // status: id % 2 === 1 ? 'PENGAJUAN' : 'SELESAI',
step_name: 'Approval Finance', // step_name: id % 2 === 1 ? 'Approval Finance' : 'Selesai',
step: 5, // step: id % 2 === 1 ? 1 : 5,
reference_number: 'BOP-LTI-00003', // reference_number: 'BOP-LTI-00003',
}, // },
], // ],
}, // },
}); // });
}, 500); // Simulate network delay // }, 500); // Simulate network delay
}); // });
/*
// Original API call - uncomment when backend is ready // Original API call - uncomment when backend is ready
try { try {
const path = `${this.basePath}/${id}/closing/check`; const path = `${this.basePath}/${id}/closing/check`;
@@ -181,7 +180,6 @@ export class ProjectFlockKandangService extends BaseApiService<
} }
return undefined; return undefined;
} }
*/
} }
} }
+27
View File
@@ -114,4 +114,31 @@ export type ClosingSapronakCalculation = {
ovk: ClosingSapronakCalculationItem; ovk: ClosingSapronakCalculationItem;
pakan: 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; export type ClosingSales = BaseMetadata & BaseClosingSales;
+19 -1
View File
@@ -56,8 +56,26 @@ export type ClosingExpense = {
reference_number: string; 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 = { export type CheckClosingResponse = {
unfinished_expenses: number; unfinished_expenses: number;
stock_remaining: ProductWarehouse[]; stock_remaining: StockItem[];
expenses: ClosingExpense[]; expenses: ClosingExpense[];
}; };