From b2f4317c08eb4aa93a636c10ab17849962ff47fe Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 12 Nov 2025 15:24:44 +0700 Subject: [PATCH] refactor(FE-238-239-240): implement approval workflow chickin & project flock, membuat custom hook useApprovals, dan handling error format approvals --- .../chickin/add/kandang/page.tsx | 8 +- .../production/project-flock/detail/page.tsx | 23 +-- src/components/Modal.tsx | 13 +- src/components/pages/ApprovalSteps.tsx | 165 ++++++++++++++--- .../production/chickin/form/ChickinForm.tsx | 21 ++- .../chickin/form/tabs/ChickLogsView.tsx | 4 +- .../chickin/ProjectFlockChickinDetail.tsx | 15 +- .../project-flock/form/ProjectFlockForm.tsx | 172 ++++-------------- .../form/ProjectFlockKandangTable.tsx | 1 - src/config/constant.ts | 13 ++ src/services/api/production/project-flock.ts | 5 - src/types/api/api-general.d.ts | 4 + src/types/config/constant.d.ts | 14 ++ 13 files changed, 258 insertions(+), 200 deletions(-) diff --git a/src/app/production/project-flock/chickin/add/kandang/page.tsx b/src/app/production/project-flock/chickin/add/kandang/page.tsx index 0805f010..a22039d1 100644 --- a/src/app/production/project-flock/chickin/add/kandang/page.tsx +++ b/src/app/production/project-flock/chickin/add/kandang/page.tsx @@ -16,8 +16,12 @@ export default function AddChickinKandang() { data: projectFlockKandang, isLoading: isLoading, mutate: refreshProjectFlockKandang, - } = useSWR(projectFlockKandangId, (id: number) => - ProjectFlockKandangApi.getSingle(id) + } = useSWR( + `get-single-project-flock-kandang/${projectFlockKandangId}`, + async () => + ProjectFlockKandangApi.getSingle( + parseInt(projectFlockKandangId as string) + ) ); if (!projectFlockKandangId) { diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx index 28a577ff..91d4dfd5 100644 --- a/src/app/production/project-flock/detail/page.tsx +++ b/src/app/production/project-flock/detail/page.tsx @@ -2,11 +2,8 @@ import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { FlockApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; -import { ProjectFlock } from '@/types/api/production/project-flock'; import { useRouter, useSearchParams } from 'next/navigation'; -import { useState } from 'react'; import useSWR from 'swr'; const ProjectFlockDetail = () => { @@ -15,22 +12,12 @@ const ProjectFlockDetail = () => { const projectFlockId = searchParams.get('projectFlockId'); - const [projectName, setProjectName] = useState(); - const { data: projectFlock, isLoading: isLoadingProjectFlock, mutate: refreshProjectFlock, } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); - const { - data: approvalLines, - isLoading: isLoadingApprovalLines, - mutate: refreshApprovalLines, - } = useSWR('approvals', (id: number) => - ProjectFlockApi.getApprovalLines((projectFlockId ?? 0) as number) - ); - if (!projectFlockId) { router.back(); @@ -51,15 +38,13 @@ const ProjectFlockDetail = () => { return (
- {isLoadingProjectFlock || - (isLoadingApprovalLines && ( - - ))} - {isResponseSuccess(projectFlock) && isResponseSuccess(approvalLines) && ( + {isLoadingProjectFlock && ( + + )} + {isResponseSuccess(projectFlock) && ( )} diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index ea53d2c9..a242b1e4 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -16,7 +16,7 @@ export const useModal = () => { const openModal = useCallback(() => { if (!ref.current) return; - ref.current.showModal(); + ref.current.show(); setOpen(true); }, []); @@ -30,7 +30,6 @@ export const useModal = () => { open ? closeModal() : openModal(); }, [open, closeModal, openModal]); - // Gunakan useEffect agar event listener tidak didaftarkan berulang kali useEffect(() => { const dialog = ref.current; if (!dialog) return; @@ -48,7 +47,6 @@ export const useModal = () => { interface ModalProps { ref: RefObject; - id?: string; children?: ReactNode; closeOnBackdrop?: boolean; className?: { @@ -57,13 +55,7 @@ interface ModalProps { }; } -const Modal = ({ - ref, - id, - children, - closeOnBackdrop, - className, -}: ModalProps) => { +const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => { const handleBackdropClick = (e: React.MouseEvent) => { if (closeOnBackdrop && e.target === ref.current) { ref.current?.close(); @@ -73,7 +65,6 @@ const Modal = ({ return ( diff --git a/src/components/pages/ApprovalSteps.tsx b/src/components/pages/ApprovalSteps.tsx index daac4ab0..ba9bde3d 100644 --- a/src/components/pages/ApprovalSteps.tsx +++ b/src/components/pages/ApprovalSteps.tsx @@ -4,8 +4,17 @@ import StepItem from '@/components/steps/StepItem'; import Tooltip from '@/components/Tooltip'; import { cn, formatDate } from '@/lib/helper'; -import { BaseApproval, BaseGroupedApproval } from '@/types/api/api-general'; -import { ApprovalLine } from '@/types/config/constant'; +import { + BaseApiResponse, + BaseApproval, + BaseGroupedApproval, + ModuleWithApproval, +} from '@/types/api/api-general'; +import { AppConfigData, ApprovalLine } from '@/types/config/constant'; +import useSWR from 'swr'; +import { httpClientFetcher } from '@/services/http/client'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useCallback, useMemo } from 'react'; export type ApprovalStepStatus = 'APPROVED' | 'REJECTED' | 'WAITING' | 'IDLE'; @@ -120,7 +129,7 @@ export const formatGroupedApprovalsToApprovalSteps = ( const currentStepNumber = approvalLineItem.step_number; const lastStepNumber = - groupedApprovals[groupedApprovals.length - 1].step_number; + groupedApprovals[groupedApprovals.length - 1]?.step_number; if (!approvalGroup && currentStepNumber <= lastStepNumber) { throw new Error( @@ -137,22 +146,24 @@ export const formatGroupedApprovalsToApprovalSteps = ( }; } - let approvalStatus: ApprovalStepStatus; + let approvalStatus: ApprovalStepStatus = 'IDLE'; if (approvalGroup.step_number <= latestApproval.step_number) { - switch (approvalGroup.approvals[0].action) { - case 'CREATED': - case 'APPROVED': - approvalStatus = 'APPROVED'; - break; + if (approvalGroup.approvals) { + switch (approvalGroup?.approvals[0]?.action) { + case 'CREATED': + case 'APPROVED': + approvalStatus = 'APPROVED'; + break; - case 'REJECTED': - approvalStatus = 'REJECTED'; - break; + case 'REJECTED': + approvalStatus = 'REJECTED'; + break; - default: - approvalStatus = 'IDLE'; - break; + default: + approvalStatus = 'IDLE'; + break; + } } } else if (approvalGroup.step_number === latestApproval.step_number + 1) { approvalStatus = 'WAITING'; @@ -160,13 +171,13 @@ export const formatGroupedApprovalsToApprovalSteps = ( approvalStatus = 'IDLE'; } - const approvalLogs: ApprovalStepLog[] = approvalGroup.approvals.map( - (approval) => ({ - action_by: approval.action_by.name, - date: approval.action_at, - notes: approval.notes, - }) - ); + const approvalLogs: ApprovalStepLog[] = approvalGroup.approvals + ? approvalGroup.approvals.map((approval) => ({ + action_by: approval.action_by.name, + date: approval.action_at, + notes: approval.notes, + })) + : []; return { name: approvalGroup.step_name, @@ -179,3 +190,113 @@ export const formatGroupedApprovalsToApprovalSteps = ( }; export default ApprovalSteps; + +const useApprovalSteps = ({ + moduleUrl, + moduleName, + moduleId, + params, +}: { + moduleUrl: string; + moduleName: string; + moduleId: string; + params?: { + page: number; + limit: number; + search?: string; + }; +}) => { + const paramString = new URLSearchParams({ + page: params?.page?.toString() || '', + limit: params?.limit?.toString() || '', + search: params?.search || '', + }).toString(); + + const SWR_KEY_CONSTANTS = '/constants'; + const SWR_KEY_APPROVALS = + moduleName && moduleId + ? `/approvals?module_name=${moduleName}&module_id=${moduleId}&group_step_number=true${ + params ? `&${paramString}` : '' + }` + : null; + const SWR_KEY_CURRENT_DATA = moduleUrl; + + // Get Approval Lines dari GET /constant + const { data: constData, isLoading: constIsLoading } = useSWR( + SWR_KEY_CONSTANTS, + async (url) => { + return await httpClientFetcher(url); + } + ); + + // Get Grouped Data dari GET /approvals + const { + data: approvalData, + isLoading: approvalIsLoading, + mutate: mutateApprovals, + } = useSWR(SWR_KEY_APPROVALS, async (url) => { + return await httpClientFetcher>(url); + }); + + // Get latest approval + const { + data: currentData, + isLoading: currentIsLoading, + mutate: mutateCurrentData, + } = useSWR(SWR_KEY_CURRENT_DATA, async (url) => { + return await httpClientFetcher>(url); + }); + + // Fungsi Refresh + const refresh = useCallback(async () => { + await Promise.all([mutateApprovals(), mutateCurrentData()]); + }, [mutateApprovals, mutateCurrentData]); + + const { approvalLine, groupedApprovals, latestApproval } = useMemo(() => { + const line = constData + ? (constData.approval_workflows.find((approval) => { + return approval.key === moduleName; + })?.steps ?? []) + : []; + + const grouped = isResponseSuccess(approvalData) ? approvalData.data : []; + + const latest = isResponseSuccess(currentData) + ? currentData.data?.approval + : undefined; + + return { + approvalLine: line, + groupedApprovals: grouped, + latestApproval: latest, + }; + }, [constData, approvalData, currentData, moduleName]); + + const isLoading = constIsLoading || approvalIsLoading || currentIsLoading; + + const approvals = useMemo(() => { + if (isLoading || !approvalLine.length || !latestApproval) { + return []; + } + try { + return formatGroupedApprovalsToApprovalSteps( + approvalLine, + groupedApprovals, + latestApproval as BaseApproval + ); + } catch (error) { + console.warn('Gagal memformat approval steps:', error); + return []; + } + }, [isLoading, approvalLine, groupedApprovals, latestApproval]); + + // Return Hook + return { + approvals, + isLoading, + rawData: isResponseSuccess(currentData) ? currentData.data : undefined, + refresh, + }; +}; + +export { useApprovalSteps }; diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index 30575d17..5bd23368 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -10,6 +10,9 @@ import Tabs from '@/components/Tabs'; import ChickinFormView from './tabs/ChickinFormView'; import ChickinLogsView from './tabs/ChickLogsView'; import { useState } from 'react'; +import ApprovalSteps, { + useApprovalSteps, +} from '@/components/pages/ApprovalSteps'; const ChickinFormKandang = ({ formType = 'add', initialValues, @@ -21,9 +24,20 @@ const ChickinFormKandang = ({ }) => { const [activeTabId, setActiveTabId] = useState('formChickIn'); + const { + approvals, + isLoading: approvalsLoading, + refresh: refreshApprovals, + } = useApprovalSteps({ + moduleUrl: `/production/project-flock-kandangs/${initialValues?.id}`, + moduleName: 'PROJECT_FLOCK_KANDANGS', + moduleId: initialValues?.id.toString() ?? '', + }); + const afterSubmitFormChickin = () => { setActiveTabId('logsChickIn'); afterSubmit && afterSubmit(); + refreshApprovals(); }; return ( @@ -32,6 +46,11 @@ const ChickinFormKandang = ({ title='Chick In DOC' backUrl={`/production/project-flock/chickin/add?projectFlockId=${initialValues?.project_flock?.id}`} /> + + {approvals && !approvalsLoading && ( + + )} + ), id: 'logsChickIn', diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 2274e64b..6f7e564d 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -47,9 +47,7 @@ const ChickinLogsView = ({ } confirmModal.closeModal(); setIsApproveLoading(false); - if (afterSubmit) { - afterSubmit(); - } + afterSubmit && afterSubmit(); }; return ( diff --git a/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx b/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx index 9ed2a1e6..7afac22b 100644 --- a/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx +++ b/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx @@ -58,7 +58,15 @@ const ProjectFlockChickinDetail = ({ options: options, isLoadingOptions: isLoadingListProjectFlock, rawData: listProjectFlock, - } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name'); + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + '', + { + search: searchProjectFlock, + } + ); // Handle Function const handleChickinClick = async ( @@ -242,7 +250,7 @@ const ProjectFlockChickinDetail = ({
} data={ - isResponseSuccess(listProjectFlockKandang) + projectFlock && isResponseSuccess(listProjectFlockKandang) ? listProjectFlockKandang.data : [] } @@ -293,6 +301,8 @@ const ProjectFlockChickinDetail = ({ .replace(/_/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase())} /> + ) : projectFlock?.approval?.step_number === 1 ? ( + ) : ( ); @@ -310,6 +320,7 @@ const ProjectFlockChickinDetail = ({ handleChickinClick(props.row.original); }} className='p-1' + disabled={projectFlock?.approval?.step_number === 1} > | undefined >; @@ -59,16 +54,11 @@ interface ProjectFlockFormProps { const ProjectFlockForm = ({ formType = 'add', initialValues, - initialApprovals, refreshProjectFlocks, }: ProjectFlockFormProps) => { // State const router = useRouter(); - const projectFlockSteps = APPROVAL_WORKFLOWS.find( - (step) => step.key === 'PROJECT_FLOCKS' - ); - const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] = useState(''); const [selectedArea, setSelectedArea] = useState(''); @@ -120,34 +110,28 @@ const ProjectFlockForm = ({ }, [initialValues]); // Fetch Data - const { - rawData: flocks, - isLoadingOptions: isLoadingFlocks, - options: optionsFlock, - } = useSelect(FlockApi.basePath, 'id', 'name'); + const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } = + useSelect(FlockApi.basePath, 'id', 'name'); - const { - options: optionsArea, - isLoadingOptions: isLoadingAreas, - rawData: areas, - } = useSelect(AreaApi.basePath, 'id', 'name'); + const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name' + ); - const { - options: optionsLocation, - isLoadingOptions: isLoadingLocations, - rawData: locations, - } = useSelect(LocationApi.basePath, 'id', 'name', '', { - area_id: - selectedArea != '' - ? selectedArea - : ((initialValues?.area?.id ?? '') as string), - }); + const { options: optionsLocation, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', '', { + area_id: + selectedArea != '' + ? selectedArea + : ((initialValues?.area?.id ?? '') as string), + }); - const { - options: optionsFcr, - isLoadingOptions: isLoadingFcrs, - rawData: fcrs, - } = useSelect(FcrApi.basePath, 'id', 'name'); + const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect( + FcrApi.basePath, + 'id', + 'name' + ); const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: '', @@ -164,6 +148,16 @@ const ProjectFlockForm = ({ (id: string) => ProjectFlockApi.getNextPeriod(id) ); + const { + approvals, + isLoading: approvalsLoading, + refresh: refreshApprovals, + } = useApprovalSteps({ + moduleUrl: `/production/project-flocks/${initialValues?.id}`, + moduleName: 'PROJECT_FLOCKS', + moduleId: initialValues?.id.toString() ?? '', + }); + useEffect(() => { if (isResponseSuccess(kandang)) { if (selectedLocation) { @@ -516,6 +510,7 @@ const ProjectFlockForm = ({ if (isResponseError(approveProjectFlockRes)) { toast.error(approveProjectFlockRes?.message as string); } + refreshApprovals(); confirmModal.closeModal(); setIsApproveLoading(false); }; @@ -558,79 +553,8 @@ const ProjectFlockForm = ({ )} - {formType == 'detail' && initialApprovals && ( -
- - {projectFlockSteps?.steps.map((step, idx) => { - const approvalLogs = initialApprovals.find( - (approve) => approve.step_number == step.step_number - ); - return ( - - {approvalLogs && - approvalLogs?.approvals?.map((approval, idx) => { - return ( -
  • -
    - Status: {approval.step_name} - - Oleh: {approval.action_by.name} - - - Tanggal:{' '} - {formatDate( - approval.action_at, - 'DD-MM-yyyy HH:mm:ss' - )} - -
    -
  • - ); - })} - - } - > - {step.step_number <= - (initialValues?.approval.step_number ?? 0) ? ( - - ) : ( - - )} - - } - > - {step.step_name} -
    - ); - })} -
    -
    + {approvals && !approvalsLoading && ( + )} {formType == 'detail' && (
    @@ -675,32 +599,12 @@ const ProjectFlockForm = ({ ); }} > + Chickin )}
    )} - - {JSON.stringify(formik.values)} - - - {JSON.stringify(formik.initialValues)} - - - {JSON.stringify(formik.errors)} -
    - {JSON.stringify(initialKandangIdSet)} data={listKandang} columns={[ diff --git a/src/config/constant.ts b/src/config/constant.ts index 008ba5f8..7e730635 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -246,6 +246,19 @@ export const APPROVAL_WORKFLOWS = [ }, ], }, + { + key: 'PROJECT_FLOCK_KANDANGS', + steps: [ + { + step_number: 1, + step_name: 'Pengajuan', + }, + { + step_number: 2, + step_name: 'Disetujui', + }, + ], + }, { key: 'RECORDINGS', steps: [ diff --git a/src/services/api/production/project-flock.ts b/src/services/api/production/project-flock.ts index f1f47994..f53d089e 100644 --- a/src/services/api/production/project-flock.ts +++ b/src/services/api/production/project-flock.ts @@ -39,11 +39,6 @@ export class ProjectFlockService extends BaseApiService< > { const path = `/approvals`; try { - console.log({ - module_id: id, - module_name: 'PROJECT_FLOCKS', - group_step_number: true, - }); return await httpClient>(path, { method: 'GET', query: { diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index af9144e2..37040654 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -112,6 +112,10 @@ export type BaseGroupedApproval = { approvals: BaseApproval[]; }; +interface ModuleWithApproval { + approval?: BaseApproval; +} + export type Approvals = BaseApiResponse; export type GroupedApprovals = BaseApiResponse; diff --git a/src/types/config/constant.d.ts b/src/types/config/constant.d.ts index 3e4371be..eeb0d7f6 100644 --- a/src/types/config/constant.d.ts +++ b/src/types/config/constant.d.ts @@ -2,3 +2,17 @@ export type ApprovalLine = { step_number: number; step_name: string; }[]; + +export interface ApprovalWorkflow { + key: string; + steps: ApprovalLine; +} + +export interface AppConfigData { + approval_workflows: ApprovalWorkflow[]; + flags: string[]; + warehouse_types: string[]; + stock_log: string; + supplier_categories: string[]; + customer_supplier_types: string[]; +}