diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c37bfd35..6028a8cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -127,6 +127,7 @@ build:dev: NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id' NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' + NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia' deploy:dev: <<: *deploy_template diff --git a/next.config.ts b/next.config.ts index c781a8ac..b2d25eb6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'export', images: { unoptimized: true }, + trailingSlash: true, }; export default nextConfig; diff --git a/src/app/marketing/page.tsx b/src/app/marketing/page.tsx index 99a80b64..c30ee501 100644 --- a/src/app/marketing/page.tsx +++ b/src/app/marketing/page.tsx @@ -7,4 +7,5 @@ const Marketing = () => { ); }; + export default Marketing; diff --git a/src/app/page.tsx b/src/app/page.tsx index db9638df..9cc0177d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,29 @@ -import { redirect } from 'next/navigation'; +'use client'; + +import { useEffect } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { useAuth } from '@/services/hooks/useAuth'; +import { redirectToSSO } from '@/lib/auth-helper'; export default function Home() { - redirect('/dashboard'); + const { user, isLoadingUser } = useAuth(); - return ( -
-

LTI ERP

-
- ); + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + if (pathname === '/') { + router.replace('/dashboard'); + } + }, [pathname]); + + if (isLoadingUser) { + return ( +
+ +
+ ); + } + + return <>Loading...; } diff --git a/src/app/production/recording/grading/add/page.tsx b/src/app/production/recording/grading/add/page.tsx deleted file mode 100644 index 9b918d98..00000000 --- a/src/app/production/recording/grading/add/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; -import { RecordingApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; - -const AddGrading = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const recordingId = searchParams.get('recording_id'); - - const { data: recording, isLoading: isLoadingRecording } = useSWR( - recordingId && recordingId !== 'new' ? [recordingId] : null, - ([id]) => RecordingApi.getSingle(parseInt(id)) - ); - - if ( - recordingId && - recordingId !== 'new' && - !isLoadingRecording && - (!recording || !isResponseSuccess(recording)) - ) { - router.replace('/404'); - return; - } - - return ( -
- {recordingId && recordingId !== 'new' && isLoadingRecording && ( - - )} - {(!recordingId || - recordingId === 'new' || - (!isLoadingRecording && recording && isResponseSuccess(recording))) && ( - - )} -
- ); -}; - -export default AddGrading; diff --git a/src/app/production/recording/grading/detail/edit/page.tsx b/src/app/production/recording/grading/detail/edit/page.tsx deleted file mode 100644 index 0a65f528..00000000 --- a/src/app/production/recording/grading/detail/edit/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; -import { RecordingApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; - -const EditGrading = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const recordingId = searchParams.get('recordingId'); - const gradingId = searchParams.get('gradingId'); - - const { data: recording, isLoading: isLoadingRecording } = useSWR( - recordingId ? [recordingId] : null, - ([id]) => RecordingApi.getSingle(parseInt(id)) - ); - - if (!recordingId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingRecording && ( - - )} - {!isLoadingRecording && recording && isResponseSuccess(recording) && ( - egg.id === parseInt(gradingId || '0') - )} - /> - )} -
- ); -}; - -export default EditGrading; diff --git a/src/app/production/recording/grading/detail/page.tsx b/src/app/production/recording/grading/detail/page.tsx deleted file mode 100644 index 6a5fbcba..00000000 --- a/src/app/production/recording/grading/detail/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; -import { RecordingApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; - -const DetailGrading = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const gradingId = searchParams.get('gradingId'); - - const { data: grading, isLoading: isLoadingGrading } = useSWR( - gradingId ? [gradingId] : null, - ([id]) => RecordingApi.getSingle(parseInt(id)) - ); - - if (!gradingId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingGrading && ( - - )} - {!isLoadingGrading && grading && isResponseSuccess(grading) && ( - egg.id === parseInt(gradingId) - )} - /> - )} -
- ); -}; - -export default DetailGrading; diff --git a/src/app/production/recording/grading/layout.tsx b/src/app/production/recording/grading/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/production/recording/grading/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 53853b96..65adf48c 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -15,7 +15,7 @@ interface RequireAuthProps { } const RequireAuth = ({ children }: RequireAuthProps) => { - const { setUser, setIsLoadingUser } = useAuth(); + const { user, setUser, setIsLoadingUser } = useAuth(); const { data: userResponse, @@ -37,12 +37,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { // Explicitly handle 401 redirect from the component level useEffect(() => { - if (userErrorResponse?.response?.status === 401) { + if ( + isResponseError(userResponse) && + userErrorResponse?.response?.status === 401 + ) { + // Clear cache to prevent stale data from rendering children + // mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate + setUser(undefined); redirectToSSO(); } - }, [userErrorResponse]); + }, [userErrorResponse, setUser, userResponse]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + useEffect(() => { + setIsLoadingUser(isLoadingUserResponse); + }, [isLoadingUserResponse]); + + if ( + (isLoadingUserResponse && !userResponse && !userErrorResponse) || + (!userResponse && !userErrorResponse) + ) { return (
@@ -68,7 +81,7 @@ const RequireAuth = ({ children }: RequireAuthProps) => { ); } - return <>{isResponseSuccess(userResponse) && children}; + return <>{isResponseSuccess(userResponse) && user && children}; }; export default RequireAuth; diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 4a413bc4..27b2d5c6 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -35,28 +35,22 @@ const RowOptionsMenu = ({ deleteClickHandler, approveClickHandler, rejectClickHandler, - isGradingCompleted, }: { type: 'dropdown' | 'collapse'; props: CellContext; deleteClickHandler: () => void; approveClickHandler: () => void; rejectClickHandler: () => void; - isGradingCompleted: (recording: Recording) => boolean; }) => { - const isLayingCategory = - props.row.original.project_flock_category === 'LAYING'; - const isRecordingApproved = (recording: Recording) => { return ( recording.approval?.action === 'APPROVED' && - recording.approval?.step_name === 'Disetujui' && - recording.approval?.step_number === 3 + recording.approval?.step_number === 2 && + recording.approval?.step_name === 'Disetujui' ); }; const isApproved = isRecordingApproved(props.row.original); - const isGradingDone = isGradingCompleted(props.row.original); return ( @@ -78,7 +72,7 @@ const RowOptionsMenu = ({ Edit - {!isApproved && !(isLayingCategory && !isGradingDone) && ( + {!isApproved && ( - {type === 'detail' && - !isRecordingApproved(initialValues) && - (!isLayingCategory || hasGradingData(initialValues)) && ( -
- + {type === 'detail' && !isRecordingApproved(initialValues) && ( +
+ - -
- )} + +
+ )}

@@ -1916,7 +1820,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {formik.values.body_weights?.map((bw, idx) => ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - + { {formik.values.stocks?.map((stock, idx) => ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - + { {formik.values.depletions?.map((depletion, idx) => ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - + { * + + Berat (gram) + + * + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( Action )} @@ -2597,7 +2510,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { (egg, idx) => ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - + { /> -
- -
+ + + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( @@ -2779,46 +2715,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Right side actions */}
- {type === 'detail' && isLayingCategory && ( - - - - )} - {type === 'edit' && (
- {isLayingCategory && ( - - - - )}
)}
@@ -2979,8 +2803,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Approve Confirmation Modal */} {(type as 'add' | 'edit' | 'detail') === 'detail' && - !isRecordingApproved(initialValues) && - (!isLayingCategory || hasGradingData(initialValues)) && ( + !isRecordingApproved(initialValues) && ( { {/* Reject Confirmation Modal */} {(type as 'add' | 'edit' | 'detail') === 'detail' && - !isRecordingApproved(initialValues) && - (!isLayingCategory || hasGradingData(initialValues)) && ( + !isRecordingApproved(initialValues) && ( { - // HOOKS & ROUTER - const router = useRouter(); - const searchParams = useSearchParams(); - const recordingId = searchParams.get('recording_id'); - - // STATE MANAGEMENT - const [selectedGradingItems, setSelectedGradingItems] = useState( - [] - ); - const [gradingFormErrorMessage, setGradingFormErrorMessage] = useState(''); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const deleteModal = useModal(); - - // API DATA FETCHING - const recordingUrl = useMemo(() => { - const recordingIdToUse = recordingId; - if (!recordingIdToUse) return null; - return `${RecordingApi.basePath}/${recordingIdToUse}`; - }, [recordingId]); - - const { data: recordingData } = useSWR( - recordingUrl, - recordingUrl ? RecordingApi.getAllFetcher : null - ); - - // DATA PROCESSING - const recording = - recordingData?.status === 'success' - ? (recordingData.data as unknown as Recording) - : undefined; - - const projectFlockKandangUrl = useMemo(() => { - if (!recording?.project_flock_kandang_id) return null; - return `${ProjectFlockKandangApi.basePath}/${recording.project_flock_kandang_id}`; - }, [recording?.project_flock_kandang_id]); - - const { data: projectFlockKandangData } = useSWR( - projectFlockKandangUrl, - projectFlockKandangUrl ? ProjectFlockKandangApi.getAllFetcher : null - ); - - const projectFlockKandang = - projectFlockKandangData?.status === 'success' - ? (projectFlockKandangData.data as unknown as ProjectFlockKandang) - : undefined; - - const konsumsiBaikEggData = useMemo(() => { - if (!recording?.eggs) return null; - - const konsumsiBaikEgg = recording.eggs.find((egg: RecordingEgg) => - egg.product_warehouse?.product?.name - ?.toLowerCase() - .includes('konsumsi baik') - ); - - return konsumsiBaikEgg || null; - }, [recording]); - - const totalKonsumsiBaikEggs = konsumsiBaikEggData?.qty || 0; - const konsumsiBaikEggId = konsumsiBaikEggData?.id; - - const isDataLoading = - !recording || - (totalKonsumsiBaikEggs === 0 && - recording?.project_flock_category === 'LAYING'); - - // FORM HANDLERS - const createGradingHandler = useCallback( - async (payload: CreateGradingPayload) => { - const res = (await RecordingApi.createGrading(payload)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setGradingFormErrorMessage(res?.message || 'Failed to add Grading'); - return; - } - - toast.success(res?.message || 'Successfully added Grading!'); - router.push('/production/recording'); - }, - [router] - ); - - const updateGradingHandler = useCallback( - async (gradingId: number, payload: UpdateGradingPayload) => { - const res = (await RecordingApi.updateGrading(gradingId, payload)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setGradingFormErrorMessage(res?.message || 'Failed to update Grading'); - return; - } - toast.success(res?.message || 'Successfully updated Grading!'); - router.refresh(); - router.push('/production/recording'); - }, - [router] - ); - - const deleteRecordingClickHandler = useCallback(() => { - deleteModal.openModal(); - }, [deleteModal]); - - const confirmationModalDeleteClickHandler = useCallback(async () => { - if (!initialValues?.id) return; - - setIsDeleteLoading(true); - try { - const res = (await RecordingApi.deleteGrading(initialValues.id)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setGradingFormErrorMessage(res?.message || 'Failed to delete Grading'); - return; - } - deleteModal.closeModal(); - toast.success(res?.message || 'Successfully delete Grading!'); - router.push('/production/recording'); - } catch { - setGradingFormErrorMessage('Failed to delete Grading'); - } finally { - setIsDeleteLoading(false); - } - }, [deleteModal, initialValues?.id, router]); - - // FORMIK SETUP - const formikInitialValues = useMemo(() => { - let recordingEggId: number | undefined = konsumsiBaikEggId; - - if (!recordingEggId && initialValues?.id) { - recordingEggId = initialValues.id; - } - - if (!recordingEggId) { - recordingEggId = parseInt(recordingId || '0') || 0; - } - - let gradingData: { - recording_egg_id: number; - grade: string; - qty: number; - }[] = []; - - if (initialValues?.grading_eggs && initialValues.grading_eggs.length > 0) { - gradingData = initialValues.grading_eggs.map((grading: GradingEgg) => ({ - recording_egg_id: recordingEggId, - grade: grading.grade, - qty: grading.qty, - })); - } else if (initialValues?.gradings && initialValues.gradings.length > 0) { - gradingData = initialValues.gradings.map( - (grading: { grade: string; qty: number }) => ({ - recording_egg_id: recordingEggId, - grade: grading.grade, - qty: grading.qty, - }) - ); - } - - return getRecordingGradingFormInitialValues({ - recording_egg_id: recordingEggId, - eggs_grading: gradingData, - }); - }, [initialValues, recordingId, konsumsiBaikEggId]); - - const formik = useFormik({ - initialValues: formikInitialValues, - enableReinitialize: true, - validationSchema: (() => { - return type === 'edit' - ? UpdateRecordingGradingFormSchema - : RecordingGradingFormSchema; - })(), - validateOnChange: true, - validateOnBlur: true, - onSubmit: async (values) => { - const gradingPayload = { - eggs_grading: (values.eggs_grading ?? []).map((grading) => ({ - recording_egg_id: grading.recording_egg_id, - grade: grading.grade, - qty: grading.qty || 0, - })), - }; - - switch (type) { - case 'add': - await createGradingHandler(gradingPayload as CreateGradingPayload); - break; - case 'edit': - await updateGradingHandler( - initialValues?.id as number, - gradingPayload as UpdateGradingPayload - ); - break; - } - }, - }); - - const currentGradingTotal = useMemo(() => { - return (formik.values.eggs_grading || []).reduce((total, grading) => { - return total + (Number(grading.qty) || 0); - }, 0); - }, [formik.values.eggs_grading]); - - const isGradingExceedsAvailable = currentGradingTotal > totalKonsumsiBaikEggs; - const isGradingIncomplete = - currentGradingTotal < totalKonsumsiBaikEggs && totalKonsumsiBaikEggs > 0; - const hasUserStartedGrading = currentGradingTotal > 0; - - // GRADING HANDLERS - const addGrading = () => { - let recordingEggId: number | undefined = konsumsiBaikEggId; - - if (!recordingEggId && initialValues?.id) { - recordingEggId = initialValues.id; - } - - if (!recordingEggId) { - recordingEggId = parseInt(recordingId || '0') || 0; - } - - const newGrading = [ - ...(formik.values.eggs_grading || []), - { - recording_egg_id: recordingEggId, - grade: '', - qty: '', - }, - ]; - formik.setFieldValue('eggs_grading', newGrading); - }; - - const handleGradingGradeChangeWrapper = useCallback( - (idx: number) => (selectedOption: OptionType | OptionType[] | null) => { - const option = selectedOption as OptionType | null; - formik.setFieldValue(`eggs_grading.${idx}.grade`, option?.label || ''); - }, - [formik] - ); - - const handleGradingQtyChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - formik.setFieldValue(`eggs_grading.${idx}.qty`, value); - }, - [formik] - ); - - const removeGrading = (idx: number) => { - const updatedGrading = formik.values.eggs_grading?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('eggs_grading', updatedGrading); - }; - - const removeSelectedGrading = () => { - const updatedGrading = formik.values.eggs_grading?.filter( - (_, idx) => !selectedGradingItems.includes(idx) - ); - formik.setFieldValue('eggs_grading', updatedGrading); - setSelectedGradingItems([]); - }; - - // VALIDATION HELPERS - const isRepeaterInputError = ( - arrayName: 'eggs_grading', - column: string, - idx: number - ) => { - const touched = formik.touched as Record; - const errors = formik.errors as Record; - - if (!touched[arrayName] || !Array.isArray(touched[arrayName])) { - return { - isError: false, - errorMessage: '', - }; - } - - const touchedField = (touched[arrayName] as unknown[])?.[idx] as Record< - string, - unknown - >; - const errorField = (errors[arrayName] as unknown[])?.[idx] as Record< - string, - unknown - >; - - return { - isError: touchedField && Boolean(errorField?.[column]), - errorMessage: - touchedField && errorField?.[column] - ? (errorField[column] as string) - : '', - }; - }; - - // EFFECTS - useEffect(() => { - if (isDataLoading) { - toast.dismiss('grading-exceeds'); - toast.dismiss('grading-incomplete'); - return; - } - - if (isGradingExceedsAvailable && currentGradingTotal > 0) { - toast.error( - `Total grading (${currentGradingTotal}) melebihi telur yang tersedia (${totalKonsumsiBaikEggs})!`, - { - id: 'grading-exceeds', - duration: 3000, - } - ); - toast.dismiss('grading-incomplete'); - } else if (isGradingIncomplete && hasUserStartedGrading) { - toast.error( - `Total grading (${currentGradingTotal}) tidak sama dengan total telur konsumsi baik yang tersedia (${totalKonsumsiBaikEggs})! Semua telur harus digrading.`, - { - id: 'grading-incomplete', - duration: 3000, - } - ); - toast.dismiss('grading-exceeds'); - } else { - toast.dismiss('grading-exceeds'); - toast.dismiss('grading-incomplete'); - } - }, [ - isDataLoading, - isGradingExceedsAvailable, - isGradingIncomplete, - hasUserStartedGrading, - currentGradingTotal, - totalKonsumsiBaikEggs, - ]); - - useEffect(() => { - if ( - konsumsiBaikEggId && - formik.values.eggs_grading && - formik.values.eggs_grading.length === 0 - ) { - formik.setFieldValue('eggs_grading', [ - { recording_egg_id: konsumsiBaikEggId, grade: '', qty: '' }, - ]); - } - }, [konsumsiBaikEggId, formik.values.eggs_grading.length]); - - return ( - <> -
-
- -

- {type === 'add' && 'Tambah Grading'} - {type === 'edit' && 'Edit Grading'} - {type === 'detail' && 'Detail Grading'} -

-
- -
- {/* Basic Info Card */} - -
- {/* Status Approval */} - {recording?.approval && ( -
- Status Approval -
- - {(() => { - const actionText = (() => { - switch (recording.approval.action) { - case 'APPROVED': - return 'Disetujui'; - case 'REJECTED': - return 'Ditolak'; - case 'CREATED': - return 'Dibuat'; - case 'UPDATED': - return 'Diperbarui'; - default: - return recording.approval.action; - } - })(); - - const stepName = recording.approval.step_name; - - if (stepName === actionText) { - return stepName; - } - - return `${stepName} - ${actionText}`; - })()} - -
-
- )} - {/* Recording Info */} -
- Lokasi -

- {projectFlockKandang?.project_flock?.location?.name || '-'} -

-
-
- Project Flock -

- {projectFlockKandang?.project_flock?.flock_name || '-'} -

-
-
- Kandang -

- {projectFlockKandang?.kandang?.name || '-'} -

-
-
- Tanggal Recording -

- {recording - ? formatDate(recording.record_datetime, 'DD MMMM YYYY') - : '-'} -

-
-
- Hari -

Hari ke-{recording?.day || '-'}

-
-
- Kategori -

- - {recording?.project_flock_category || '-'} - -

-
-
- Periode -

- - Periode {projectFlockKandang?.project_flock?.period || '-'} - -

-
-
- -
- {/* Additional Recording Info */} -
-
-
- -
- - Detail Recording - -
-
-
-

Area

-

- {projectFlockKandang?.project_flock?.area?.name || '-'} -

-
-
-

Status Kandang

-

- {projectFlockKandang?.kandang?.status || '-'} -

-
-
-
- - {/* Total Telur Konsumsi Baik Info */} -
-
-
-

- Total Telur Konsumsi Baik -

-
-

- {isDataLoading ? ( - - ) : ( - totalKonsumsiBaikEggs - )}{' '} - - telur - -

-
-
-
- -
-
- - {/* Progress Bar */} -
-
- Total yang digrading: - - {isDataLoading ? ( - - ) : ( - `${currentGradingTotal} / ${totalKonsumsiBaikEggs}` - )} - -
-
-
-
- {!isDataLoading && isGradingExceedsAvailable && ( -
- - Melebihi batas tersedia -
- )} - {!isDataLoading && - isGradingIncomplete && - hasUserStartedGrading && ( -
- - - Grading belum lengkap, semua telur harus digrading - -
- )} - {isDataLoading && ( -
- - Memuat data telur konsumsi baik... -
- )} -
-
-
- - - {/* Grading Table */} - -
- - - - {type !== 'detail' && ( - - )} - - - {type !== 'detail' && } - - - - {formik.values.eggs_grading?.map((grading, idx) => ( - - {type !== 'detail' && ( - - )} - - - {type !== 'detail' && ( - - )} - - ))} - -
- 0 - } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedGradingItems( - formik.values.eggs_grading?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedGradingItems([]); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - Grade - - * - - - Jumlah - - * - - Action
- - ) => { - if (e.target.checked) { - setSelectedGradingItems([ - ...selectedGradingItems, - idx, - ]); - } else { - setSelectedGradingItems( - selectedGradingItems.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - - - - -
- -
-
-
- {type !== 'detail' && ( -
- {selectedGradingItems.length > 0 && ( - - )} - -
- )} -
- - {/* Action buttons */} -
- {type !== 'add' && ( -
- {deleteRecordingClickHandler && ( - - )} - {type !== 'edit' && initialValues && ( - - )} -
- )} - {type !== 'detail' && ( -
- - -
- )} -
- {gradingFormErrorMessage && ( -
- - {gradingFormErrorMessage} -
- )} - -
- - {/* ===== MODALS ===== */} - {type !== 'add' && ( - <> - - - )} - - ); -}; - -export default GradingForm; diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index bf59d5ee..a77e1158 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -16,7 +16,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; -import { cn, formatDate, formatCurrency } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -136,14 +136,6 @@ const PurchaseTable = () => { ? formatDate(props.row.original.po_date, 'DD MMM YYYY') : '-', }, - { - accessorKey: 'due_date', - header: 'Jatuh Tempo', - cell: (props) => - props.row.original.due_date - ? formatDate(props.row.original.due_date, 'DD MMM YYYY') - : '-', - }, { header: 'Aging', cell: (props) => { @@ -156,11 +148,6 @@ const PurchaseTable = () => { return `${diffDays} hari`; }, }, - { - accessorKey: 'grand_total', - header: 'Total (Rp.)', - cell: (props) => formatCurrency(props.row.original.grand_total), - }, { header: 'Aksi', cell: (props) => { diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 79762da9..15106c5e 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -52,6 +52,8 @@ const PurchaseOrderAcceptApprovalForm = ({ const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; + // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, @@ -64,7 +66,6 @@ const PurchaseOrderAcceptApprovalForm = ({ | 'expedition_vendor_id' | 'received_qty' | 'transport_per_item' - | 'transport_total' ): { isError: boolean; errorMessage: string } => { const touchedItem = formik.touched.items?.[idx]; const errorItem = formik.errors.items?.[idx] as @@ -163,6 +164,7 @@ const PurchaseOrderAcceptApprovalForm = ({ validateOnBlur: true, onSubmit: async (values) => { const payload: CreateAcceptApprovalRequestPayload = { + action: 'APPROVED', notes: values.notes || '', items: values.items?.map((formItem) => { @@ -181,10 +183,6 @@ const PurchaseOrderAcceptApprovalForm = ({ typeof formItem.transport_per_item === 'string' ? parseFloat(formItem.transport_per_item) || 0 : formItem.transport_per_item || 0, - transport_total: - typeof formItem.transport_total === 'string' - ? parseFloat(formItem.transport_total) || 0 - : formItem.transport_total || 0, }; }) || [], }; @@ -239,9 +237,8 @@ const PurchaseOrderAcceptApprovalForm = ({ vehicle_number: item.vehicle_number || '', expedition_vendor: null, expedition_vendor_id: 0, - received_qty: '', + received_qty: item.total_qty || '', transport_per_item: '', - transport_total: '', }; }); formik.setFieldValue('items', updatedItems); @@ -301,7 +298,7 @@ const PurchaseOrderAcceptApprovalForm = ({ // ===== PURCHASE ITEM OPERATIONS ===== const handlePurchaseItemChange = ( idx: number, - field: 'received_qty' | 'transport_per_item' | 'transport_total', + field: 'received_qty' | 'transport_per_item', value: string | number ) => { const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; @@ -318,26 +315,6 @@ const PurchaseOrderAcceptApprovalForm = ({ : parseFloat( formik.values.items?.[idx]?.transport_per_item as string ) || 0; - - if (receivedQty > 0 && transportPerItem >= 0) { - const calculatedTransportTotal = receivedQty * transportPerItem; - formik.setFieldValue( - `items.${idx}.transport_total`, - calculatedTransportTotal - ); - } - } - - if (field === 'transport_total') { - const receivedQty = - parseFloat(formik.values.items?.[idx]?.received_qty as string) || 0; - if (receivedQty > 0 && numValue >= 0) { - const calculatedTransportPerItem = numValue / receivedQty; - formik.setFieldValue( - `items.${idx}.transport_per_item`, - calculatedTransportPerItem - ); - } } }; @@ -386,10 +363,6 @@ const PurchaseOrderAcceptApprovalForm = ({ Transport/Item * - - Total Transport - * - @@ -657,37 +630,6 @@ const PurchaseOrderAcceptApprovalForm = ({ }} /> - - - handlePurchaseItemChange( - idx, - 'transport_total', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan total transport' - allowNegative={false} - decimalScale={2} - thousandSeparator=',' - decimalSeparator='.' - inputPrefix={'Rp'} - isError={ - isRepeaterInputError(idx, 'transport_total').isError - } - errorMessage={ - isRepeaterInputError(idx, 'transport_total') - .errorMessage - } - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> - ); })} @@ -732,7 +674,8 @@ const PurchaseOrderAcceptApprovalForm = ({ disabled={ !formik.isValid || formik.isSubmitting || - hasQuantityExceededErrors + hasQuantityExceededErrors || + isRejected } > Submit diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 96836bc6..c7da956d 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -23,10 +23,12 @@ type PurchaseRequestStaffApprovalFormSchemaType = { }; type PurchaseRequestManagerApprovalFormSchemaType = { + action: 'APPROVED' | 'REJECTED'; notes: string | null; }; type PurchaseRequestAcceptApprovalFormSchemaType = { + action: 'APPROVED' | 'REJECTED'; notes: string | null; items: { purchase_item?: { @@ -45,7 +47,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = { expedition_vendor_id: number; received_qty: number | string; transport_per_item: number | string; - transport_total: number | string; }[]; }; @@ -83,7 +84,6 @@ export type PurchaseAcceptApprovalItemSchema = { expedition_vendor_id: number; received_qty: number | string; transport_per_item: number | string; - transport_total: number | string; }; export type PurchaseDeleteItemsSchema = { @@ -152,6 +152,10 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema = Yup.object({ + action: Yup.mixed<'APPROVED' | 'REJECTED'>() + .oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED') + .required('Action wajib diisi!') + .default('APPROVED'), notes: Yup.string().nullable().default(null), }); @@ -230,20 +234,6 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema() - .required('Total biaya transport wajib diisi!') - .test( - 'is-valid-transport-total', - 'Total biaya transport harus berupa angka lebih dari atau sama dengan 0!', - function (value) { - if (value === '' || value === null || value === undefined) - return false; - const numValue = - typeof value === 'string' ? parseFloat(value) : value; - return !isNaN(numValue) && numValue >= 0; - } - ) - .typeError('Total biaya transport harus berupa angka!'), }); export const PurchaseRequestStaffApprovalFormSchema: Yup.ObjectSchema = @@ -368,6 +358,7 @@ export const PurchaseRequestManagerApprovalFormDefaultValues = ( purchase?: Purchase ): PurchaseRequestManagerApprovalFormSchemaType => { return { + action: 'APPROVED', notes: purchase?.notes ?? null, }; }; @@ -378,6 +369,10 @@ export type PurchaseRequestManagerApprovalFormValues = Yup.InferType< export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema = Yup.object({ + action: Yup.mixed<'APPROVED' | 'REJECTED'>() + .oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED') + .required('Action wajib diisi!') + .default('APPROVED'), notes: Yup.string().nullable().default(null), items: Yup.array() .of(PurchaseAcceptApprovalItemObjectSchema) @@ -388,6 +383,7 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema { return { + action: 'APPROVED', notes: purchase?.notes ?? null, items: purchase?.items ? purchase.items.map((item) => ({ @@ -419,7 +415,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( expedition_vendor_id: 0, received_qty: '', transport_per_item: '', - transport_total: '', })) : [ { @@ -431,7 +426,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( expedition_vendor_id: 0, received_qty: '', transport_per_item: '', - transport_total: '', }, ], }; diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 791e2592..1fcd7a94 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -61,7 +61,7 @@ const PurchaseOrderStaffApprovalForm = ({ return 'add'; } - const currentStep = initialValues?.approval?.step_number || 1; + const currentStep = initialValues?.latest_approval?.step_number || 1; switch (currentStep) { case 1: @@ -77,7 +77,9 @@ const PurchaseOrderStaffApprovalForm = ({ // Step 4+ (Penerimaan Barang dan selesai), tidak boleh edit kalau sudah disetujui return 'edit'; } - }, [rawDataApprovals, propType, initialValues?.approval?.step_number]); + }, [rawDataApprovals, propType, initialValues?.latest_approval?.step_number]); + + const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; const router = useRouter(); const searchParams = useSearchParams(); @@ -93,16 +95,16 @@ const PurchaseOrderStaffApprovalForm = ({ // ===== UTILITY FUNCTIONS ===== const canUpdatePurchaseItems = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; return currentStep >= 3; - }, [initialValues?.approval]); + }, [initialValues?.latest_approval]); const canShowDeleteAddButtons = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; // Step 2 (Staff Purchase) dengan mode 'add' tidak boleh add/delete items // User hanya boleh input harga dan total harga untuk items yang sudah ada @@ -112,7 +114,7 @@ const PurchaseOrderStaffApprovalForm = ({ // Step 3 (Manager Purchase) boleh add/delete items return currentStep === 3; - }, [initialValues?.approval, type]); + }, [initialValues?.latest_approval, type]); const isRepeaterInputError = ( idx: number, @@ -312,7 +314,9 @@ const PurchaseOrderStaffApprovalForm = ({ const isNewItemForm = !formItem.purchase_item_id || formItem.purchase_item_id === 0; - let cleanPayload: UpdateStaffApprovalRequestPayload['items'][0]; + let cleanPayload: NonNullable< + UpdateStaffApprovalRequestPayload['items'] + >[0]; if (isNewItemForm) { cleanPayload = { @@ -360,7 +364,9 @@ const PurchaseOrderStaffApprovalForm = ({ const isNewItemForm = !formItem.purchase_item_id || formItem.purchase_item_id === 0; - let cleanPayload: UpdateStaffApprovalRequestPayload['items'][0]; + let cleanPayload: NonNullable< + UpdateStaffApprovalRequestPayload['items'] + >[0]; if (isNewItemForm) { cleanPayload = { @@ -719,7 +725,10 @@ const PurchaseOrderStaffApprovalForm = ({ 'min-w-52 md:min-w-72 lg:min-w-80', }} bottomLabel={ - 'Previous: ' + purchaseItem.product.name + type === 'edit' + ? 'Previous: ' + + purchaseItem.product.name + : undefined } /> @@ -819,7 +828,11 @@ const PurchaseOrderStaffApprovalForm = ({ thousandSeparator=',' decimalSeparator='.' inputPrefix={'Rp'} - bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}`} + bottomLabel={ + type === 'edit' + ? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}` + : undefined + } isError={ isRepeaterInputError( formItemIndex, @@ -857,7 +870,11 @@ const PurchaseOrderStaffApprovalForm = ({ thousandSeparator=',' decimalSeparator='.' inputPrefix={'Rp'} - bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}`} + bottomLabel={ + type === 'edit' + ? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}` + : undefined + } isError={ isRepeaterInputError( formItemIndex, @@ -1131,7 +1148,7 @@ const PurchaseOrderStaffApprovalForm = ({ color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={!formik.isValid || formik.isSubmitting || isRejected} > Submit diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts b/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts index 414371c3..67a694bc 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts @@ -78,14 +78,14 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema { - if (!initialValues?.approval) return null; - return initialValues.approval.step_number; - }, [initialValues?.approval]); + if (!initialValues?.latest_approval) return null; + return initialValues.latest_approval.step_number; + }, [initialValues?.latest_approval]); const { approvals, @@ -166,7 +169,7 @@ const PurchaseOrderDetail = ({ rawDataApprovals, refresh: refreshApprovals, } = useApprovalSteps({ - latestApproval: initialValues?.approval, + latestApproval: initialValues?.latest_approval, approvalLines: PURCHASE_ORDER_APPROVAL_LINE, moduleName: 'PURCHASES', moduleId: initialValues?.id?.toString() ?? '', @@ -177,19 +180,22 @@ const PurchaseOrderDetail = ({ }); const showApprovalButton = - approvalStep !== null && approvalStep >= 1 && approvalStep <= 3; + approvalStep !== null && + approvalStep >= 1 && + approvalStep <= 3 && + initialValues?.latest_approval?.action !== 'REJECTED'; const canDeleteItems = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; const hasReachedStep5 = rawDataApprovals?.some( (approval) => approval.step_number === 5 ); return currentStep === 3 && !hasReachedStep5; - }, [initialValues?.approval, rawDataApprovals]); + }, [initialValues?.latest_approval, rawDataApprovals]); const handleApprovalClick = () => { if (!approvalStep) return; @@ -216,24 +222,30 @@ const PurchaseOrderDetail = ({ case 1: staffRejectionModal.openModal(); break; + case 2: + managerRejectionModal.openModal(); + break; + case 3: + acceptRejectionModal.openModal(); + break; default: break; } }; const canShowPurchaseOrderInvoice = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; return currentStep >= 3; - }, [initialValues?.approval]); + }, [initialValues?.latest_approval]); const canShowPenerimaanBarang = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; return currentStep === 5; - }, [initialValues?.approval]); + }, [initialValues?.latest_approval]); const totalBeforeTax = useMemo(() => { return purchaseOrderItems.reduce( @@ -296,6 +308,33 @@ const PurchaseOrderDetail = ({ [initialValues?.id, searchParams, refetchData] ); + const createAcceptApprovalHandler = useCallback( + async (payload: CreateAcceptApprovalRequestPayload) => { + const purchaseRequestId = searchParams.get('purchaseId') + ? parseInt(searchParams.get('purchaseId')!) + : initialValues?.id || 1; + + if (!purchaseRequestId) { + toast.error('Purchase Request ID is required'); + return; + } + + const res = await PurchaseApi.acceptApproval.create( + purchaseRequestId, + payload + ); + + if (isResponseError(res)) { + toast.error(res.message); + return; + } + toast.success(res?.message as string); + refreshApprovals(); + refetchData?.(); + }, + [initialValues?.id, searchParams, refreshApprovals, refetchData] + ); + // ===== MODAL HANDLERS ===== const handleStaffApprovalModalClose = useCallback(() => { refreshApprovals(); @@ -544,11 +583,6 @@ const PurchaseOrderDetail = ({ accessorKey: 'transport_per_item', cell: (props) => formatCurrency(props.getValue() as number), }, - { - header: 'Transport Total', - accessorKey: 'transport_total', - cell: (props) => formatCurrency(props.getValue() as number), - }, ]; const summaryData = [ @@ -647,7 +681,7 @@ const PurchaseOrderDetail = ({
- + Area @@ -657,7 +691,7 @@ const PurchaseOrderDetail = ({
- + Lokasi @@ -671,7 +705,7 @@ const PurchaseOrderDetail = ({
- + Gudang @@ -685,7 +719,7 @@ const PurchaseOrderDetail = ({
- + Nama Vendor @@ -696,7 +730,7 @@ const PurchaseOrderDetail = ({
- + Kategori Vendor @@ -706,18 +740,7 @@ const PurchaseOrderDetail = ({
- - Tgl. Jatuh Tempo - - - : {formatDate(purchaseData.due_date, 'D MMM YYYY')} ( - {purchaseData.credit_term} hari) - -
-
-
-
- + Nomor @@ -727,7 +750,7 @@ const PurchaseOrderDetail = ({
- + Nomor PO
@@ -925,6 +948,7 @@ const PurchaseOrderDetail = ({ color: 'success', onClick: async (notes) => { const payload: CreateManagerApprovalRequestPayload = { + action: 'APPROVED', notes: notes || null, }; @@ -1028,7 +1052,6 @@ const PurchaseOrderDetail = ({ const payload: CreateStaffApprovalRequestPayload = { action: 'REJECTED', notes: notes || null, - items: [], }; await createStaffApprovalHandler(payload); @@ -1041,6 +1064,60 @@ const PurchaseOrderDetail = ({ }} /> + {/* Accept Rejection Modal */} + { + const payload: CreateAcceptApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + }; + + await createAcceptApprovalHandler(payload); + await refetchData?.(); + acceptRejectionModal.closeModal(); + }, + }} + secondaryButton={{ + text: 'Batal', + }} + /> + + {/* Manager Rejection Modal */} + { + const payload: CreateManagerApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + }; + + await createManagerApprovalHandler(payload); + await refetchData?.(); + managerRejectionModal.closeModal(); + }, + }} + secondaryButton={{ + text: 'Batal', + }} + /> + {/* Delete Confirmation Modal */} { const lastRedirect = sessionStorage.getItem('auth_redirect_timestamp'); const now = Date.now(); - // Loop protection: allow redirect only if last one was > 2 seconds ago + // Loop protection: allow redirect only if last one was > 5 seconds ago // or if no redirect has happened yet. - if (!lastRedirect || now - parseInt(lastRedirect, 10) > 2000) { + if (!lastRedirect || now - parseInt(lastRedirect, 10) > 5000) { sessionStorage.setItem('auth_redirect_timestamp', now.toString()); // const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 110a881c..8e66d57e 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -9,8 +9,6 @@ import { CreateRecordingPayload, Recording, UpdateRecordingPayload, - CreateGradingPayload, - UpdateGradingPayload, NextDayRecording, } from '@/types/api/production/recording'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; @@ -64,28 +62,6 @@ export class RecordingService extends BaseApiService< }); } - async createGrading( - payload: CreateGradingPayload - ): Promise | undefined> { - return await this.customRequest>('gradings', { - method: 'POST', - payload, - }); - } - - async updateGrading( - gradingId: number, - payload: UpdateGradingPayload - ): Promise | undefined> { - return await this.customRequest>( - `gradings/${gradingId}`, - { - method: 'PUT', - payload, - } - ); - } - async deleteGrading( gradingId: number ): Promise | undefined> { diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index e7b28f47..9bed7685 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -9,8 +9,7 @@ export type ProductionMetrics = { cum_intake: number; fcr_value: number; total_chick_qty: number; - daily_depletion_rate?: number; - cum_depletion?: number; + cum_depletion: number; }; export type BaseRecording = { @@ -18,42 +17,33 @@ export type BaseRecording = { project_flock_kandang_id: number; record_datetime: string; day: number; - created_by: User; + project_flock_category?: 'GROWING' | 'LAYING'; } & ProductionMetrics; export type RecordingBW = { - id: number; - recording_id: number; avg_weight: number; qty: number; total_weight: number; }; export type RecordingDepletion = { - id: number; - recording_id: number; product_warehouse_id: number; qty: number; product_warehouse: ProductWarehouse; }; export type RecordingStock = { - id: number; - recording_id: number; product_warehouse_id: number; usage_amount?: number; - usage_qty: number; - qty: number; pending_qty: number; product_warehouse: ProductWarehouse; }; export type RecordingEgg = { id: number; - recording_id: number; product_warehouse_id: number; qty: number; - created_by: User; + weight: number; product_warehouse: ProductWarehouse; gradings?: { grade: string; @@ -71,19 +61,12 @@ export type GradingEgg = { export type Recording = BaseMetadata & BaseRecording & { - project_flock_category?: 'GROWING' | 'LAYING'; approval?: BaseApproval; - egg_grading_status?: string | null; - egg_grading_pending_qty?: number | null; - egg_grading_completed_qty?: number | null; + created_user: User; body_weights?: RecordingBW[]; depletions?: RecordingDepletion[]; stocks?: RecordingStock[]; eggs?: RecordingEgg[]; - recording_bws?: RecordingBW[]; - recording_depletions?: RecordingDepletion[]; - recording_stocks?: RecordingStock[]; - recording_eggs?: RecordingEgg[]; grading_eggs?: GradingEgg[]; }; @@ -108,27 +91,10 @@ export type CreateGrowingRecordingPayload = { }[]; }; -export type CreateGradingPayload = { - eggs_grading: { - recording_egg_id: number; - grade: string; - qty: number; - }[]; -}; - -export type UpdateGradingPayload = CreateGradingPayload; - -export type CreateGradingRecordingPayload = { - eggs_grading: { - recording_egg_id: number; - grade: string; - qty: number; - }[]; -}; - export type CreateEggPayload = { product_warehouse_id: number; qty: number; + weight: number; }; export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { @@ -137,11 +103,9 @@ export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { export type CreateRecordingPayload = | CreateGrowingRecordingPayload - | CreateLayingRecordingPayload - | CreateGradingRecordingPayload; + | CreateLayingRecordingPayload; export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload; export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload; -export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload; diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 56cbd810..e4de565b 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -42,7 +42,6 @@ export type PurchaseItem = { expedition_vendor_name?: string | null; received_qty?: number | null; transport_per_item?: number | null; - transport_total?: number | null; }; export type BasePurchase = { @@ -52,9 +51,8 @@ export type BasePurchase = { po_document_path?: string | null; po_date: string; supplier: Supplier; - credit_term: number; + credit_term?: number; due_date: string; - grand_total: number; notes?: string | null; deleted_at?: string | null; created_by: number; @@ -62,7 +60,7 @@ export type BasePurchase = { location?: Location; warehouse?: Warehouse; items?: PurchaseItem[]; - approval?: BaseApproval; + latest_approval?: BaseApproval; }; export type Purchase = BaseMetadata & BasePurchase; @@ -71,7 +69,7 @@ export type CreatePurchaseRequestPayload = { supplier_id: number; credit_term: number; notes?: string | null; - items: { + items?: { warehouse_id: number; product_id: number; qty: number; @@ -81,7 +79,7 @@ export type CreatePurchaseRequestPayload = { export type CreateStaffApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: { + items?: { purchase_item_id: number; qty: number; price: number; @@ -92,7 +90,7 @@ export type CreateStaffApprovalRequestPayload = { export type UpdateStaffApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: Array<{ + items?: Array<{ purchase_item_id?: number; product_id?: number; warehouse_id?: number; @@ -103,12 +101,14 @@ export type UpdateStaffApprovalRequestPayload = { }; export type CreateManagerApprovalRequestPayload = { + action: 'APPROVED' | 'REJECTED'; notes?: string | null; }; export type CreateAcceptApprovalRequestPayload = { - notes?: string; - items: { + action: 'APPROVED' | 'REJECTED'; + notes?: string | null; + items?: { purchase_item_id: number; received_date: string; travel_number: string; @@ -117,7 +117,6 @@ export type CreateAcceptApprovalRequestPayload = { expedition_vendor_id: number; received_qty: number; transport_per_item: number; - transport_total: number; }[]; };