diff --git a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx index f683ec58..da89d963 100644 --- a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx +++ b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx @@ -13,7 +13,6 @@ interface HppExpeditionReportTableProps { } const HppExpeditionReportTable = ({ - type = 'detail', initialValues, }: HppExpeditionReportTableProps) => { const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 99868984..0632676b 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -4,7 +4,6 @@ import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, @@ -20,10 +19,7 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -const SalesReportTable = ({ - type = 'detail', - initialValues, -}: SalesReportTableProps) => { +const SalesReportTable = ({ initialValues }: SalesReportTableProps) => { const salesData: BaseSales[] = useMemo(() => { return initialValues?.sales || []; }, [initialValues]); diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index ac814bcf..6513956e 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -1,9 +1,8 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; -import useSWR from 'swr'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -57,10 +56,6 @@ const ExpenseRequestContent = ({ const isLatestApprovalRejected = initialValues?.latest_approval.action === 'REJECTED'; - const isLatestApprovalRejectedOrDone = - isLatestApprovalRejected || - initialValues?.latest_approval.step_number === 6; - const isCurrentApprovalOnHeadArea = !isLatestApprovalRejected && initialValues?.latest_approval.step_number === 1; diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 895c0997..69376992 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -35,7 +35,6 @@ import { ExpenseApi } from '@/services/api/expense'; import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; import { LocationApi, SupplierApi } from '@/services/api/master-data'; import { Location } from '@/types/api/master-data/location'; import { Supplier } from '@/types/api/master-data/supplier'; @@ -44,8 +43,6 @@ import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ type = 'dropdown', props, - approveClickHandler, - rejectClickHandler, deleteClickHandler, }: { type: 'dropdown' | 'collapse'; @@ -186,7 +183,6 @@ const ExpensesTable = () => { undefined ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [isCompleteLoading, setIsCompleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); @@ -247,23 +243,6 @@ const ExpensesTable = () => { }); }, [expenses, selectedRowIds]); - const isAllSelectedRowLatestApprovalOnRealization = useMemo(() => { - return selectedRowIds.every((rowId) => { - if (!isResponseSuccess(expenses)) return false; - - const expenseItem = expenses.data.find((item) => item.id === rowId); - - const isLatestApprovalRejected = - expenseItem?.latest_approval.action === 'REJECTED'; - - const isCurrentApprovalOnRealization = - !isLatestApprovalRejected && - expenseItem?.latest_approval.step_number === 5; - - return isCurrentApprovalOnRealization; - }); - }, [expenses, selectedRowIds]); - const expensesColumns: ColumnDef[] = [ { id: 'select', @@ -589,12 +568,6 @@ const ExpensesTable = () => { updateFilter('realizationDate', e.target.value); }; - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - - setPageSize(newVal.value as number); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); diff --git a/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx b/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx index 3f6f2220..fcf367eb 100644 --- a/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx @@ -30,7 +30,7 @@ interface ExpenseRealizationKandangDetailExpenseProps { const ExpenseRealizationKandangDetailExpense: React.FC< ExpenseRealizationKandangDetailExpenseProps -> = ({ type, formik, supplierId, location, className }) => { +> = ({ formik, supplierId, location, className }) => { const { setInputValue: setNonstockInputValue, options: nonstockOptions, diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index d36fb067..bdf37487 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useFormik } from 'formik'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -18,6 +17,7 @@ import { Movement, } from '@/types/api/inventory/movement'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { formatNumber } from '@/lib/helper'; import { useRouter } from 'next/navigation'; import { MovementFormSchema, @@ -54,6 +54,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const [selectedProducts, setSelectedProducts] = useState([]); const [selectedDeliveries, setSelectedDeliveries] = useState([]); const [formErrorList, setFormErrorList] = useState([]); + const [productQtyErrorShown, setProductQtyErrorShown] = useState(false); + const [deliveryQtyErrorShown, setDeliveryQtyErrorShown] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); // ===== FORM HANDLERS ===== const createMovementHandler = useCallback( @@ -82,22 +85,21 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { quantity: number; } - // ===== API DATA FETCHING ===== - const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`; - const { data: allProductWarehouses } = useSWR( - allProductWarehousesUrl, - ProductWarehouseApi.getAllFetcher - ); - // ===== USE SELECT HOOKS ===== const { setInputValue: setWarehouseSelectInputValue, isLoadingOptions: isLoadingWarehouses, loadMore: loadMoreWarehouses, rawData: warehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', { - flag: 'EKSPEDISI', - }); + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + + const { rawData: allProductWarehouses } = useSelect( + ProductWarehouseApi.basePath, + 'id', + 'product.name', + 'search', + { limit: '100' } + ); // ===== SELECT INPUT DATA ===== const { @@ -106,6 +108,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { isLoadingOptions: isLoadingSuppliers, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'BOP', + flag: 'EKSPEDISI', }); // ===== DATA PROCESSING ===== @@ -322,16 +325,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { } ); - const productWarehouseOptions = isResponseSuccess(productWarehouses) - ? productWarehouses?.data.map((pw) => ({ - value: pw.product.id, - label: pw.product.name, - product_id: pw.product.id, - warehouse_id: pw.warehouse.id, - warehouse_name: pw.warehouse.name, - quantity: pw.quantity, - })) - : []; + const productWarehouseOptions = useMemo(() => { + return isResponseSuccess(productWarehouses) + ? productWarehouses?.data.map((pw) => ({ + value: pw.product.id, + label: pw.product.name, + product_id: pw.product.id, + warehouse_id: pw.warehouse.id, + warehouse_name: pw.warehouse.name, + quantity: pw.quantity, + })) + : []; + }, [productWarehouses]); // ===== HELPER FUNCTIONS ===== const isRepeaterInputError = ( @@ -464,19 +469,24 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }, ]; formik.setFieldValue('products', newProducts); - }, []); + }, [formik.values.products]); - const removeProduct = useCallback((i: number) => { - const updatedProducts = - formik.values.products?.reduce((acc: ProductSchema[], item, index) => { - if (index !== i) { - acc.push(item); - } - return acc; - }, []) ?? []; + const removeProduct = useCallback( + (i: number) => { + const updatedProducts = formik.values.products?.filter( + (_, idx) => idx !== i + ); + formik.setFieldValue('products', updatedProducts); - formik.setFieldValue('products', updatedProducts); - }, []); + setSelectedProducts([]); + + if (productQtyErrorShown) { + toast.dismiss(); + setProductQtyErrorShown(false); + } + }, + [formik.values.products, productQtyErrorShown, setSelectedProducts] + ); const bulkRemoveProduct = useCallback(() => { const updatedProducts = @@ -485,7 +495,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ) ?? []; formik.setFieldValue('products', updatedProducts); setSelectedProducts([]); - }, [formik, selectedProducts, setSelectedProducts]); + + if (productQtyErrorShown) { + toast.dismiss(); + setProductQtyErrorShown(false); + } + }, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]); const handleProductChange = useCallback( (idx: number, val: OptionType | OptionType[] | null) => { @@ -543,19 +558,24 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ], }, ]); - }, []); + }, [formik.values.deliveries]); - const removeDelivery = useCallback((i: number) => { - const updatedDeliveries = - formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => { - if (index !== i) { - acc.push(item); - } - return acc; - }, []) ?? []; + const removeDelivery = useCallback( + (i: number) => { + const updatedDeliveries = formik.values.deliveries?.filter( + (_, idx) => idx !== i + ); + formik.setFieldValue('deliveries', updatedDeliveries); - formik.setFieldValue('deliveries', updatedDeliveries); - }, []); + setSelectedDeliveries([]); + + if (deliveryQtyErrorShown) { + toast.dismiss(); + setDeliveryQtyErrorShown(false); + } + }, + [formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries] + ); const bulkRemoveDelivery = useCallback(() => { const updatedDeliveries = @@ -564,7 +584,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ) ?? []; formik.setFieldValue('deliveries', updatedDeliveries); setSelectedDeliveries([]); - }, [formik, selectedDeliveries, setSelectedDeliveries]); + + if (deliveryQtyErrorShown) { + toast.dismiss(); + setDeliveryQtyErrorShown(false); + } + }, [ + formik, + selectedDeliveries, + setSelectedDeliveries, + deliveryQtyErrorShown, + ]); const handleDeliverySelectAllChange = useCallback( (e: React.ChangeEvent) => { @@ -638,26 +668,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [] ); - const handleDeliveryCostChange = useCallback((idx: number, value: number) => { - formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); + const handleDeliveryCostChange = useCallback( + (idx: number, value: number) => { + formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); - const delivery = formik.values.deliveries?.[idx]; - if (delivery) { - const productQty = delivery.products.reduce( - (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0), - 0 - ); - if (productQty > 0 && value > 0) { - const perItem = value / productQty; - formik.setFieldValue( - `deliveries.${idx}.delivery_cost_per_item`, - perItem + const delivery = formik.values.deliveries?.[idx]; + if (delivery) { + const productQty = delivery.products.reduce( + (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0), + 0 ); - } else if (value === 0) { - formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0); + if (productQty > 0 && value > 0) { + const perItem = value / productQty; + formik.setFieldValue( + `deliveries.${idx}.delivery_cost_per_item`, + perItem + ); + } else if (value === 0) { + formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0); + } } - } - }, []); + }, + [formik.values.deliveries] + ); const handleDeliveryCostPerItemChange = useCallback( (idx: number, value: number) => { @@ -677,7 +710,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { } } }, - [] + [formik.values.deliveries] ); const handleDeliveryCostChangeWrapper = useCallback( @@ -696,17 +729,52 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [handleDeliveryCostPerItemChange] ); - // UTILITY FUNCTIONS + const getAvailableProductOptions = useCallback( + (currentIdx: number) => { + const selectedProductIds = + formik.values.products + ?.filter((p, idx) => { + return idx !== currentIdx && p.product_id && p.product_id !== 0; + }) + .map((p) => p.product_id) || []; + + return productWarehouseOptions.filter( + (pw) => !selectedProductIds.includes(pw.product_id) + ); + }, + [formik.values.products, productWarehouseOptions] + ); + const getFilteredProductWarehouseOptions = useCallback(() => { return ( formik.values.products ?.filter((p) => p.product) - .map((p) => ({ - value: p.product_id, - label: (p.product as OptionType)?.label, - })) ?? [] + .map((p) => { + const totalQtyUsed = + formik.values.deliveries?.reduce((total, d) => { + const productQty = d.products.reduce((sum, deliveryProduct) => { + if (deliveryProduct.product_id === p.product_id) { + return sum + (Number(deliveryProduct.product_qty) || 0); + } + return sum; + }, 0); + return total + productQty; + }, 0) || 0; + + const availableQty = Number(p.product_qty) - totalQtyUsed; + + if (availableQty > 0) { + return { + value: p.product_id, + label: (p.product as OptionType)?.label, + }; + } + + return null; + }) + .filter((option) => option !== null) ?? [] ); - }, [formik.values.products]); + }, [formik.values.products, formik.values.deliveries]); const getAvailableStock = useCallback( (productId: number) => { @@ -730,10 +798,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const remainingStock = availableStock - requestedQty; if (requestedQty > 0) { - return `Sisa: ${remainingStock.toLocaleString('en-US')}`; + return `Sisa: ${formatNumber(remainingStock)}`; } - return `Tersedia: ${availableStock.toLocaleString('en-US')}`; + return `Tersedia: ${formatNumber(availableStock)}`; }, [formik.values.products, getAvailableStock, type] ); @@ -753,12 +821,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { if (!relatedProduct) return undefined; const totalQtyUsed = - formik.values.deliveries?.reduce((total, d, dIdx) => { - const productQty = d.products.reduce((sum, p, pIdx) => { - if ( - p.product_id === deliveryProduct.product_id && - !(dIdx === deliveryIdx && pIdx === productIdx) - ) { + formik.values.deliveries?.reduce((total, d) => { + const productQty = d.products.reduce((sum, p) => { + if (p.product_id === deliveryProduct.product_id) { return sum + (Number(p.product_qty) || 0); } return sum; @@ -767,7 +832,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }, 0) || 0; const availableQty = Number(relatedProduct.product_qty) - totalQtyUsed; - return `Tersedia: ${availableQty.toLocaleString('en-US')}`; + + const displayQty = availableQty > 0 ? availableQty : 0; + + return `Tersedia: ${formatNumber(displayQty)}`; }, [formik.values.deliveries, formik.values.products, type] ); @@ -817,7 +885,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const requestedQty = Number(product.product_qty) || 0; if (requestedQty > availableStock) { - return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`; + return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`; } return null; @@ -966,20 +1034,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }); }, [ formik.values.deliveries - ?.map((d) => - d.products.reduce( + ?.map((d, idx) => ({ + idx, + productQty: d.products.reduce( (sum, p) => sum + (parseInt(p.product_qty.toString()) || 0), 0 - ) + ), + deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0, + deliveryCostPerItem: + parseInt((d.delivery_cost_per_item || '').toString()) || 0, + })) + .map( + (item) => + `${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}` ) - .join(','), + .join('|'), ]); useEffect(() => { if ( formik.values.source_warehouse_id && type !== 'edit' && - type !== 'detail' + type !== 'detail' && + !isInitialized ) { if (formik.values.products.length === 0) { formik.setFieldValue('products', [ @@ -1011,8 +1088,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }, ]); } + setIsInitialized(true); } - }, [formik.values.source_warehouse_id]); + }, [formik.values.source_warehouse_id, isInitialized, type]); useEffect(() => { if ( @@ -1039,6 +1117,113 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { formik.errors.destination_warehouse_id, ]); + useEffect(() => { + if (formik.values.products && formik.values.deliveries) { + const productIds = formik.values.products.map((p) => p.product_id); + + const updatedDeliveries = formik.values.deliveries.map((delivery) => { + const deliveryProduct = delivery.products[0]; + if (deliveryProduct && deliveryProduct.product_id !== 0) { + if (!productIds.includes(deliveryProduct.product_id)) { + return { + ...delivery, + products: [ + { + product: null, + product_id: 0, + product_qty: '', + }, + ], + delivery_cost: '', + delivery_cost_per_item: '', + }; + } + } + return delivery; + }); + + const hasChanges = formik.values.deliveries.some( + (delivery, idx) => + delivery.products[0]?.product_id !== + updatedDeliveries[idx]?.products[0]?.product_id || + delivery.delivery_cost !== updatedDeliveries[idx]?.delivery_cost + ); + + if (hasChanges) { + formik.setFieldValue('deliveries', updatedDeliveries); + } + } + }, [formik.values.products]); + + useEffect(() => { + if (productQtyErrorShown) { + toast.dismiss(); + setProductQtyErrorShown(false); + } + }, [formik.values.products?.map((p) => p.product_qty).join(',')]); + + useEffect(() => { + if (deliveryQtyErrorShown) { + toast.dismiss(); + setDeliveryQtyErrorShown(false); + } + }, [ + formik.values.deliveries + ?.map((d) => d.products.map((p) => p.product_qty).join(',')) + .join('|'), + formik.values.products?.map((p) => p.product_qty).join(','), + ]); + + useEffect(() => { + if (hasExceededStock && !productQtyErrorShown && type !== 'detail') { + const firstErrorIndex = formik.values.products?.findIndex( + (product, idx) => getProductQtyError(idx) !== null + ); + if (firstErrorIndex !== undefined && firstErrorIndex >= 0) { + const errorMsg = getProductQtyError(firstErrorIndex); + if (errorMsg) { + toast.error(errorMsg, { duration: Infinity }); + setProductQtyErrorShown(true); + } + } + } + }, [ + hasExceededStock, + productQtyErrorShown, + type, + formik.values.products, + getProductQtyError, + ]); + + useEffect(() => { + if (hasInvalidQty && !deliveryQtyErrorShown && type !== 'detail') { + const firstError = formik.values.deliveries?.find( + (delivery, deliveryIdx) => + delivery.products.some( + (product, productIdx) => + getDeliveryQtyError(deliveryIdx, productIdx) !== null + ) + ); + if (firstError) { + const deliveryIdx = formik.values.deliveries?.indexOf(firstError); + if (deliveryIdx !== undefined && deliveryIdx >= 0) { + const errorMsg = getDeliveryQtyError(deliveryIdx, 0); + if (errorMsg) { + toast.error(errorMsg, { duration: Infinity }); + setDeliveryQtyErrorShown(true); + } + } + } + } + }, [ + hasInvalidQty, + deliveryQtyErrorShown, + type, + formik.values.deliveries, + formik.values.products, + getDeliveryQtyError, + ]); + const handleValidateForm = async () => { const errors = await formik.validateForm(); @@ -1340,7 +1525,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { required value={product.product ?? undefined} onChange={(val) => handleProductChange(idx, val)} - options={productWarehouseOptions} + options={getAvailableProductOptions(idx)} onInputChange={setProductWarehouseSelectInputValue} onMenuScrollToBottom={loadMoreProductWarehouses} isLoading={isLoadingProductWarehouses} @@ -1543,7 +1728,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.deliveries?.map((delivery, idx) => ( - + {type !== 'detail' && ( { className='px-4' isLoading={formik.isSubmitting} disabled={ - hasInvalidQty || - hasExceededStock || formik.isSubmitting || (formik.values.source_warehouse_id === formik.values.destination_warehouse_id && diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 8c04d594..45ba192d 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -17,7 +16,6 @@ import SelectInput, { import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; -import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { @@ -25,7 +23,7 @@ import { ProductFormValues, UpdateProductFormSchema, } from '@/components/pages/master-data/product/form/ProductForm.schema'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { Product, CreateProductPayload, diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index a000f4a3..63d78397 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -71,7 +71,6 @@ import { import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; -import { getUniqueFormikErrors } from '@/lib/formik-helper'; import toast from 'react-hot-toast'; import ApprovalSteps, { useApprovalSteps, @@ -423,7 +422,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { options: locationOptions, isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, - hasMore: hasMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); const { @@ -432,7 +430,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { rawData: projectFlocksRawData, isLoadingOptions: isLoadingProjectFlocks, loadMore: loadMoreProjectFlocks, - hasMore: hasMoreProjectFlocks, } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { location_id: selectedProjectFlockLocationId, }); @@ -531,7 +528,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { rawData: stockProducts, isLoadingOptions: isLoadingStockProducts, loadMore: loadMoreStockProducts, - hasMore: hasMoreStockProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { flags: 'PAKAN,OVK', location_id: stockProductsLocationId, @@ -539,11 +535,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }); const { - options: depletionProductOptions, rawData: depletionProductsData, isLoadingOptions: isLoadingDepletionProducts, loadMore: loadMoreDepletionProducts, - hasMore: hasMoreDepletionProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { location_id: depletionProductsLocationId, kandang_id: depletionProductsKandangId, @@ -584,11 +578,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [nextDayRecordingData]); const { - options: eggProductOptions, rawData: eggProductsData, isLoadingOptions: isLoadingEggProducts, loadMore: loadMoreEggProducts, - hasMore: hasMoreEggProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { search: 'telur', location_id: eggProductsLocationId, diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index c2049ab1..29d1155a 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -51,6 +51,13 @@ import { generateUniformityExcel } from '@/components/pages/production/uniformit import Dropdown from '@/components/Dropdown'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; +import { useFormik } from 'formik'; +import { + UniformityTableFilterSchema, + type UniformityTableFilterValues, +} from '@/components/pages/production/uniformity/UniformityTableFilter.schema'; +import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; const UniformityConfirmationPreview = ({ uniformity, @@ -241,7 +248,6 @@ const UniformityTable = () => { options: filterLocationOptions, isLoadingOptions: isLoadingFilterLocations, loadMore: loadMoreFilterLocations, - hasMore: hasMoreFilterLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); // ===== FETCH PROJECT FLOCKS DATA FOR FILTER ===== @@ -251,7 +257,6 @@ const UniformityTable = () => { rawData: filterProjectFlocksRawData, isLoadingOptions: isLoadingFilterProjectFlocks, loadMore: loadMoreFilterProjectFlocks, - hasMore: hasMoreFilterProjectFlocks, } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { location_id: filterProjectFlockLocationId, }); @@ -316,6 +321,34 @@ const UniformityTable = () => { } }, [projectFlockKandangLookup]); + // ===== FORMIK FILTER ===== + const filterFormik = useFormik({ + initialValues: { + start_date: filterStartDate, + end_date: filterEndDate, + location: filterLocation, + project_flock: filterProjectFlock, + project_flock_kandang_id: filterProjectFlockKandangId, + kandang: filterKandang, + }, + validationSchema: UniformityTableFilterSchema, + enableReinitialize: true, + onSubmit: async (values) => { + setFilterStartDate(values.start_date); + setFilterEndDate(values.end_date); + setFilterLocation(values.location ?? null); + setFilterProjectFlock(values.project_flock ?? null); + setFilterKandang(values.kandang ?? null); + + setIsSubmitted(true); + filterModal.closeModal(); + }, + }); + + // ===== FORMIK ERROR LIST ===== + const { formErrorList, close, handleFormSubmit } = + useFormikErrorList(filterFormik); + // ===== BUILD SWR KEY WITH FILTERS ===== const uniformitySwrKey = useMemo(() => { const basePath = UniformityApi.basePath; @@ -372,29 +405,54 @@ const UniformityTable = () => { const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; + const locationId = Number(location?.value) || 0; + + filterFormik.setFieldValue('location', location); + filterFormik.setFieldValue('location_id', locationId); + setFilterLocation(location); setFilterProjectFlock(null); setFilterKandang(null); setFilterProjectFlockLocationId( location ? location.value.toString() : '' ); + + filterFormik.setFieldValue('project_flock', null); + filterFormik.setFieldValue('project_flock_id', 0); + filterFormik.setFieldValue('kandang', null); + filterFormik.setFieldValue('kandang_id', 0); }, - [] + [filterFormik] ); const handleFilterProjectFlockChange = useCallback( (val: OptionType | OptionType[] | null) => { - setFilterProjectFlock(val as OptionType | null); + const projectFlock = val as OptionType | null; + const projectFlockId = Number(projectFlock?.value) || 0; + + filterFormik.setFieldValue('project_flock', projectFlock); + filterFormik.setFieldValue('project_flock_id', projectFlockId); + + setFilterProjectFlock(projectFlock); setFilterKandang(null); + + filterFormik.setFieldValue('kandang', null); + filterFormik.setFieldValue('kandang_id', 0); }, - [] + [filterFormik] ); const handleFilterKandangChange = useCallback( (val: OptionType | OptionType[] | null) => { - setFilterKandang(val as OptionType | null); + const kandang = val as OptionType | null; + const kandangId = Number(kandang?.value) || 0; + + filterFormik.setFieldValue('kandang', kandang); + filterFormik.setFieldValue('kandang_id', kandangId); + + setFilterKandang(kandang); }, - [] + [filterFormik] ); const handleResetFilters = useCallback(() => { @@ -405,41 +463,34 @@ const UniformityTable = () => { setFilterProjectFlockKandangId(undefined); setFilterStartDate(''); setFilterEndDate(''); - }, []); + setFilterErrors({}); + + filterFormik.resetForm(); + }, [filterFormik]); + + const handleFilterStartDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterStartDate(value); + filterFormik.setFieldValue('start_date', value); + }, + [filterFormik] + ); + + const handleFilterEndDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterEndDate(value); + filterFormik.setFieldValue('end_date', value); + }, + [filterFormik] + ); const handleApplyFilters = useCallback(() => { - const errors: Record = {}; - - if (!filterStartDate) { - errors.start_date = 'Tanggal mulai wajib diisi'; - } - if (!filterEndDate) { - errors.end_date = 'Tanggal akhir wajib diisi'; - } - if (!filterLocation) { - errors.location = 'Lokasi wajib dipilih'; - } - if (!filterProjectFlock) { - errors.project_flock = 'Project Flock wajib dipilih'; - } - if (!filterKandang) { - errors.kandang = 'Kandang wajib dipilih'; - } - - setFilterErrors(errors); - - if (Object.keys(errors).length === 0) { - setIsSubmitted(true); - filterModal.closeModal(); - } - }, [ - filterModal, - filterStartDate, - filterEndDate, - filterLocation, - filterProjectFlock, - filterKandang, - ]); + handleFormSubmit( + new Event('submit') as unknown as React.FormEvent + ); + }, [handleFormSubmit]); const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) @@ -1136,108 +1187,117 @@ const UniformityTable = () => { + + {/* Error List Alert */} + {formErrorList.length > 0 && ( +
+ +
+ )} +
{ - setFilterStartDate(e.target.value); - setFilterErrors((prev) => ({ ...prev, start_date: '' })); - }} + value={filterFormik.values.start_date} + onChange={handleFilterStartDateChange} + onBlur={filterFormik.handleBlur} + isError={ + filterFormik.touched.start_date && + Boolean(filterFormik.errors.start_date) + } + errorMessage={filterFormik.errors.start_date} className={{ wrapper: 'w-full' }} /> - {filterErrors.start_date && ( -

- {filterErrors.start_date} -

- )}
{ - setFilterEndDate(e.target.value); - setFilterErrors((prev) => ({ ...prev, end_date: '' })); - }} + value={filterFormik.values.end_date} + onChange={handleFilterEndDateChange} + onBlur={filterFormik.handleBlur} + isError={ + filterFormik.touched.end_date && + Boolean(filterFormik.errors.end_date) + } + errorMessage={filterFormik.errors.end_date} className={{ wrapper: 'w-full' }} /> - {filterErrors.end_date && ( -

- {filterErrors.end_date} -

- )}
{ handleFilterLocationChange(value); - setFilterErrors((prev) => ({ ...prev, location: '' })); }} options={filterLocationOptions} onInputChange={setFilterLocationInputValue} isLoading={isLoadingFilterLocations} onMenuScrollToBottom={loadMoreFilterLocations} + isError={ + filterFormik.touched.location && + Boolean(filterFormik.errors.location) + } + errorMessage={filterFormik.errors.location} + isClearable className={{ wrapper: 'w-full' }} /> - {filterErrors.location && ( -

- {filterErrors.location} -

- )}
{ handleFilterProjectFlockChange(value); - setFilterErrors((prev) => ({ ...prev, project_flock: '' })); }} options={filterProjectFlockOptions} onInputChange={setFilterProjectFlockSearchValue} isLoading={isLoadingFilterProjectFlocks} onMenuScrollToBottom={loadMoreFilterProjectFlocks} - isDisabled={!filterLocation} + isDisabled={!filterFormik.values.location} + isError={ + filterFormik.touched.project_flock && + Boolean(filterFormik.errors.project_flock) + } + errorMessage={filterFormik.errors.project_flock} + isClearable className={{ wrapper: 'w-full' }} /> - {filterErrors.project_flock && ( -

- {filterErrors.project_flock} -

- )}
{ handleFilterKandangChange(value); - setFilterErrors((prev) => ({ ...prev, kandang: '' })); }} options={filterKandangOptions} - isDisabled={!filterProjectFlock} + isDisabled={!filterFormik.values.project_flock} + isError={ + filterFormik.touched.kandang && + Boolean(filterFormik.errors.kandang) + } + errorMessage={filterFormik.errors.kandang} + isClearable className={{ wrapper: 'w-full' }} /> - {filterErrors.kandang && ( -

- {filterErrors.kandang} -

- )}
diff --git a/src/components/pages/production/uniformity/UniformityTableFilter.schema.ts b/src/components/pages/production/uniformity/UniformityTableFilter.schema.ts new file mode 100644 index 00000000..3c14553c --- /dev/null +++ b/src/components/pages/production/uniformity/UniformityTableFilter.schema.ts @@ -0,0 +1,59 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type UniformityTableFilterType = { + start_date: string; + end_date: string; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang_id: number | undefined; + kandang: OptionType | null; +}; + +export const UniformityTableFilterSchema = yup.object({ + start_date: yup.string().required('Tanggal mulai wajib diisi'), + end_date: yup + .string() + .required('Tanggal akhir wajib diisi') + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + location: yup + .mixed() + .required('Lokasi wajib dipilih') + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + project_flock: yup + .mixed() + .required('Project Flock wajib dipilih') + .test('is-not-empty', 'Project Flock wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + project_flock_kandang_id: yup.number().optional(), + kandang: yup + .mixed() + .required('Kandang wajib dipilih') + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), +}) as yup.ObjectSchema; + +export type UniformityTableFilterValues = yup.InferType< + typeof UniformityTableFilterSchema +>; diff --git a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx index 88d0dc59..82f0085c 100644 --- a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx @@ -49,7 +49,7 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
-
+
Ideal
@@ -84,7 +84,7 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {

Uniformity 2025

-
+
Ideal
{chartData.idealRange} diff --git a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx index 04fbef9c..54f8e4ec 100644 --- a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; import Card from '@/components/Card'; import { formatNumber } from '@/lib/helper'; diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index f46a15b3..d7013520 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -5,7 +5,6 @@ import { useFormik } from 'formik'; import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; -import moment from 'moment'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; @@ -28,6 +27,7 @@ import { LocationApi } from '@/services/api/master-data'; import { ProjectFlockApi, ProjectFlockKandangApi, + RecordingApi, } from '@/services/api/production'; import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -40,6 +40,7 @@ import { ProjectFlockKandangLookup, ProjectFlock, } from '@/types/api/production/project-flock'; +import { Recording } from '@/types/api/production/recording'; import { Kandang } from '@/types/api/master-data/kandang'; import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm'; import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm'; @@ -87,9 +88,7 @@ const UniformityForm = ({ const fileInputRef = useRef(null); // ===== SELECT INPUT DATA ===== - const [selectedLocation, setSelectedLocation] = useState( - null - ); + const [, setSelectedLocation] = useState(null); const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] = useState(''); @@ -106,7 +105,6 @@ const UniformityForm = ({ options: locationOptions, isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, - hasMore: hasMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); const { @@ -115,7 +113,6 @@ const UniformityForm = ({ rawData: projectFlocksRawData, isLoadingOptions: isLoadingProjectFlocks, loadMore: loadMoreProjectFlocks, - hasMore: hasMoreProjectFlocks, } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { location_id: selectedProjectFlockLocationId, }); @@ -204,6 +201,20 @@ const UniformityForm = ({ ? projectFlockKandangLookupData.data : undefined; + // ===== RECORDINGS DATA (FOR WEEK CALCULATION) ===== + const recordingsUrl = useMemo(() => { + const params = new URLSearchParams({ + page: '1', + limit: '100', + }); + return `${RecordingApi.basePath}?${params.toString()}`; + }, []); + + const { data: recordingsData } = useSWR( + recordingsUrl, + RecordingApi.getAllFetcher + ); + // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo( () => getUniformityFormInitialValues(initialValues), @@ -387,14 +398,24 @@ const UniformityForm = ({ // ===== SIDE EFFECTS ===== useEffect(() => { - if (formik.values.date) { - const date = moment(formik.values.date); - const weekNumber = date.week() - moment(date).startOf('month').week() + 1; - const adjustedWeekNumber = weekNumber <= 0 ? weekNumber + 52 : weekNumber; + if ( + projectFlockKandangLookup?.project_flock_kandang_id && + isResponseSuccess(recordingsData) && + recordingsData.data + ) { + const matchingRecording = recordingsData.data.find( + (recording: Recording) => + recording.project_flock?.project_flock_kandang_id === + projectFlockKandangLookup.project_flock_kandang_id + ); - formik.setFieldValue('week', adjustedWeekNumber); + if (matchingRecording?.project_flock?.production_standart?.week) { + const weekValue = + matchingRecording.project_flock.production_standart.week; + formik.setFieldValue('week', weekValue); + } } - }, [formik.values.date]); + }, [projectFlockKandangLookup?.project_flock_kandang_id, recordingsData]); useEffect(() => { const unsub = subscribeValidate(() => { @@ -598,7 +619,7 @@ const UniformityForm = ({
diff --git a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 436fab9a..17ed7ee9 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -29,7 +29,7 @@ const UniformityGaugeChartSkeleton: React.FC< return (
-
+
@@ -57,7 +57,7 @@ const UniformityGaugeChartSkeleton: React.FC<
-