fix(FE-279): adjust closing project flock kandang

This commit is contained in:
randy-ar
2025-12-09 10:33:38 +07:00
parent b97cc39854
commit 8a0adf847e
10 changed files with 398 additions and 190 deletions
@@ -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) {
+166 -33
View File
@@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
import { isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general';
// 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 {
children?: ReactNode;
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth();
const {
data: userResponse,
isLoading: isLoadingUserResponse,
error: userErrorResponse,
} = useSWRImmutable<
GetMeResponse & { ok?: boolean },
AxiosError<BaseApiResponse>,
SWRHttpKey
>('/sso/userinfo', httpClientFetcher, {
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
});
const { data: userResponse, isLoading: isLoadingUserResponse } =
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
'/auth/sso/userinfo',
httpClientFetcher,
{
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
}
);
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
useEffect(() => {
if (isResponseSuccess(userResponse)) {
setUser(userResponse.data);
} else if (
isResponseError(userErrorResponse?.response?.data) &&
typeof window !== 'undefined'
) {
router.replace(
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
}
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]);
}, [userResponse, setIsLoadingUser, setUser]);
if (isLoadingUserResponse && !userResponse && !userErrorResponse) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
// TODO: uncomment this later
// if (isLoadingUserResponse && !userResponse) {
// return (
// <div className='w-full flex flex-row justify-center items-center p-4'>
// <span className='loading loading-spinner loading-xl' />
// </div>
// );
// }
return <>{isResponseSuccess(userResponse) && children}</>;
return <>{children}</>;
};
export default RequireAuth;
+51 -15
View File
@@ -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);
@@ -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 >= 1 && (
<Button
color='success'
onClick={handleClickApprove}
@@ -43,7 +43,7 @@ const ProjectFlockClosingForm = ({
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
projectFlock?.id as number,
{
closed_date: formatDate(new Date(), 'yyyy-MM-dd'),
closed_date: formatDate(new Date(), 'YYYY-MM-DD'),
action: isCanClose ? 'close' : 'unclose',
}
);
@@ -212,7 +212,7 @@ const ProjectFlockClosingForm = ({
}}
/>
{errorExpense && (
<div className='text-center text-error'>
<div className='text-center text-error text-sm'>
*Pastikan semua biaya sudah selesai sebelum melakukan closing.
</div>
)}
@@ -260,7 +260,7 @@ const ProjectFlockClosingForm = ({
}}
/>
{errorStock && (
<div className='text-center text-error'>
<div className='text-center text-error text-sm'>
*Masih ada sisa stock yang belum dihabiskan.
</div>
)}
@@ -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,20 @@ 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(parseInt(selectedKandangId))
: null
);
const {
approvals,
isLoading: approvalsLoading,
@@ -53,6 +68,17 @@ 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 ?? '',
});
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteProjectFlockRes = await ProjectFlockApi.delete(
@@ -151,7 +177,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 */}
@@ -194,7 +220,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 +230,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 +239,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 +247,7 @@ const ProjectFlockDetail = ({
Kategori
</div>
<div className='col-span-2'>
{formatTitleCase(projectFlock.category)}
{formatTitleCase(projectFlock.category ?? '')}
</div>
</div>
</div>
@@ -231,6 +257,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 +275,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
@@ -355,7 +384,7 @@ const ProjectFlockDetail = ({
color='neutral'
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`}
+20 -1
View File
@@ -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: 'Closed',
},
] as const;
export const CHICKINS_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Pengajuan',
-7
View File
@@ -57,13 +57,6 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
link: '/expense',
icon: 'uil:wallet',
},
{
title: 'Perhitungan Sapronak',
link: '/us-284',
icon: 'uil:calculator',
},
{
title: 'Persediaan',
link: '/inventory',
@@ -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: 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
// });
/*
// 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;
}
*/
}
}