From 5dccaf40cb011956c6cf65cc06cbd75bae03ad5b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 12 Nov 2025 09:04:23 +0700 Subject: [PATCH] refactor(FE-88): memisahkan file api project flock & penyesuaian tipe data dan paylod dengan BE --- .../project-flock/detail/edit/page.tsx | 28 +- .../production/project-flock/detail/page.tsx | 49 ++-- src/components/Tabs.tsx | 129 ++++++++++ .../production/chickin/form/ChickinForm.tsx | 3 +- .../chickin/form/tabs/ChickLogsView.tsx | 172 +++++++++++++ .../chickin/form/tabs/ChickinFormView.tsx | 236 +++++++++++++++++ .../project-flock/ProjectFlockTable.tsx | 2 +- .../chickin/ProjectFlockChickinDetail.tsx | 6 +- .../project-flock/form/ProjectFlockForm.tsx | 176 ++++++++++--- .../form/ProjectFlockKandangTable.tsx | 243 +++++++++--------- .../recording/form/RecordingForm.tsx | 2 +- src/services/api/production.ts | 10 - src/services/api/production/chickin.ts | 43 ++++ 13 files changed, 888 insertions(+), 211 deletions(-) create mode 100644 src/components/Tabs.tsx create mode 100644 src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx create mode 100644 src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx create mode 100644 src/services/api/production/chickin.ts diff --git a/src/app/production/project-flock/detail/edit/page.tsx b/src/app/production/project-flock/detail/edit/page.tsx index 7576cc27..2af068ab 100644 --- a/src/app/production/project-flock/detail/edit/page.tsx +++ b/src/app/production/project-flock/detail/edit/page.tsx @@ -2,7 +2,7 @@ import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { ProjectFlockApi } from '@/services/api/production'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; @@ -12,10 +12,11 @@ const ProjectFlockEdit = () => { const projectFlockId = searchParams.get('projectFlockId'); - const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR( - projectFlockId, - (id: number) => ProjectFlockApi.getSingle(id) - ); + const { + data: projectFlock, + isLoading: isLoadingProjectFlock, + mutate: refreshProjectFlocks, + } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); if (!projectFlockId) { router.back(); @@ -27,18 +28,25 @@ const ProjectFlockEdit = () => { ); } - if (!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))) { + if ( + !isLoadingProjectFlock && + (!projectFlock || isResponseError(projectFlock)) + ) { router.replace('/404'); return; } return ( -
- {isLoadingCostumer && ( +
+ {isLoadingProjectFlock && ( )} - {!isLoadingCostumer && isResponseSuccess(projectFlock) && ( - + {!isLoadingProjectFlock && isResponseSuccess(projectFlock) && ( + <> + {JSON.stringify(projectFlock.data)} + + + )}
); diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx index e64005d4..28a577ff 100644 --- a/src/app/production/project-flock/detail/page.tsx +++ b/src/app/production/project-flock/detail/page.tsx @@ -3,7 +3,7 @@ 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'; +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'; @@ -23,12 +23,13 @@ const ProjectFlockDetail = () => { mutate: refreshProjectFlock, } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); - const flockUrl = `${FlockApi.basePath}`; const { - data: flock, - isLoading: isLoadingFlock, - mutate: refreshFlock, - } = useSWR(flockUrl, FlockApi.getAllFetcher); + data: approvalLines, + isLoading: isLoadingApprovalLines, + mutate: refreshApprovalLines, + } = useSWR('approvals', (id: number) => + ProjectFlockApi.getApprovalLines((projectFlockId ?? 0) as number) + ); if (!projectFlockId) { router.back(); @@ -48,37 +49,17 @@ const ProjectFlockDetail = () => { return; } - // Attach flock id to project flock - let projectFlockAttached: ProjectFlock | undefined; - - if (isResponseSuccess(projectFlock) && isResponseSuccess(flock)) { - projectFlockAttached = { - ...projectFlock.data, - flock: flock.data.find( - (flock) => - flock.name == - projectFlock?.data?.flock_name - .trim() - .split(/\s+/) - .slice(0, -1) - .join(' ') - ), - }; - console.log('projectFlockAttached'); - console.log(projectFlockAttached); - console.log('flocks'); - console.log(flock.data); - } - return ( -
- {isLoadingProjectFlock && ( - - )} - {projectFlockAttached && ( +
+ {isLoadingProjectFlock || + (isLoadingApprovalLines && ( + + ))} + {isResponseSuccess(projectFlock) && isResponseSuccess(approvalLines) && ( )} diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx new file mode 100644 index 00000000..2ad2477d --- /dev/null +++ b/src/components/Tabs.tsx @@ -0,0 +1,129 @@ +import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'; +import { cn } from '@/lib/helper'; + +export interface TabItem { + id: string; + label: ReactNode; + content?: ReactNode; + disabled?: boolean; +} + +export interface TabsProps + extends Omit, 'className'> { + tabs: TabItem[]; + variant?: 'bordered' | 'lifted' | 'boxed'; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + placement?: 'top' | 'bottom'; + /** Tab yang aktif secara default (uncontrolled mode) */ + defaultActiveId?: string; + /** Tab yang aktif (controlled mode, dikontrol parent) */ + activeTabId?: string; + className?: + | string + | { + wrapper?: string; + tab?: string; + content?: string; + }; + onTabChange?: (tabId: string) => void; +} + +const Tabs = ({ + tabs, + variant, + size = 'md', + placement = 'top', + defaultActiveId, + activeTabId: controlledActiveId, + className, + onTabChange, + ...props +}: TabsProps) => { + // State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset + const [uncontrolledActiveId, setUncontrolledActiveId] = useState( + defaultActiveId || tabs[0]?.id || '' + ); + + const isControlled = controlledActiveId !== undefined; + const activeTabId = isControlled ? controlledActiveId : uncontrolledActiveId; + + const handleTabChange = (tabId: string) => { + if (tabId === activeTabId) return; + if (!isControlled) setUncontrolledActiveId(tabId); + onTabChange?.(tabId); + }; + + const { wrapper: wrapperClassName, tab: tabClassName } = + typeof className === 'object' + ? className + : { wrapper: className, tab: undefined }; + + const getTabsClasses = () => { + const variantClasses: Record = { + bordered: 'tabs-bordered', + lifted: 'tabs-lift', + boxed: 'tabs-box', + }; + + const sizeClasses: Record = { + xs: 'tabs-xs', + sm: 'tabs-sm', + md: '', + lg: 'tabs-lg', + xl: 'tabs-xl', + }; + + const placementClasses: Record = { + top: '', + bottom: 'tabs-bottom', + }; + + return cn( + 'tabs', + variant && variantClasses[variant], + sizeClasses[size], + placementClasses[placement], + wrapperClassName + ); + }; + + const getTabClasses = (isActive: boolean, isDisabled?: boolean) => + cn( + 'tab', + { + 'tab-active': isActive, + 'tab-disabled': isDisabled, + }, + tabClassName + ); + + const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content; + + return ( +
+
+ {tabs.map(({ id, label, disabled }) => ( + + ))} +
+ + {activeContent &&
{activeContent}
} +
+ ); +}; + +export default Tabs; diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index f007a1cf..30575d17 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -29,7 +29,6 @@ const ChickinFormKandang = ({ return (
@@ -96,7 +95,7 @@ const ChickinFormKandang = ({ tabs={[ { id: 'formChickIn', - label: 'Form Chick In', + label: 'Tambah Chick In', content: ( void; +}) => { + const confirmModal = useModal(); + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + + const handleClickApprove = () => { + confirmModal.openModal(); + }; + + const confirmationModalApproveClickHandler = async () => { + setChickinErrorMessage(''); + setIsApproveLoading(true); + const approveChickinRes = await ChickinApi.singleApproval( + initialValues?.id as number, + 'APPROVED' + ); + if (isResponseSuccess(approveChickinRes)) { + toast.success(approveChickinRes?.message as string); + } + if (isResponseError(approveChickinRes)) { + toast.error(approveChickinRes?.message as string); + setChickinErrorMessage(approveChickinRes?.message as string); + } + confirmModal.closeModal(); + setIsApproveLoading(false); + if (afterSubmit) { + afterSubmit(); + } + }; + + return ( + <> + +
+ +
+ + data={initialValues?.chickins || []} + columns={[ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorFn: (row) => row.chick_in_date, + header: 'Tanggal Chick In', + cell: (props) => { + return formatDate(props.getValue() as string, 'DD MMM YYYY'); + }, + }, + { + accessorFn: (row) => row.product_warehouse?.warehouse?.name, + header: 'Kandang', + }, + { + accessorFn: (row) => row.product_warehouse?.product?.name, + header: 'Produk', + }, + { + accessorFn: (row) => row.usage_qty ?? row.pending_usage_qty, + header: 'Jumlah Chick In', + cell: (props) => { + if (props.row.original.usage_qty != 0) { + return formatNumber(props.row.original.usage_qty); + } else if (props.row.original.pending_usage_qty != 0) { + return formatNumber(props.row.original.pending_usage_qty); + } else { + return '-'; + } + }, + }, + { + accessorFn: (row) => row.pending_usage_qty, + header: 'Status', + cell: (props) => { + return ( + + ); + }, + }, + ]} + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.chickins?.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + /> + {chickinErrorMessage && ( +
setChickinErrorMessage('')}> + {chickinErrorMessage} +
+ )} +
+ + + ); +}; + +export default ChickinLogsView; diff --git a/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx b/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx new file mode 100644 index 00000000..1803b0a0 --- /dev/null +++ b/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx @@ -0,0 +1,236 @@ +'use client'; + +import Card from '@/components/Card'; +import Table from '@/components/Table'; +import { + ChickinFormValues, + ChickinRequestFormValues, + ChickinSchema, +} from '../ChickinForm.schema'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import NumberInput from '@/components/input/NumberInput'; +import Button from '@/components/Button'; +import { useCallback, useEffect, useState } from 'react'; +import { useFormik } from 'formik'; +import { flushSync } from 'react-dom'; +import { CreateChickinPayload } from '@/types/api/production/chickin'; +import { ChickinApi } from '@/services/api/production/chickin'; +import { isResponseError } from '@/lib/api-helper'; +import toast from 'react-hot-toast'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { useRouter } from 'next/navigation'; +import Alert from '@/components/Alert'; + +const ChickinFormView = ({ + formType = 'add', + initialValues, + afterSubmit, +}: { + formType?: 'add' | 'detail' | 'edit'; + initialValues: ProjectFlockKandang; + afterSubmit?: () => void; +}) => { + const router = useRouter(); + const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + + const createChickin = useCallback( + async (payload: CreateChickinPayload) => { + const createChickinRes = await ChickinApi.create(payload); + if (isResponseError(createChickinRes)) { + setChickinErrorMessage(createChickinRes.message); + return; + } + + toast.success(createChickinRes?.message as string); + // router.push( + // `/production/project-flock/chickin/add?projectFlockId=${initialValues?.project_flock?.id}` + // ); + if (afterSubmit) { + afterSubmit(); + } + }, + [router] + ); + const handleReset = async () => { + flushSync(() => { + formik.resetForm({ + values: { + project_flock_kandang_id: initialValues?.id, + chickin_requests: initialValues?.available_qtys + ? initialValues.available_qtys.map((availableQty) => ({ + chick_in_date: '', + product_warehouse_id: availableQty.product_warehouse.id, + available_qty: availableQty.available_qty, + note: `Chickin project-flock-kandang-${initialValues?.id} product-warehouse-${availableQty.product_warehouse.id}`, + })) + : [], + }, + }); + }); + + formik.setTouched({ + chickin_requests: initialValues?.available_qtys?.map(() => ({ + chick_in_date: true, + })), + }); + + const errors = await formik.validateForm(); + formik.setErrors(errors); + }; + + const formik = useFormik({ + enableReinitialize: true, + validationSchema: ChickinSchema, + initialValues: { + project_flock_kandang_id: initialValues?.id, + chickin_requests: initialValues?.available_qtys + ? initialValues.available_qtys.map((availableQty) => ({ + chick_in_date: '', + product_warehouse_id: availableQty.product_warehouse.id, + available_qty: availableQty.available_qty, + note: `Chickin project-flock-kandang-${initialValues?.id} product-warehouse-${availableQty.product_warehouse.id}`, + })) + : [], + }, + onSubmit: (values) => { + setChickinErrorMessage(''); + createChickin(values as CreateChickinPayload); + if (afterSubmit) { + afterSubmit(); + } + }, + }); + + const { setValues: formikSetValues } = formik; + + useEffect(() => { + formikSetValues({ + project_flock_kandang_id: initialValues?.id, + chickin_requests: initialValues?.available_qtys + ? initialValues.available_qtys.map((availableQty) => ({ + chick_in_date: '', + product_warehouse_id: availableQty.product_warehouse.id, + available_qty: availableQty.available_qty, + note: `Chickin project-flock-kandang-${initialValues?.id} product-warehouse-${availableQty.product_warehouse.id}`, + })) + : [], + }); + }, [formikSetValues, initialValues]); + + return ( +
{ + handleReset(); + }} + onSubmit={formik.handleSubmit} + > + + + data={formik.values.chickin_requests || []} + columns={[ + { + accessorFn: (row) => row.chick_in_date, + header: 'Tanggal Chick In', + cell(props) { + return ( + + ); + }, + }, + { + accessorFn: (row) => row.product_warehouse_id, + header: 'Produk', + cell(props) { + const availableQty = initialValues?.available_qtys?.find( + (availableQty) => + availableQty.product_warehouse.id === + props.row.original.product_warehouse_id + ); + return ( + + ); + }, + }, + { + accessorFn: (row) => row.product_warehouse_id, + header: 'Jumlah (ekor)', + cell(props) { + const availableQty = initialValues?.available_qtys?.find( + (availableQty) => + availableQty.product_warehouse.id === + props.row.original.product_warehouse_id + ); + return ( + + ); + }, + }, + ]} + className={{ + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-2 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-2 py-2 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + emptyContent={ +
+ + Isi persediaan DOC untuk kandang belum tersedia... + +
+ } + /> +
+
+ + +
+ {chickinErrorMessage && ( +
setChickinErrorMessage('')}> + {chickinErrorMessage} +
+ )} +
+ ); +}; + +export default ChickinFormView; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 4bafac88..7c7c70ed 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -13,7 +13,7 @@ import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; -import { ProjectFlockApi } from '@/services/api/production'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { BaseApiResponse } from '@/types/api/api-general'; import { Kandang } from '@/types/api/master-data/kandang'; diff --git a/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx b/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx index 8052d958..9ed2a1e6 100644 --- a/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx +++ b/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx @@ -11,10 +11,8 @@ import PillBadge from '@/components/PillBadge'; import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; -import { - ProjectFlockApi, - ProjectFlockKandangApi, -} from '@/services/api/production'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; +import { ProjectFlockKandangApi } from '@/services/api/production'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Kandang } from '@/types/api/master-data/kandang'; import { ProjectFlock } from '@/types/api/production/project-flock'; diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 932b87f8..5b52e59d 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -34,7 +34,7 @@ import TextInput from '@/components/input/TextInput'; import { Kandang } from '@/types/api/master-data/kandang'; import Collapse from '@/components/Collapse'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; -import { BaseApiResponse } from '@/types/api/api-general'; +import { BaseApiResponse, BaseGroupedApproval } from '@/types/api/api-general'; import { APPROVAL_WORKFLOWS, FLOCK_CATEGORY_OPTIONS } from '@/config/constant'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; @@ -45,10 +45,12 @@ import StepItem from '@/components/steps/StepItem'; import Tooltip from '@/components/Tooltip'; import { id, is } from 'react-day-picker/locale'; import { formatDate } from '@/lib/helper'; +import Card from '@/components/Card'; interface ProjectFlockFormProps { formType?: 'add' | 'edit' | 'detail'; initialValues?: ProjectFlock; + initialApprovals?: BaseGroupedApproval[]; refreshProjectFlocks?: KeyedMutator< BaseApiResponse | undefined >; @@ -57,6 +59,7 @@ interface ProjectFlockFormProps { const ProjectFlockForm = ({ formType = 'add', initialValues, + initialApprovals, refreshProjectFlocks, }: ProjectFlockFormProps) => { // State @@ -70,14 +73,16 @@ const ProjectFlockForm = ({ useState(''); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); - const [disabledLocation, setDisabledLocation] = useState(true); + const [disabledLocation, setDisabledLocation] = useState( + initialValues?.location?.id ? false : true + ); const [openSelectKandangs, setOpenSelectKandangs] = useState( initialValues?.kandangs && initialValues?.kandangs?.length > 0 ); const [optionsKandang, setOptionsKandang] = useState( initialValues?.kandangs ?? [] ); - const [selectedFlock, setSelectedFlock] = useState( + const [selectedFlock, setSelectedFlock] = useState( initialValues?.flock?.id ?? 0 ); @@ -131,7 +136,12 @@ const ProjectFlockForm = ({ options: optionsLocation, isLoadingOptions: isLoadingLocations, rawData: locations, - } = useSelect(LocationApi.basePath, 'id', 'name'); + } = useSelect(LocationApi.basePath, 'id', 'name', '', { + area_id: + selectedArea != '' + ? selectedArea + : ((initialValues?.area?.id ?? '') as string), + }); const { options: optionsFcr, @@ -150,27 +160,34 @@ const ProjectFlockForm = ({ } = useSWR(kandangUrl, KandangApi.getAllFetcher); const { data: periodFlocks, isLoading: isLoadingPeriodFlocks } = useSWR( - `${selectedFlock.toString()}/periods`, + `${selectedFlock?.toString()}/periods`, (id: string) => ProjectFlockApi.getNextPeriod(id) ); - const { data: approvalLines, isLoading: isLoadingApprovalLines } = useSWR( - selectedFlock.toString(), - (id: number) => ProjectFlockApi.getApprovalLines(id) - ); - useEffect(() => { if (isResponseSuccess(kandang)) { if (selectedLocation) { setOptionsKandang(kandang.data); setOpenSelectKandangs(true); } else { + formik.setFieldValue('kandang_ids', []); setOptionsKandang([]); setOpenSelectKandangs(false); - formik.setFieldValue('kandang_ids', []); + const selectedRowIds = Object.keys(rowSelection) + .filter((id) => rowSelection[id]) + .map((id) => parseInt(id)); + if ( + JSON.stringify(kandang.data.map((k) => k.id)) !== + JSON.stringify(formik.values.kandang_ids) + ) { + formik.setFieldValue('kandang_ids', []); + setRowSelection({}); + } else { + formik.setFieldValue('kandang_ids', selectedRowIds); + } } } - }, [kandang]); + }, [kandang, selectedLocation]); useEffect(() => { if (initialValues?.kandangs) { refreshKandang(); @@ -181,7 +198,7 @@ const ProjectFlockForm = ({ ); setRowSelection(newRowSelection); } - }, [initialValues, refreshKandang]); + }, [initialValues, kandang]); // Options Handler const areaChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -202,9 +219,13 @@ const ProjectFlockForm = ({ }; const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('kandang_ids', []); setSelectedLocation((val as OptionType)?.value as string); optionChangeHandler(val, 'location'); - formik.setFieldValue('kandang_ids', []); + const selectedRowIds = Object.keys(rowSelection) + .filter((id) => rowSelection[id]) + .map((id) => parseInt(id)); + formik.setFieldValue('kandang_ids', selectedRowIds); }; const optionChangeHandler = ( @@ -216,13 +237,8 @@ const ProjectFlockForm = ({ `${inputName}_id`, val ? (val as OptionType)?.value : 0 ); - formik.setFieldValue( - `${inputName}_name`, - val ? (val as OptionType)?.label : 0 - ); formik.setFieldTouched(`${inputName}_id`, true); - formik.setFieldTouched(`${inputName}_name`, true); }; const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -267,11 +283,12 @@ const ProjectFlockForm = ({ // Formik InitialValue const formikInitialValues = useMemo(() => { return { - name: initialValues?.name ?? '', - flock: initialValues?.flock_name + name: initialValues?.flock_name, + flock: initialValues?.flock ? { value: initialValues?.flock?.id ?? 0, - label: initialValues?.flock_name, + label: + initialValues?.flock?.name ?? initialValues?.flock_name ?? '', } : null, area: initialValues?.area @@ -312,11 +329,57 @@ const ProjectFlockForm = ({ | undefined )[], }; - }, [initialValues, flocks]); + }, [initialValues]); // Formik const formik = useFormik({ - initialValues: formikInitialValues, + initialValues: { + name: initialValues?.flock_name, + flock: initialValues?.flock + ? { + value: initialValues?.flock?.id ?? 0, + label: + initialValues?.flock?.name ?? initialValues?.flock_name ?? '', + } + : null, + area: initialValues?.area + ? { + value: initialValues.area?.id, + label: initialValues.area.name, + } + : null, + category_option: initialValues?.category + ? { + value: initialValues.category, + label: initialValues.category, + } + : null, + fcr: initialValues?.fcr + ? { + value: initialValues.fcr?.id, + label: initialValues.fcr.name, + } + : null, + location: initialValues?.location + ? { + value: initialValues.location?.id, + label: initialValues.location.name, + } + : null, + flock_id: initialValues?.flock?.id ?? 0, + flock_name: initialValues?.flock_name ?? '', + area_id: initialValues?.area?.id ?? 0, + category: initialValues?.category as NonNullable< + 'GROWING' | 'LAYING' | undefined + >, + fcr_id: initialValues?.fcr?.id ?? 0, + location_id: initialValues?.location?.id ?? 0, + period: initialValues?.period ?? 1, + kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as ( + | number + | undefined + )[], + } as ProjectFlockFormValues, enableReinitialize: true, validationSchema: formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema, @@ -326,7 +389,7 @@ const ProjectFlockForm = ({ onSubmit: async (values) => { setProjectFlockFormErrorMessage(''); const payload: CreateProjectFlockPayload = { - flock_name: values.flock_name as string, + flock_name: values.flock?.label as string, area_id: values.area_id as number, category: values.category as string, fcr_id: values.fcr_id as number, @@ -367,7 +430,7 @@ const ProjectFlockForm = ({ useEffect(() => { formikSetValues(formikInitialValues); - }, [formikSetValues, formikInitialValues]); + }, [formikSetValues]); // Aktifkan lokasi jika formType = 'detail' useEffect(() => { @@ -397,7 +460,7 @@ const ProjectFlockForm = ({ if (isResponseError(periodFlocks)) { console.log(periodFlocks?.message as string); } - }, [periodFlocks, toast]); + }, [periodFlocks]); useEffect(() => { const selectedRowIds = Object.keys(rowSelection) @@ -495,11 +558,11 @@ const ProjectFlockForm = ({
)} - {formType == 'detail' && isResponseSuccess(approvalLines) && ( + {formType == 'detail' && initialApprovals && (
{projectFlockSteps?.steps.map((step, idx) => { - const approvalLogs = approvalLines.data.find( + const approvalLogs = initialApprovals.find( (approve) => approve.step_number == step.step_number ); return ( @@ -523,7 +586,7 @@ const ProjectFlockForm = ({ content={
    {approvalLogs && - approvalLogs.approvals.map((approval, idx) => { + approvalLogs?.approvals?.map((approval, idx) => { return (
  • @@ -601,8 +664,43 @@ const ProjectFlockForm = ({ Reject + {initialValues?.approval?.step_number == 2 && ( + + )}
    )} + + {JSON.stringify(formik.values)} + + + {JSON.stringify(formik.initialValues)} + + + {JSON.stringify(formik.errors)} +
    { optionChangeHandler(val, 'flock'); setSelectedFlock((val as OptionType)?.value as number); + formik.setFieldValue( + 'flock_name', + (val as OptionType)?.label + ); }} options={optionsFlock} isLoading={isLoadingFlocks} @@ -650,7 +752,11 @@ const ProjectFlockForm = ({ label='Lokasi' value={formik.values.location as OptionType} onChange={locationChangeHandler} - options={optionsLocation} + options={ + selectedArea != '' || initialValues?.area?.id + ? optionsLocation + : [] + } isLoading={isLoadingLocations} isError={ formik.touched.location_id && @@ -695,7 +801,7 @@ const ProjectFlockForm = ({ name='period' label='Periode' placeholder='Masukkan periode yang project' - value={formik.values.period as number} + value={formik.values.period ?? (1 as number)} onChange={formik.handleChange} isError={ formik.touched.period && Boolean(formik.errors.period) @@ -743,6 +849,7 @@ const ProjectFlockForm = ({ setRowSelection={setRowSelection} selectedIds={formik.values.kandang_ids} formType={formType} + initialValues={initialValues?.kandangs ?? []} />
@@ -764,7 +871,10 @@ const ProjectFlockForm = ({ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={ + !formik.isValid || formik.isSubmitting + // TODO: Add logic && ketika nilai kandang_ids sudah beda dari initial values + } className='px-4' > Submit diff --git a/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx b/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx index 759e44d8..ef96a52b 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx @@ -5,148 +5,159 @@ import PillBadge from '@/components/PillBadge'; import Table from '@/components/Table'; import { cn } from '@/lib/helper'; import { Kandang } from '@/types/api/master-data/kandang'; -import { OnChangeFn } from '@tanstack/react-table'; +import { OnChangeFn, Row } from '@tanstack/react-table'; +import { useMemo } from 'react'; const ProjectFlockKandangTable = ({ listKandang, rowSelection, setRowSelection, selectedIds, + initialValues, formType = 'add', }: { listKandang: Kandang[]; rowSelection: Record; setRowSelection: OnChangeFn>; selectedIds: (number | undefined)[]; + initialValues?: Kandang[]; formType: 'add' | 'edit' | 'detail'; }) => { - console.log('selectedIds'); - console.log(selectedIds); + const initialKandangIdSet = useMemo(() => { + return initialValues?.map((k) => k.id) ?? []; + }, [initialValues]); + const isRowEnabled = (row: Row) => { + const isDisabled = + !initialKandangIdSet.includes(row.original.id) && + (row.original.status == 'ACTIVE' || + row.original.status == 'PENGAJUAN' || + formType == 'detail'); + return !isDisabled; + }; return ( - - data={listKandang} - columns={[ - { - id: 'select', - header: ({ table }) => { - const allRows = table.getRowModel().rows; - const selectableRows = allRows.filter( - (row) => - row.original.status == 'NON_ACTIVE' || - row.original.status == 'PENGAJUAN' - ); + <> + {JSON.stringify(initialKandangIdSet)} + + data={listKandang} + columns={[ + { + id: 'select', + header: ({ table }) => { + const allRows = table.getRowModel().rows; + // 1. Filter semua baris dengan logika yang sama persis seperti di cell + const selectableRows = allRows.filter(isRowEnabled); - const allSelected = - selectableRows.every((row) => row.getIsSelected()) && - selectableRows.length != 0 && - formType != 'detail'; + // 2. Cek apakah SEMUA baris yang BISA DIPILIH sudah terpilih + const allSelected = + selectableRows.length > 0 && + selectableRows.every((row) => row.getIsSelected()); - const someSelected = - selectableRows.some((row) => row.getIsSelected()) && - !allSelected && - formType != 'detail'; + // 3. Cek apakah BEBERAPA baris yang BISA DIPILIH sudah terpilih + const someSelected = + selectableRows.some((row) => row.getIsSelected()) && + !allSelected; - const toggleSelectableRows = () => { - const shouldSelect = !allSelected; - selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); - }; + // 4. Fungsi toggle HANYA akan mentoggle baris yang BISA DIPILIH + const toggleSelectableRows = () => { + const shouldSelect = !allSelected; + selectableRows.forEach((row) => + row.toggleSelected(shouldSelect) + ); + }; - return ( -
+ return ( +
+ +
+ ); + }, + cell: ({ row }) => { + return ( - kandang.status == 'NON_ACTIVE' || - kandang.status == 'PENGAJUAN' - ).length == 0 || formType == 'detail' + formType == 'detail' || + (!initialKandangIdSet.includes(row.original.id) && + (row.original.status == 'ACTIVE' || + row.original.status == 'PENGAJUAN')) } + indeterminate={row.getIsSomeSelected()} + onChange={row.getToggleSelectedHandler()} /> -
- ); + ); + }, }, - cell: ({ row }) => { - return ( - - ); + { + accessorFn: (row) => row.name, + header: 'Kandang', }, - }, - { - accessorFn: (row) => row.name, - header: 'Kandang', - }, - { - accessorFn: (row) => row.status, - header: 'Status', - cell: (props) => { - return ( - { - switch (props.row.original.status) { - case 'ACTIVE': - return 'red'; - case 'PENGAJUAN': - return 'green'; - case 'NON_ACTIVE': - return 'blue'; - default: - return 'gray'; - } - })()} - content={props.row.original.status - .toLowerCase() - .replace(/_/g, ' ') - .replace(/\b\w/g, (char) => char.toUpperCase())} - /> - ); + { + accessorFn: (row) => row.status, + header: 'Status', + cell: (props) => { + return ( + { + switch (props.row.original.status) { + case 'ACTIVE': + return 'red'; + case 'PENGAJUAN': + return 'green'; + case 'NON_ACTIVE': + return 'blue'; + default: + return 'gray'; + } + })()} + content={props.row.original.status + .toLowerCase() + .replace(/_/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase())} + /> + ); + }, }, - }, - { - accessorFn: (row) => row.capacity, - header: 'Kapasitas', - }, - { - accessorFn: (row) => row.pic?.name, - header: 'Penanggung Jawab', - }, - ]} - className={{ - containerClassName: cn({ - 'mb-20': listKandang?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', - }} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - /> + { + accessorFn: (row) => row.capacity, + header: 'Kapasitas', + }, + { + accessorFn: (row) => row.pic?.name, + header: 'Penanggung Jawab', + }, + ]} + className={{ + containerClassName: cn({ + 'mb-20': listKandang?.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + /> + ); }; diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 68a24ef4..e3668237 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -22,7 +22,7 @@ import { UpdateRecordingFormSchema, } from './RecordingForm.schema'; import { useRecordingFormHandlers } from './useRecordingFormHandlers'; -import { ProjectFlockApi } from '@/services/api/production'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { isResponseSuccess } from '@/lib/api-helper'; import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; import useSWR from 'swr'; diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 5a15bb8b..79617a75 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -1,9 +1,4 @@ import { BaseApiService } from '@/services/api/base'; -import { - CreateProjectFlockPayload, - ProjectFlock, - UpdateProjectFlockPayload, -} from '@/types/api/production/project-flock'; import { CreateRecordingPayload, Recording, @@ -11,11 +6,6 @@ import { } from '@/types/api/production/recording'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -export const ProjectFlockApi = new BaseApiService< - ProjectFlock, - CreateProjectFlockPayload, - UpdateProjectFlockPayload ->('/production/project-flocks'); export const ProjectFlockKandangApi = new BaseApiService< ProjectFlockKandang, unknown, diff --git a/src/services/api/production/chickin.ts b/src/services/api/production/chickin.ts new file mode 100644 index 00000000..39eb2501 --- /dev/null +++ b/src/services/api/production/chickin.ts @@ -0,0 +1,43 @@ +import { + Chickin, + CreateChickinPayload, + UpdateChickinPayload, +} from '@/types/api/production/chickin'; +import { BaseApiService } from '../base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { httpClient } from '@/services/http/client'; + +export class ChickinService extends BaseApiService< + Chickin, + CreateChickinPayload, + UpdateChickinPayload +> { + constructor(basePath: string = '/production/chickins') { + super(basePath); + } + + /** + * Approve single marketing data + */ + async singleApproval( + id: number, + action: 'APPROVED' | 'REJECTED' + ): Promise | undefined> { + try { + const path = `${this.basePath}/approvals`; + return await httpClient>(path, { + method: 'POST', + body: { + action: action, + approvable_ids: [id], + notes: `${action} chickin ${id}`, + }, + }); + } catch (error) { + console.error('Error approve chickin:', error); + return undefined; + } + } +} + +export const ChickinApi = new ChickinService('/production/chickins');