diff --git a/src/components/input/FileInput.tsx b/src/components/input/FileInput.tsx index aee7cb78..5f3a8610 100644 --- a/src/components/input/FileInput.tsx +++ b/src/components/input/FileInput.tsx @@ -33,6 +33,7 @@ const FileInput = ({ isError, errorMessage, disabled = false, + required = false, onChange, onBlur, readOnly = false, @@ -56,6 +57,13 @@ const FileInput = ({ )} > {label} + {required && ( + <> + + * + + > + )} )} diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 2b9086e0..657c5e5c 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -140,17 +140,17 @@ const ExpenseRequestContent = ({ const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - try { - await ExpenseApi.delete(initialValues?.id as number); + const deleteResponse = await ExpenseApi.delete(initialValues?.id as number); + if (isResponseSuccess(deleteResponse)) { toast.success('Berhasil menghapus data biaya operasional!'); router.push('/expense'); - } catch (error) { + } else { toast.error('Gagal menghapus data biaya operasional!'); - } finally { - deleteModal.closeModal(); - setIsDeleteLoading(false); } + + deleteModal.closeModal(); + setIsDeleteLoading(false); }; const confirmationModalCompleteClickHandler = async () => { diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index 3a84f6bc..a70b6454 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -21,7 +21,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { switch (latestApprovalStepNumber) { case 1: - expenseStatusPillBadgeColor = 'yellow'; + expenseStatusPillBadgeColor = 'gray'; break; case 2: @@ -33,7 +33,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { break; case 4: - expenseStatusPillBadgeColor = 'red'; + expenseStatusPillBadgeColor = 'yellow'; break; case 5: diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 9ae3ed34..1f3e9df5 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -420,11 +420,19 @@ const ExpensesTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ExpenseApi.delete(selectedExpense?.id as number); - refreshExpenses(); + const deleteResponse = await ExpenseApi.delete( + selectedExpense?.id as number + ); + + if (isResponseSuccess(deleteResponse)) { + refreshExpenses(); + deleteModal.closeModal(); + toast.success('Berhasil menghapus biaya operasional!'); + } else { + deleteModal.closeModal(); + toast.error('Gagal menghapus biaya operasional!'); + } - deleteModal.closeModal(); - toast.success('Berhasil menghapus biaya operasional!'); setIsDeleteLoading(false); }; diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 5ea844ab..9b2c8557 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup'; -import { Movement } from '@/types/api/inventory/movement'; +import { Movement, MovementDocument } from '@/types/api/inventory/movement'; type MovementFormSchemaType = { transfer_reason: string; @@ -29,7 +29,7 @@ type MovementFormSchemaType = { deliveries: { delivery_cost?: number | string; delivery_cost_per_item?: number | string; - document?: File | string | null; + document?: File | MovementDocument | null; document_path?: string | null; driver_name: string; vehicle_plate: string; @@ -61,7 +61,7 @@ export type ProductSchema = { export type DeliverySchema = { delivery_cost?: number | string; delivery_cost_per_item?: number | string; - document?: File | string | null; + document?: File | MovementDocument | null; document_path?: string | null; driver_name: string; vehicle_plate: string; @@ -129,13 +129,12 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ }), document_path: Yup.string().optional(), document_index: Yup.number().optional(), - document: Yup.mixed() + document: Yup.mixed() .nullable() .test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { if (!value) return true; - if (typeof value === 'string') return true; if (value instanceof File) return value.size <= 2 * 1024 * 1024; - return false; + return true; }), driver_name: Yup.string().required('Nama sopir wajib diisi!'), vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'), @@ -241,7 +240,7 @@ export const getMovementFormInitialValues = ( delivery_cost: d.shipping_cost_total ?? undefined, delivery_cost_per_item: d.shipping_cost_item ?? undefined, document_number: d.document_number ?? '', - document: d.document_path ?? null, + document: d.document ?? null, document_path: d.document_path ?? null, driver_name: d.driver_name ?? '', vehicle_plate: d.vehicle_plate ?? '', diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index ae9b68d9..d92b14de 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -35,6 +35,7 @@ import FileInput from '@/components/input/FileInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import Badge from '@/components/Badge'; import Card from '@/components/Card'; +import { S3_PUBLIC_BASE_URL } from '@/config/constant'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -55,16 +56,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { // ===== FORM HANDLERS ===== const createMovementHandler = useCallback( - async (payload: CreateMovementPayload, documents: File[] = []) => { - const formData = new FormData(); - formData.append('data', JSON.stringify(payload)); - documents.forEach((file, index) => { - formData.append(`documents[${index}]`, file); - }); - - const res = await MovementApi.create( - formData as unknown as CreateMovementPayload - ); + async (payload: CreateMovementPayload) => { + const res = await MovementApi.createMovement(payload); if (isResponseError(res)) { setMovementFormErrorMessage(res.message); return; @@ -218,20 +211,23 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }); const payload: CreateMovementPayload = { - transfer_reason: values.transfer_reason, - transfer_date: values.transfer_date, - source_warehouse_id: values.source_warehouse_id, - destination_warehouse_id: values.destination_warehouse_id, - products: values.products.map((p) => ({ - product_id: p.product_id, - product_qty: parseInt(p.product_qty.toString()) || 0, - })), - deliveries: deliveriesPayload, + data: { + transfer_reason: values.transfer_reason, + transfer_date: values.transfer_date, + source_warehouse_id: values.source_warehouse_id, + destination_warehouse_id: values.destination_warehouse_id, + products: values.products.map((p) => ({ + product_id: p.product_id, + product_qty: parseInt(p.product_qty.toString()) || 0, + })), + deliveries: deliveriesPayload, + }, + documents: documents.length > 0 ? documents : undefined, }; switch (type) { case 'add': - await createMovementHandler(payload, documents); + await createMovementHandler(payload); break; } }, @@ -1537,31 +1533,58 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type === 'detail' ? ( <> - - {delivery.document_path ? ( - <> - - Lihat Dokumen - > - ) : ( - '-' - )} - + {delivery.document_path ? ( + + + Lihat Dokumen + + ) : delivery.document && + delivery.document instanceof File === false ? ( + + + + {delivery.document.name} + + + ) : ( + + + Tidak ada dokumen + + )} > ) : ( { const file = e.target.files?.[0]; diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index e5bd30cb..39b17ef7 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -872,7 +872,7 @@ const RecordingTable = () => { 'mb-20': isResponseSuccess(recordings) && recordings?.data?.length === 0, }), - tableWrapperClassName: 'overflow-x-auto min-h-full overflow-visible!', + 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: diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 1b58b16c..77d1608d 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -1,91 +1,93 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Card from '@/components/Card'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; +import { + UniformityDetailItem, + Uniformity, +} from '@/types/api/production/uniformity'; -interface BarChartData { - name: string; - uv: number; +interface UniformityChartProps { + uniformityData?: Uniformity | null; + uniformityDetails?: UniformityDetailItem[]; } -interface GaugeChartData { - value: number; - label: string; - kandang?: string; - week?: string; - currentValue?: number; - totalValue?: number; -} - -const UniformityChart = () => { - // TODO: Replace with actual API call - const barChartData: BarChartData[] = [ - { - name: '48-52', - uv: 80, - }, - { - name: '52-56', - uv: 120, - }, - { - name: '56-60', - uv: 160, - }, - { - name: '60-64', - uv: 200, - }, - { - name: '64-68', - uv: 160, - }, - { - name: '68-72', - uv: 120, - }, - { - name: '72-76', - uv: 80, - }, - { - name: '76-80', - uv: 120, - }, - { - name: '84-88', - uv: 160, - }, - { - name: '88-92', - uv: 200, - }, - { - name: '92-96', - uv: 160, - }, +const UniformityChart = ({ + uniformityData, + uniformityDetails, +}: UniformityChartProps) => { + const defaultUniformityDetails: UniformityDetailItem[] = [ + { id: 1, weight: 61, range: 'Ideal' }, + { id: 2, weight: 62, range: 'Ideal' }, + { id: 3, weight: 63, range: 'Ideal' }, + { id: 4, weight: 64, range: 'Ideal' }, + { id: 5, weight: 65, range: 'Ideal' }, + { id: 6, weight: 66, range: 'Ideal' }, + { id: 7, weight: 67, range: 'Ideal' }, ]; - // TODO: Replace with actual API call - // const gaugeChartData: GaugeChartData = { - // value: 0, - // label: '', - // kandang: 'Kandang Cirangga', - // week: 'Week 2', - // currentValue: 512, - // totalValue: 1024, - // }; + const detailsToUse = uniformityDetails || defaultUniformityDetails; - const gaugeChartData: GaugeChartData = { - value: 52, - label: 'Uniformity', - kandang: 'Kandang Cirangga', - week: 'Week 2', - currentValue: 512, - totalValue: 1024, - }; + const barChartData = useMemo(() => { + if (!uniformityData) { + return []; + } + + if (!detailsToUse || detailsToUse.length === 0) { + return []; + } + + const weights = detailsToUse.map((d) => d.weight); + const minWeight = Math.floor(Math.min(...weights) / 5) * 5; + const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5; + + const rangeSize = maxWeight - minWeight < 11 ? 4 : 5; + const ranges: string[] = []; + + for (let start = minWeight; start <= maxWeight; start += rangeSize) { + const end = start + rangeSize; + ranges.push(`${start}-${end}`); + } + + const totalIdealCount = detailsToUse.filter( + (d) => d.range === 'Ideal' + ).length; + + return ranges.map((range) => { + const [minStr, maxStr] = range.split('-').map(Number); + const min = minStr; + const max = maxStr; + + const birdsInRange = detailsToUse.filter( + (d) => d.weight >= min && d.weight < max + ).length; + + const hasIdeal = detailsToUse.some( + (d) => d.range === 'Ideal' && d.weight >= min && d.weight < max + ); + + return { + name: range, + uv: birdsInRange, + isIdeal: hasIdeal, + idealCount: hasIdeal ? totalIdealCount : undefined, + }; + }); + }, [uniformityData, detailsToUse]); + + const gaugeChartData = useMemo(() => { + if (!uniformityData) return undefined; + + return { + value: uniformityData.uniformity, + label: 'Uniformity', + week: `Week ${uniformityData.week}`, + currentValue: uniformityData.uniform_qty, + totalValue: uniformityData.chick_qty_of_weight, + }; + }, [uniformityData]); return ( @@ -98,14 +100,14 @@ const UniformityChart = () => { }} > - {barChartData.length === 0 ? ( + {!uniformityData || barChartData.length === 0 ? ( ) : ( )} - {gaugeChartData.value === 0 ? ( + {!uniformityData || !gaugeChartData ? ( { - !isOpen && router.push('/production/uniformity')} - /> + { - // Uniformity data is never locked - checkbox is always enabled - return false; -}; - -const canApproveRejectUniformity = (uniformity: Uniformity): boolean => { - return uniformity.status === 'CREATED' || uniformity.status === 'Pengajuan'; -}; - -interface UniformityPreviewData { - id: string; - label: string; - value: string; -} - const UniformityConfirmationPreview = ({ uniformity, + uniformityDetail, }: { uniformity?: Uniformity; + uniformityDetail?: UniformityDetail; }) => { - const data: UniformityPreviewData[] = [ + const data: DetailOptionType[] = [ { id: 'tanggal', label: 'Tanggal', value: uniformity ? formatDate(uniformity.applied_at, 'DD MMM YYYY') - : '-', + : uniformityDetail + ? formatDate(uniformityDetail.info_umum.tanggal, 'DD MMM YYYY') + : '-', }, { id: 'lokasi-farm', label: 'Lokasi Farm', - value: uniformity?.location_name || '-', + value: + uniformity?.location_name || + uniformityDetail?.info_umum?.lokasi_farm || + '-', }, { id: 'project-flock', label: 'Project Flock', - value: uniformity?.flock_name || '-', + value: + uniformity?.flock_name || + uniformityDetail?.info_umum?.project_flock || + '-', }, { id: 'kandang', label: 'Kandang', - value: uniformity?.kandang_name || '-', + value: + uniformity?.kandang_name || uniformityDetail?.info_umum?.kandang || '-', }, { id: 'file-uniformity', label: 'File Uniformity', - value: '-', // File name tidak tersedia di GET ALL response + value: + uniformity?.file_name || uniformityDetail?.info_umum?.file_name || '-', }, { id: 'status', label: 'Status', - value: uniformity?.status || '-', + value: uniformity?.status || (uniformityDetail ? 'CREATED' : '-'), }, ]; - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { accessorKey: 'label', header: 'Label', @@ -148,7 +149,52 @@ const UniformityConfirmationPreview = ({ ); }; -const UniformityTable = ({ refresh }: { refresh?: () => void }) => { +const UniformityChartWrapper = ({ + uniformitySwrKey, +}: { + uniformitySwrKey: string; +}) => { + const { data: uniformities } = useSWR( + uniformitySwrKey, + UniformityApi.getAllFetcher + ); + + const uniformityData = useMemo(() => { + if (isResponseSuccess(uniformities) && uniformities?.data?.length > 0) { + return uniformities.data[0]; + } + return null; + }, [uniformities]); + + const shouldFetchDetails = !!uniformityData; + const uniformityDetailSwrKey = useMemo(() => { + if (!uniformityData) return null; + return `${UniformityApi.basePath}/${uniformityData.id}?with_details=true`; + }, [uniformityData]); + + const { data: uniformityDetailResponse } = useSWR( + uniformityDetailSwrKey, + shouldFetchDetails ? UniformityApi.getAllFetcher : null + ); + + const uniformityDetails = useMemo(() => { + if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { + const detailData = + uniformityDetailResponse.data as unknown as UniformityDetail; + return detailData.uniformity_details; + } + return undefined; + }, [shouldFetchDetails, uniformityDetailResponse]); + + return ( + + ); +}; + +const UniformityTable = () => { const router = useRouter(); const searchParams = useSearchParams(); const isSuccess = useUniformityStore((s) => s.isSuccess); @@ -355,6 +401,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { mutate: refreshUniformities, } = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher); + useEffect(() => { + if (isSuccess) { + refreshUniformities(); + } + }, [isSuccess, refreshUniformities]); + // ===== FILTER HANDLERS ===== const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { @@ -408,9 +460,15 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const canApproveReject = useMemo(() => { return ( selectedUniformities.length > 0 && - selectedUniformities.every( - (u) => u.status === 'CREATED' || u.status === 'Pengajuan' - ) + selectedUniformities.every((u) => { + const approvalAction = u.latest_approval?.action; + return ( + approvalAction === 'CREATED' || + approvalAction === 'Pengajuan' || + (!approvalAction && + (u.status === 'CREATED' || u.status === 'Pengajuan')) + ); + }) ); }, [selectedUniformities]); @@ -765,7 +823,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { accessorKey: 'status', header: 'Status', cell: (props) => { - const status = props.row.original.status; + const uniformity = props.row.original; + const status = + uniformity.latest_approval?.action ?? uniformity.status; return ( void }) => { - + void }) => { {createdUniformity ? ( ) : selectedRowIds.length === 1 ? ( + Uniformity 2025 + + + + {chartData.idealCount} of Birds + + {labelStr} + + + ); + } + return ( Uniformity 2025 @@ -105,9 +126,6 @@ const UniformityBarChart: React.FC = ({ data }) => { = ({ data }) => { radius={[25, 25, 0, 0]} /> } - /> + > + {data.map((entry, index) => ( + + ))} + ); diff --git a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx index eda3d0ab..04fbef9c 100644 --- a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx @@ -1,25 +1,29 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; import Card from '@/components/Card'; -import { Icon } from '@iconify/react'; import { formatNumber } from '@/lib/helper'; +import { Icon } from '@iconify/react'; interface UniformityGaugeChartProps { value: number; label: string; - kandang?: string; week?: string; currentValue?: number; totalValue?: number; + onWeekChange?: (direction: 'prev' | 'next') => void; + hasPrevWeek?: boolean; + hasNextWeek?: boolean; } const UniformityGaugeChart: React.FC = ({ value, label, - kandang, week, currentValue, totalValue, + onWeekChange, + hasPrevWeek = false, + hasNextWeek = false, }) => { const numberOfSegments = 50; const filledSegments = Math.round((value / 100) * numberOfSegments); @@ -34,7 +38,7 @@ const UniformityGaugeChart: React.FC = ({ const inactiveColor = '#f0f0f0'; return ( - + @@ -73,34 +77,49 @@ const UniformityGaugeChart: React.FC = ({ - - - - - - - - {kandang} - • - - {week} - + + onWeekChange?.('prev')} + disabled={!hasPrevWeek} + className='p-2 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shadow-sm' + aria-label='Previous week' + > + + + + + + + + {week} + + + + + {formatNumber(currentValue ?? 0)} + + From + + {formatNumber(totalValue ?? 0)} + + - - - {formatNumber(currentValue ?? 0)} - - From - {formatNumber(totalValue ?? 0)} - - - - + + + onWeekChange?.('next')} + disabled={!hasNextWeek} + className='p-2 rounded-lg border border-gray-200 bg-white hover:bg-gray-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors shadow-sm' + aria-label='Next week' + > + + + ); }; diff --git a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx index 0cc39d9a..4d0cd887 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -119,11 +119,13 @@ const UniformityDetail: React.FC = ({ const statusValue = latest_approval?.action ?? '-'; const valueMap: Record = { - tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'), - 'lokasi-farm': info_umum.lokasi_farm, - 'project-flock': info_umum.project_flock, - kandang: info_umum.kandang, - 'document-name': info_umum.file_name, + tanggal: info_umum?.tanggal + ? formatDate(info_umum.tanggal, 'DD MMMM YYYY') + : '-', + 'lokasi-farm': info_umum?.lokasi_farm ?? '-', + 'project-flock': info_umum?.project_flock ?? '-', + kandang: info_umum?.kandang ?? '-', + 'document-name': info_umum?.file_name ?? '-', 'approval-status': statusValue, }; diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx index d98bba23..21be03d7 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -229,49 +229,52 @@ const UniformityDetailsPreview = ({ {/* Form Section */} - {uniformity_details && uniformity_details.length > 0 ? ( + {info_umum || sampling || result ? ( {/* Sampling and Range */} - - Sampling and Range - - data={samplingTableData} - columns={columnsSampling} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> - - {/* Result */} - - Result - - data={resultTableData} - columns={resultColumns} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> - + {sampling && ( + + Sampling and Range + + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + )} - {/* Body Weight Details Button */} - - - {isLoading ? 'Loading...' : 'Show Body Weight Details'} - - - {/*{!uniformity_details || uniformity_details.length === 0 ? ( - <>> - ) : null}*/} + {/* Result */} + {result && ( + + Result + + data={resultTableData} + columns={resultColumns} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + )} + + {!uniformity_details || uniformity_details.length === 0 ? ( + + + {isLoading ? 'Loading...' : 'Show Body Weight Details'} + + + ) : null} {/* Body Weight Details */} {uniformity_details && uniformity_details.length > 0 && ( diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 2daac10d..bbca72f8 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -97,12 +97,16 @@ const UniformityForm = ({ setInputValue: setLocationSelectInputValue, options: locationOptions, isLoadingOptions: isLoadingLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', + }); // ===== FETCH PROJECT FLOCKS DATA ===== const projectFlocksUrl = useMemo(() => { const params = new URLSearchParams({ search: projectFlockSearchValue || '', + page: '1', limit: '100', }); if (selectedLocation) { @@ -141,6 +145,7 @@ const UniformityForm = ({ const approvedProjectFlockKandangsUrl = useMemo(() => { const params = new URLSearchParams({ step_name: 'Disetujui', + page: '1', limit: '100', }); return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; diff --git a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 02ec70c1..436fab9a 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -28,7 +28,7 @@ const UniformityGaugeChartSkeleton: React.FC< const inactiveColor = '#f0f0f0'; return ( - + diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index d6ef5952..d39b2e1a 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -52,9 +52,14 @@ const PurchaseOrderAcceptApprovalForm = ({ const searchParams = useSearchParams(); const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const [key, setKey] = useState(0); const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; + useEffect(() => { + setKey((prev) => prev + 1); + }, [initialValues?.id]); + // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, @@ -164,6 +169,7 @@ const PurchaseOrderAcceptApprovalForm = ({ validationSchema: PurchaseRequestAcceptApprovalFormSchema, validateOnChange: true, validateOnBlur: true, + enableReinitialize: false, onSubmit: async (values) => { const payload: CreateAcceptApprovalRequestPayload = { action: 'APPROVED', @@ -238,7 +244,12 @@ const PurchaseOrderAcceptApprovalForm = ({ travel_number: item.travel_number || '', travel_document_path: item.travel_document_path || '', vehicle_number: item.vehicle_number || '', - expedition_vendor: null, + expedition_vendor: item.expedition_vendor + ? { + value: item.expedition_vendor.id, + label: item.expedition_vendor.name, + } + : null, expedition_vendor_id: item.expedition_vendor_id || 0, received_qty: item.total_qty || '', transport_per_item: item.transport_per_item || '', @@ -246,7 +257,7 @@ const PurchaseOrderAcceptApprovalForm = ({ }); formik.setFieldValue('items', updatedItems); } - }, [purchaseItems, initialValues]); + }, [purchaseItems, initialValues, key]); useEffect(() => { if ( @@ -336,7 +347,11 @@ const PurchaseOrderAcceptApprovalForm = ({ }; return ( - + {type === 'add' @@ -674,6 +689,16 @@ const PurchaseOrderAcceptApprovalForm = ({ accept='.pdf,.jpg,.jpeg,.png' onChange={(e) => { const files = Array.from(e.target.files || []); + const invalidFiles = files.filter( + (file) => file.size > 2 * 1024 * 1024 + ); + + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 2 MB!'); + e.target.value = ''; + return; + } + formik.setFieldValue('travel_documents', files); }} onBlur={formik.handleBlur} @@ -699,7 +724,9 @@ const PurchaseOrderAcceptApprovalForm = ({ color='warning' className='px-4' onClick={() => { - formik.resetForm(); + if (type === 'add') { + formik.resetForm(); + } setPurchaseOrderFormErrorMessage(''); onCancel?.(); onModalClose?.(); diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index bb70053f..07a868a3 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -312,7 +312,8 @@ export const PurchaseRequestStaffApprovalFormInitialValues: PurchaseRequestStaff }; export const PurchaseRequestStaffApprovalFormDefaultValues = ( - purchase?: Purchase + purchase?: Purchase, + type?: 'add' | 'edit' ): PurchaseRequestStaffApprovalFormSchemaType => { return { action: 'APPROVED', @@ -331,8 +332,18 @@ export const PurchaseRequestStaffApprovalFormDefaultValues = ( label: item.warehouse?.name || '', }, qty: item.sub_qty || item.qty || 0, - price: item.price, - total_price: item.total_price, + price: + type === 'add' + ? 'ProductPrice' in item.product + ? item.product.ProductPrice || item.price || '' + : item.price + : item.price, + total_price: + type === 'add' + ? ('ProductPrice' in item.product + ? item.product.ProductPrice || item.price || 0 + : item.price) * (item.sub_qty || item.qty || 0) + : item.total_price, })) : [ { @@ -381,7 +392,15 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema().required()) + .of( + Yup.mixed() + .required('Dokumen surat jalan wajib diupload!') + .test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { + if (!value) return true; + if (value instanceof File) return value.size <= 2 * 1024 * 1024; + return true; + }) + ) .required('Dokumen surat jalan wajib diupload!') .min(1, 'Minimal upload 1 dokumen surat jalan!') .typeError('Dokumen surat jalan wajib diupload!'), diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 76d9c11d..6a08e53b 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -294,9 +294,9 @@ const PurchaseOrderStaffApprovalForm = ({ // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo(() => { return initialValues - ? PurchaseRequestStaffApprovalFormDefaultValues(initialValues) + ? PurchaseRequestStaffApprovalFormDefaultValues(initialValues, type) : PurchaseRequestStaffApprovalFormInitialValues; - }, [initialValues]); + }, [initialValues, type]); const formik = useFormik({ initialValues: formikInitialValues, @@ -485,9 +485,18 @@ const PurchaseOrderStaffApprovalForm = ({ }, warehouse_id: purchaseItem.warehouse_id || 0, qty: originalItem?.qty || purchaseItem.quantity || 0, - price: type === 'edit' && originalItem ? originalItem.price : '', + price: + type === 'edit' && originalItem + ? originalItem.price + : originalItem?.product && 'ProductPrice' in originalItem.product + ? originalItem.product.ProductPrice || '' + : '', total_price: - type === 'edit' && originalItem ? originalItem.total_price : '', + type === 'edit' && originalItem + ? originalItem.total_price + : (originalItem?.product && 'ProductPrice' in originalItem.product + ? originalItem.product.ProductPrice || 0 + : 0) * (originalItem?.qty || purchaseItem.quantity || 0), }; return itemData; }); @@ -1140,6 +1149,7 @@ const PurchaseOrderStaffApprovalForm = ({ color='warning' className='px-4' onClick={() => { + formik.setValues(formikInitialValues); formik.resetForm(); setPurchaseOrderFormErrorMessage(''); onCancel?.(); diff --git a/src/services/api/inventory.ts b/src/services/api/inventory.ts index fa406917..70a7c8f9 100644 --- a/src/services/api/inventory.ts +++ b/src/services/api/inventory.ts @@ -1,4 +1,5 @@ import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; import { CreateProductWarehousePayload, ProductWarehouse, @@ -20,11 +21,38 @@ export const ProductWarehouseApi = new BaseApiService< UpdateProductWarehousePayload >('/inventory/product-warehouses'); -export const MovementApi = new BaseApiService< +export class MovementApiService extends BaseApiService< Movement, CreateMovementPayload, unknown ->('/inventory/transfers'); +> { + constructor(basePath: string) { + super(basePath); + } + + async createMovement( + payload: CreateMovementPayload + ): Promise | undefined> { + const formData = new FormData(); + + // Append data as JSON string + formData.append('data', JSON.stringify(payload.data)); + + // Append documents if any + if (payload.documents && payload.documents.length > 0) { + payload.documents.forEach((file) => { + formData.append('documents', file); + }); + } + + return await this.customRequest>('', { + method: 'POST', + payload: formData as unknown as Record, + }); + } +} + +export const MovementApi = new MovementApiService('/inventory/transfers'); export const InventoryAdjustmentApi = new BaseApiService< InventoryAdjustment, diff --git a/src/types/api/inventory/movement.d.ts b/src/types/api/inventory/movement.d.ts index 53dfa61d..2f6caceb 100644 --- a/src/types/api/inventory/movement.d.ts +++ b/src/types/api/inventory/movement.d.ts @@ -14,6 +14,14 @@ type MovementWarehouse = { }; }; +export type MovementDocument = { + id: number; + path: string; + name: string; + ext: string; + size: number; +}; + export type BaseMovement = { id: number; transfer_reason: string; @@ -39,6 +47,7 @@ export type BaseMovement = { document_path: string; shipping_cost_item: number; shipping_cost_total: number; + document?: MovementDocument; items: { id: number; stock_transfer_detail_id: number; @@ -49,7 +58,7 @@ export type BaseMovement = { export type Movement = BaseMetadata & BaseMovement; -export type CreateMovementPayload = { +export type CreateMovementPayloadData = { transfer_reason: string; transfer_date: string; source_warehouse_id: number; @@ -71,3 +80,8 @@ export type CreateMovementPayload = { }[]; }[]; }; + +export type CreateMovementPayload = { + data: CreateMovementPayloadData; + documents?: File[]; +}; diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index cb8f98a1..f75e4060 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -10,6 +10,8 @@ export type BaseInventoryProduct = { name: string; brand: string; sku: string; + ProductPrice: number; + SellingPrice?: number; product_price: number; selling_price?: number; tax?: number; diff --git a/src/types/api/production/uniformity.d.ts b/src/types/api/production/uniformity.d.ts index 0ebc7ea9..0863c08a 100644 --- a/src/types/api/production/uniformity.d.ts +++ b/src/types/api/production/uniformity.d.ts @@ -1,7 +1,4 @@ import { BaseMetadata } from '@/types/api/api-general'; -import { Location } from '@/types/api/location/location'; -import { ProjectFlock } from '@/types/api/project-flock/project-flock'; -import { Kandang } from '@/types/api/kandang/kandang'; import { BaseApproval } from '@/types/api/approval/approval'; // ==================== GET ALL RESPONSE ==================== @@ -11,6 +8,7 @@ export type Uniformity = BaseMetadata & { location_name: string; flock_name: string; kandang_name: string; + file_name: string; applied_at: string; week: number; status: string; diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 4e717f15..d355c2f8 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -10,6 +10,8 @@ export type PurchaseItemProduct = { id: number; name: string; flags?: string[]; + ProductPrice?: number; + SellingPrice?: number; uom?: { name: string; };
Uniformity 2025
Sampling and Range
Result