diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 8d5b9170..9cc9fda5 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -325,7 +325,7 @@ const SelectInput = (props: SelectInputProps) => { }; const useSelect = ( - basePath: string, + basePath: string | null, valueKey: keyof T | string, labelKey: keyof T | string, searchKey: string = 'search', @@ -354,7 +354,7 @@ const useSelect = ( [limitKey]: String(limit), }).toString(); - return `${basePath}?${qs}`; + return basePath ? `${basePath}?${qs}` : null; }; const { diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 7eb34369..010bfc2f 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -8,6 +8,7 @@ import { HppPurchaseData, ProfitLossDataAmount, } from '@/types/api/closing'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; type HppTableRow = @@ -55,9 +56,16 @@ const ClosingFinanceTable = ({ }: { projectFlockId: number; }) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { data: finance, isLoading } = useSWR( - `/closing/finance/${projectFlockId}`, - () => ClosingApi.getFinance(projectFlockId) + `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => + ClosingApi.getFinance( + projectFlockId, + kandangId ? Number(kandangId) : undefined + ) ); const staticHppRows: Array<{ @@ -283,6 +291,7 @@ const ClosingFinanceTable = ({
data={hppTableData} + isLoading={isLoading} columns={[ { header: 'No.', @@ -455,6 +464,7 @@ const ClosingFinanceTable = ({
data={profitLossTableData} + isLoading={isLoading} columns={[ { header: 'Jenis', diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 0cacb549..ec334104 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -163,6 +163,7 @@ const ClosingsTable = () => { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); const [selectedLocation, setSelectedLocation] = useState( @@ -228,6 +229,7 @@ const ClosingsTable = () => { value={selectedLocation} onChange={locationChangeHandler} onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6', diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index e586b4a3..348f6c43 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -283,261 +283,311 @@ const DashboardLineChart = ({ })()}
- {/* Chart */} - - { - // Transform data based on analysisMode - if (analysisMode === 'OVERVIEW') { - // For OVERVIEW mode, use the selected chart data - if (isOverviewCharts(data.charts)) { - const selectedChartData = data.charts[chartData]; - if (!selectedChartData || !selectedChartData.dataset) return []; - return selectedChartData.dataset; + {/* Chart Container with Empty State Overlay */} +
+ {/* Chart */} + + { + // Transform data based on analysisMode + if (analysisMode === 'OVERVIEW') { + // For OVERVIEW mode, use the selected chart data + if (isOverviewCharts(data.charts)) { + const selectedChartData = data.charts[chartData]; + if (!selectedChartData || !selectedChartData.dataset) + return []; + return selectedChartData.dataset; + } + return []; + } else { + // For COMPARISON mode, use the first available comparison chart + if (isComparisonCharts(data.charts)) { + const chartData = + data.charts.location || + data.charts.flock || + data.charts.kandang; + + if (!chartData || !chartData.dataset) return []; + return chartData.dataset; + } + return []; } - return []; - } else { - // For COMPARISON mode, use the first available comparison chart - if (isComparisonCharts(data.charts)) { - const chartData = - data.charts.location || - data.charts.flock || - data.charts.kandang; - - if (!chartData || !chartData.dataset) return []; - return chartData.dataset; - } - return []; - } - })()} - margin={{ - top: 5, - right: 10, - left: 0, - bottom: 5, - }} - > - - - { - // Calculate dynamic domain based on visible data - let seriesData: DashboardChartsSeries[] = []; - let dataset: DashboardChartsDataset[] = []; - - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - dataset = data.charts[chartData]?.dataset || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - dataset = comparisonChart?.dataset || []; - } - - // Get all values from visible series - const visibleSeriesIds = Array.from(visibleSeries); - const allValues: number[] = []; - - dataset.forEach((item: DashboardChartsDataset) => { - visibleSeriesIds.forEach((seriesId) => { - const value = item[seriesId]; - if (typeof value === 'number') { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [0, 100]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - - // Add padding (10% on each side) - const padding = (maxValue - minValue) * 0.1; - const domainMin = Math.floor(Math.max(0, minValue - padding)); - const domainMax = Math.ceil(maxValue + padding); - - return [domainMin, domainMax]; })()} - ticks={(() => { - // Calculate dynamic ticks based on domain - let seriesData: DashboardChartsSeries[] = []; - let dataset: DashboardChartsDataset[] = []; - - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - dataset = data.charts[chartData]?.dataset || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - dataset = comparisonChart?.dataset || []; - } - - const visibleSeriesIds = Array.from(visibleSeries); - const allValues: number[] = []; - - dataset.forEach((item: DashboardChartsDataset) => { - visibleSeriesIds.forEach((seriesId) => { - const value = item[seriesId]; - if (typeof value === 'number') { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [0, 25, 50, 75, 100]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - const padding = (maxValue - minValue) * 0.1; - const domainMin = Math.floor(Math.max(0, minValue - padding)); - const domainMax = Math.ceil(maxValue + padding); - - // Generate 5 evenly spaced ticks - const range = domainMax - domainMin; - const step = range / 4; - - return [ - domainMin, - Math.round(domainMin + step), - Math.round(domainMin + step * 2), - Math.round(domainMin + step * 3), - domainMax, - ]; - })()} - /> - `Week ${value}`} - formatter={( - value: number | undefined, - name: string | undefined - ) => { - if (value === undefined || name === undefined) return ['', '']; + > + + + { + // Calculate dynamic domain based on visible data + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; - // Get series data to find the unit - let seriesData: DashboardChartsSeries[] = []; - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - } + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } - // Find the series that matches this line's name - const series = seriesData.find((s) => s.label === name); - const unit = series?.unit || ''; + // Get all values from visible series + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; - return [`${value} ${unit}`, name]; - }} - /> - {/* Dynamic Line rendering based on visible series */} - {(() => { - let seriesData: DashboardChartsSeries[] = []; - - if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { - seriesData = data.charts[chartData]?.series || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - } - - return seriesData - .filter((series) => visibleSeries.has(series.id)) - .map((series, index) => { - const isStandard = series.id - .toString() - .toLowerCase() - .includes('std'); - // Use series.id directly as dataKey to match dataset fields - const dataKey = series.id.toString(); - - return ( - { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); } - activeDot={isStandard ? undefined : { r: 5 }} + }); + }); + + if (allValues.length === 0) return [0, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + + // Add padding (10% on each side) + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + return [domainMin, domainMax]; + })()} + ticks={(() => { + // Calculate dynamic ticks based on domain + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } + + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; + + dataset.forEach((item: DashboardChartsDataset) => { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); + } + }); + }); + + if (allValues.length === 0) return [0, 25, 50, 75, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + // Generate 5 evenly spaced ticks + const range = domainMax - domainMin; + const step = range / 4; + + return [ + domainMin, + Math.round(domainMin + step), + Math.round(domainMin + step * 2), + Math.round(domainMin + step * 3), + domainMax, + ]; + })()} + /> + `Week ${value}`} + formatter={( + value: number | undefined, + name: string | undefined + ) => { + if (value === undefined || name === undefined) return ['', '']; + + // Get series data to find the unit + let seriesData: DashboardChartsSeries[] = []; + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + // Find the series that matches this line's name + const series = seriesData.find((s) => s.label === name); + const unit = series?.unit || ''; + + return [`${value} ${unit}`, name]; + }} + /> + {/* Dynamic Line rendering based on visible series */} + {(() => { + let seriesData: DashboardChartsSeries[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + return seriesData + .filter((series) => visibleSeries.has(series.id)) + .map((series, index) => { + const isStandard = series.id + .toString() + .toLowerCase() + .includes('std'); + // Use series.id directly as dataKey to match dataset fields + const dataKey = series.id.toString(); + + return ( + + ); + }); + })()} + + + + {/* Empty State Overlay */} + {(() => { + // Get current dataset + let dataset: DashboardChartsDataset[] = []; + + if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || data.charts.flock || data.charts.kandang; + dataset = comparisonChart?.dataset || []; + } + + // Show empty state if dataset is empty + if (dataset.length === 0) { + return ( +
+ {/* Chart icon */} +
+ - ); - }); - })()} - - +
+ + {/* Empty state text */} +

+ Data Not Yet Available +

+

+ Please change your filters to get the data. +

+
+ ); + } + return null; + })()} +
); }; diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index c1fc25da..3bae393d 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { CreateInventoryAdjustmentPayload, @@ -22,12 +22,18 @@ import { } from '@/services/api/master-data'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; import TextInput from '@/components/input/TextInput'; import { RadioGroup } from '@/components/input/RadioInput'; import TextArea from '@/components/input/TextArea'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Product } from '@/types/api/master-data/product'; +import { Warehouse } from '@/types/api/master-data/warehouse'; interface InventoryAdjustmentFormProps { type?: 'add' | 'edit' | 'detail'; @@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({ InventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage, ] = useState(''); - const [selectedProductCategories, setSelectedProductCategories] = - useState(''); const [disabledProduct, setDisabledProduct] = useState(true); - const [optionsProduct, setOptionsProduct] = useState([]); const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); // Submit Handler @@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({ }); // Fetch Data - const productCategoriesUrl = `${ - ProductCategoryApi.basePath - }?${new URLSearchParams({ - search: '', - }).toString()}`; - const { data: productCategories, isLoading: isLoadingProductCategories } = - useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher); + const { + setInputValue: setProductCategoryInputValue, + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategoryOptions, + loadMore: loadMoreProductCategories, + } = useSelect(ProductCategoryApi.basePath, 'id', 'name'); - const productUrl = `${ProductApi.basePath}?${new URLSearchParams({ - search: '', - product_category_id: selectedProductCategories, - }).toString()}`; - const { data: products, isLoading: isLoadingProducts } = useSWR( - productUrl, - ProductApi.getAllFetcher - ); + const { + setInputValue: setProductInputValue, + options: productOptions, + isLoadingOptions: isLoadingProductOptions, + loadMore: loadMoreProducts, + } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { + product_category_id: formik.values.product_category_id + ? String(formik.values.product_category_id) + : '', + }); - const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ - search: '', - limit: '100', - }).toString()}`; - const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( - warehouseUrl, - WarehouseApi.getAllFetcher - ); - - // Map Data to Options - const optionsProductCategory = isResponseSuccess(productCategories) - ? productCategories?.data.map((productCategory) => ({ - value: productCategory.id, - label: productCategory.name, - })) - : []; - const optionsWarehouse = isResponseSuccess(warehouses) - ? warehouses?.data.map((warehouse) => ({ - value: warehouse.id, - label: warehouse.name, - })) - : []; + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name'); // Options Handler const productCategoryChangeHandler = ( @@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({ formik.setFieldValue('product_category', val); - setSelectedProductCategories((val as OptionType)?.value as string); const disabled = (val as OptionType)?.value == null; setDisabledProduct(disabled); formik.setFieldValue('product_id', 0); @@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({ // Effect useEffect(() => { if (initialValues?.product_warehouse?.product?.id) { - setSelectedProductCategories( - String(initialValues.product_warehouse.product.id) - ); setDisabledProduct(false); formik.setFieldValue( 'product_id', @@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({ ); formik.setFieldValue('note', initialValues.note); } - }, [ - formik, - initialValues, - setQuantityLabel, - setDisabledProduct, - setSelectedProductCategories, - ]); + }, [formik, initialValues, setQuantityLabel, setDisabledProduct]); useEffect(() => { formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); }, [formikSetValues, formikInitialValues]); - useEffect(() => { - if (isResponseSuccess(products)) { - const options = products.data.map((p) => ({ - value: p.id, - label: p.name, - })); - setOptionsProduct(options); - } - }, [products]); // Utils Function const formatNumber = (value: string) => { @@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({ label='Kategori Produk' value={formik.values.product_category as OptionType} onChange={productCategoryChangeHandler} - onInputChange={setSelectedProductCategories} - options={optionsProductCategory} - isLoading={isLoadingProductCategories} + onInputChange={setProductCategoryInputValue} + options={productCategoryOptions} + onMenuScrollToBottom={loadMoreProductCategories} + isLoading={isLoadingProductCategoryOptions} isError={ formik.touched.product_category && Boolean(formik.errors.product_category) @@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({ label='Produk' value={formik.values.product as OptionType} onChange={productChangeHandler} - options={optionsProduct} - isLoading={isLoadingProducts} + onInputChange={setProductInputValue} + options={productOptions} + onMenuScrollToBottom={loadMoreProducts} + isLoading={isLoadingProductOptions} isError={formik.touched.product && Boolean(formik.errors.product)} errorMessage={formik.errors.product as string} isDisabled={type === 'detail' || disabledProduct} @@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({ label='Warehouse' value={formik.values.warehouse as OptionType} onChange={warehouseChangeHandler} - options={optionsWarehouse} - isLoading={isLoadingWarehouses} + onInputChange={setWarehouseInputValue} + options={warehouseOptions} + onMenuScrollToBottom={loadMoreWarehouses} + isLoading={isLoadingWarehouseOptions} isError={ formik.touched.warehouse && Boolean(formik.errors.warehouse) } diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index d9aef6cd..40e08c5d 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -38,6 +38,8 @@ import Card from '@/components/Card'; import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { // ===== STATE MANAGEMENT ===== const [movementFormErrorMessage, setMovementFormErrorMessage] = useState(''); - const [ - productWarehouseSelectInputValue, - setProductWarehouseSelectInputValue, - ] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [selectedDeliveries, setSelectedDeliveries] = useState([]); const [formErrorList, setFormErrorList] = useState([]); @@ -93,10 +91,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { // ===== USE SELECT HOOKS ===== const { - inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, isLoadingOptions: isLoadingWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + loadMore: loadMoreWarehouses, + rawData: warehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); // ===== SELECT INPUT DATA ===== const { @@ -107,12 +106,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { category: 'BOP', }); - const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`; - const { data: warehouses } = useSWR( - warehousesUrl, - WarehouseApi.getAllFetcher - ); - // ===== DATA PROCESSING ===== const warehouseStockMap = useMemo(() => { if (!isResponseSuccess(allProductWarehouses)) return new Map(); @@ -269,25 +262,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }); // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== - const getProductWarehousesUrl = useCallback(() => { - const productWarehouseParams = new URLSearchParams({ - search: productWarehouseSelectInputValue, - }); - if (formik.values.source_warehouse_id) { - productWarehouseParams.append( - 'warehouse_id', - formik.values.source_warehouse_id.toString() - ); + const { + setInputValue: setProductWarehouseSelectInputValue, + isLoadingOptions: isLoadingProductWarehouses, + loadMore: loadMoreProductWarehouses, + rawData: productWarehouses, + } = useSelect( + formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null, + 'id', + 'name', + 'search', + { + warehouse_id: formik.values.source_warehouse_id + ? formik.values.source_warehouse_id.toString() + : '', } - return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`; - }, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]); - - const productWarehousesUrl = getProductWarehousesUrl(); - const { data: productWarehouses, isLoading: isLoadingProductWarehouses } = - useSWR( - formik.values.source_warehouse_id ? productWarehousesUrl : null, - ProductWarehouseApi.getAllFetcher - ); + ); const productWarehouseOptions = isResponseSuccess(productWarehouses) ? productWarehouses?.data.map((pw) => ({ @@ -1006,6 +996,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }} options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} + onMenuScrollToBottom={loadMoreWarehouses} isLoading={isLoadingWarehouses} isError={ formik.touched.source_warehouse_id && @@ -1104,6 +1095,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} isLoading={isLoadingWarehouses} + onMenuScrollToBottom={loadMoreWarehouses} isError={ formik.touched.destination_warehouse_id && Boolean(formik.errors.destination_warehouse_id) @@ -1263,6 +1255,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }} options={productWarehouseOptions} onInputChange={setProductWarehouseSelectInputValue} + onMenuScrollToBottom={loadMoreProductWarehouses} isLoading={isLoadingProductWarehouses} isDisabled={ type === 'detail' || diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index d81cdb9c..b09129c3 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -11,6 +11,13 @@ import { type MarketingSchemaType = { customer_id: number | undefined; sales_person_id: number | undefined; + sales_person: + | { + value: number; + label: string; + } + | undefined + | null; customer: | { value: number; @@ -33,7 +40,11 @@ type DeliveryOrderSchemaType = { export const SalesOrderSchema: Yup.ObjectSchema = Yup.object({ customer_id: Yup.number().required('Customer wajib diisi!'), - sales_person_id: Yup.number().required('Sales Person wajib diisi!'), + sales_person_id: Yup.number().required('Sales wajib diisi!'), + sales_person: Yup.object({ + value: Yup.number().required(), + label: Yup.string().required(), + }).nullable(), customer: Yup.object({ value: Yup.number().required(), label: Yup.string().required(), diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index 2fbca835..be4367cb 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -50,6 +50,8 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for import RequirePermission from '@/components/helper/RequirePermission'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import { CreatedUser } from '@/types/api/api-general'; +import { UserApi } from '@/services/api/user'; const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); @@ -244,7 +246,15 @@ const MarketingForm = ({ const { options: customerOptions, isLoadingOptions: isLoadingCustomerOptions, + setInputValue: setInputCustomerValue, + loadMore: loadMoreCustomer, } = useSelect(CustomerApi.basePath, 'id', 'name'); + const { + options: salesOptions, + isLoadingOptions: isLoadingSalesOptions, + setInputValue: setInputSalesValue, + loadMore: loadMoreSales, + } = useSelect(UserApi.basePath, 'id', 'name'); // ================== SETUP FORMIK ================== const formikInitialValues = useMemo< @@ -255,6 +265,12 @@ const MarketingForm = ({ notes: initialValues?.notes || undefined, customer_id: initialValues?.customer?.id || undefined, sales_person_id: initialValues?.sales_person?.id || 1, + sales_person: initialValues?.sales_person + ? { + value: initialValues.sales_person.id, + label: initialValues.sales_person.name, + } + : null, customer: initialValues?.customer ? { value: initialValues.customer.id, @@ -443,6 +459,13 @@ const MarketingForm = ({ }, [] ); + const handleChangeSalesPerson = useCallback( + (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('sales_person_id', (val as OptionType)?.value); + formik.setFieldValue('sales_person', val as OptionType); + }, + [] + ); const handleDelete = useCallback(() => { deleteModal.openModal(); }, [deleteModal]); @@ -580,6 +603,7 @@ const MarketingForm = ({ className={{ wrapper: 'bg-white w-full', }} + variant='bordered' >
- -
+
+ + +
+
Total Penjualan {formatCurrency(grandTotal)}{' '} diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index 25a20982..da3b2fee 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -18,6 +18,11 @@ import * as Yup from 'yup'; import { isResponseSuccess } from '@/lib/api-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import useSWR from 'swr'; +import { ProductApi } from '@/services/api/master-data'; + +const roundWeight = (value: number) => Number(value.toFixed(2)); +const roundPrice = (value: number) => Math.round(value); const DeliveryOrderProductForm = ({ formState, @@ -43,6 +48,17 @@ const DeliveryOrderProductForm = ({ ); const [currentInput, setCurrentInput] = useState(''); + // ============ Fetch Data ============ + const { data: productData } = useSWR( + selectedProduct?.value + ? ProductApi.basePath + '/' + selectedProduct?.value + : null, + () => + selectedProduct?.value + ? ProductApi.getSingle(Number(selectedProduct?.value)) + : undefined + ); + const salesOrder = salesOrders.find( (item) => item.id === initialValues?.marketing_product_id ); @@ -113,22 +129,60 @@ const DeliveryOrderProductForm = ({ const handleBlurField = (field: string) => { setCurrentInput(field); - const { qty, unit_price, total_price, avg_weight, total_weight } = - formik.values; - if (field === 'unit_price' || field === 'total_price' || field === 'qty') { - if (qty && unit_price && (field === 'unit_price' || field === 'qty')) { - formik.setFieldValue('total_price', Number(qty) * Number(unit_price)); - } else if (qty && total_price && field === 'total_price') { - formik.setFieldValue('unit_price', Number(total_price) / Number(qty)); + const qty = Number(formik.values.qty || 0); + const avgWeight = Number(formik.values.avg_weight || 0); + const totalWeight = Number(formik.values.total_weight || 0); + const unitPrice = Number(formik.values.unit_price || 0); + const totalPrice = Number(formik.values.total_price || 0); + + if (qty <= 0) return; + + switch (field) { + // ===== SOURCE FIELDS ===== + case 'qty': { + if (avgWeight > 0) { + formik.setFieldValue('total_weight', roundWeight(qty * avgWeight)); + } + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; } - } - if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') { - if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) { - formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight)); - } else if (qty && total_weight && field === 'total_weight') { - formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty)); + case 'avg_weight': { + if (avgWeight > 0) { + const tw = roundWeight(qty * avgWeight); + formik.setFieldValue('total_weight', tw); + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + } + break; + } + + case 'unit_price': { + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; + } + + // ===== TOTAL EDITABLE ===== + case 'total_weight': { + if (totalWeight > 0) { + formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty)); + } + break; + } + + case 'total_price': { + if (totalPrice > 0) { + formik.setFieldValue('unit_price', roundPrice(totalPrice / qty)); + } + break; } } }; @@ -183,7 +237,7 @@ const DeliveryOrderProductForm = ({
)} -
+
- +
+
+
+ + {isResponseSuccess(productData) + ? productData?.data?.uom.name + : ''} + +
+ } bottomLabel={ formik.values.marketing_product_id ? 'Stok dijual: ' + salesOrders?.find( (item) => item.id === formik.values.marketing_product_id - )?.qty + )?.qty + + ' ' + + (isResponseSuccess(productData) + ? productData?.data?.uom.name + : '') : '' } /> -
-
-
- { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('avg_weight')} - isError={Boolean(formik.errors.avg_weight)} - errorMessage={formik.errors.avg_weight} - placeholder='Masukan Bobot Rata-rata' - /> - - + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('avg_weight')} + isError={Boolean(formik.errors.avg_weight)} + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> Number(value.toFixed(2)); +const roundPrice = (value: number) => Math.round(value); const SalesOrderProductForm = ({ initialValues, @@ -39,6 +43,19 @@ const SalesOrderProductForm = ({ }) => { const [formErrorMessage, setFormErrorMessage] = useState(''); const [currentInput, setCurrentInput] = useState(''); + const [selectedProductWarehouse, setSelectedProductWarehouse] = + useState(null); + + // ============ Fetch Data ============ + const { data: productData } = useSWR( + selectedProductWarehouse?.product_id + ? ProductApi.basePath + '/' + selectedProductWarehouse?.product_id + : null, + () => + selectedProductWarehouse?.product_id + ? ProductApi.getSingle(selectedProductWarehouse?.product_id) + : undefined + ); // ============ Formik ============ const formik = useFormik({ @@ -69,17 +86,21 @@ const SalesOrderProductForm = ({ const { options: kandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions, + setInputValue: setKandangInputValue, + loadMore: loadMoreKandang, } = useSelect(WarehouseApi.basePath, 'id', 'name'); const { options: warehouseSourceOptions, rawData: warehouseSourceRawData, isLoadingOptions: isLoadingWarehouseSourceOptions, + setInputValue: setWarehouseInputValue, + loadMore: loadMoreWarehouse, } = useSelect( ProductWarehouseApi.basePath, 'id', 'product.name', - 'search', + '', { warehouse_id: formik.values.kandang_id?.toString() ?? '', } @@ -112,6 +133,7 @@ const SalesOrderProductForm = ({ const productWarehouse = warehouseSourceRawData?.data.find( (item: ProductWarehouse) => item.id === newId ); + setSelectedProductWarehouse(productWarehouse || null); formik.setFieldValue('qty', productWarehouse?.quantity); handleBlurField('qty'); } else { @@ -139,34 +161,60 @@ const SalesOrderProductForm = ({ const handleBlurField = (field: string) => { setCurrentInput(field); - const { qty, unit_price, total_price, avg_weight, total_weight } = - formik.values; - if (field === 'unit_price' || field === 'total_price' || field === 'qty') { - if (qty && unit_price && (field === 'unit_price' || field === 'qty')) { - formik.setFieldValue( - 'total_price', - (qty as number) * (unit_price as number) - ); - } else if (qty && total_price && field === 'total_price') { - formik.setFieldValue( - 'unit_price', - (total_price as number) / (qty as number) - ); + const qty = Number(formik.values.qty || 0); + const avgWeight = Number(formik.values.avg_weight || 0); + const totalWeight = Number(formik.values.total_weight || 0); + const unitPrice = Number(formik.values.unit_price || 0); + const totalPrice = Number(formik.values.total_price || 0); + + if (qty <= 0) return; + + switch (field) { + // ===== SOURCE FIELDS ===== + case 'qty': { + if (avgWeight > 0) { + formik.setFieldValue('total_weight', roundWeight(qty * avgWeight)); + } + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; } - } - if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') { - if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) { - formik.setFieldValue( - 'total_weight', - (qty as number) * (avg_weight as number) - ); - } else if (qty && total_weight && field === 'total_weight') { - formik.setFieldValue( - 'avg_weight', - (total_weight as number) / (qty as number) - ); + case 'avg_weight': { + if (avgWeight > 0) { + const tw = roundWeight(qty * avgWeight); + formik.setFieldValue('total_weight', tw); + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + } + break; + } + + case 'unit_price': { + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; + } + + // ===== TOTAL EDITABLE ===== + case 'total_weight': { + if (totalWeight > 0) { + formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty)); + } + break; + } + + case 'total_price': { + if (totalPrice > 0) { + formik.setFieldValue('unit_price', roundPrice(totalPrice / qty)); + } + break; } } }; @@ -188,7 +236,7 @@ const SalesOrderProductForm = ({
)} -
+
+
+
+
+ + {isResponseSuccess(productData) + ? productData?.data?.uom.name + : ''} + +
+ } bottomLabel={ isResponseSuccess(warehouseSourceRawData) && formik.values.product_warehouse_id @@ -264,32 +328,13 @@ const SalesOrderProductForm = ({ (item) => item.id === formik.values.product_warehouse_id )?.quantity ?? 0 )} ${ - warehouseSourceRawData?.data?.find( - (item) => item.id === formik.values.product_warehouse_id - )?.product?.uom?.name ?? '' + isResponseSuccess(productData) + ? productData?.data?.uom.name + : '' }` : '' } /> -
-
-
- { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('avg_weight')} - isError={ - formik.touched.avg_weight && Boolean(formik.errors.avg_weight) - } - errorMessage={formik.errors.avg_weight} - placeholder='Masukan Bobot Rata-rata' - /> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('avg_weight')} + isError={ + formik.touched.avg_weight && Boolean(formik.errors.avg_weight) + } + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama kandang' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index cd2c361b..4622a6a3 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama nonstock' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index 9dcf713e..8a1d3de2 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -3,7 +3,7 @@ import * as Yup from 'yup'; type ProductFormSchemaType = { name: string; brand: string; - sku: string; + sku?: string; uom?: { value: number; label: string; @@ -15,10 +15,16 @@ type ProductFormSchemaType = { } | null; product_category_id: number; product_price: number | string; - selling_price: number | string; - tax: number | string; - expiry_period: number | string; - supplier_ids: number[]; + selling_price?: number | string; + tax?: number | string; + expiry_period?: number | string; + suppliers: { + supplier: { + value: number; + label: string; + } | null; + price: number; + }[]; flags: string[]; }; @@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema = Yup.object({ name: Yup.string().required('Nama wajib diisi!'), brand: Yup.string().required('Merek wajib diisi!'), - sku: Yup.string().required('SKU wajib diisi!'), + sku: Yup.string(), uom: Yup.object({ value: Yup.number() @@ -58,24 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema = .min(1, 'Harga produk tidak boleh kurang dari 1!'), selling_price: Yup.number() - .required('Harga jual wajib diisi!') - .typeError('Harga jual wajib diisi!') + .typeError('Harga hanya boleh angka!') .min(1, 'Harga jual tidak boleh kurang dari 1!'), tax: Yup.number() - .required('Pajak wajib diisi!') - .typeError('Pajak wajib diisi!') + .typeError('Pajak hanya boleh angka!') .min(0, 'Pajak tidak boleh kurang dari 0!') .max(100, 'Pajak tidak boleh lebih dari 100%!'), expiry_period: Yup.number() - .required('Periode kadaluarsa wajib diisi!') - .typeError('Periode kadaluarsa wajib diisi!') + .typeError('Periode kadaluarsa hanya boleh angka!') .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'), - supplier_ids: Yup.array() - .of(Yup.number().required().typeError('Supplier tidak valid!')) - .min(1, 'Minimal harus ada 1 supplier!') + suppliers: Yup.array() + .of( + Yup.object({ + supplier: Yup.object({ + value: Yup.number() + .min(1, 'Supplier wajib dipilih!') + .required('Supplier wajib dipilih!') + .typeError('Supplier wajib dipilih!'), + label: Yup.string().required('Supplier wajib dipilih!'), + }).required('Supplier wajib dipilih!'), + price: Yup.number() + .min(1, 'Harga tidak boleh kurang dari 1!') + .required('Harga wajib diisi!') + .typeError('Harga wajib diisi!'), + }) + ) .required('Supplier wajib diisi!'), flags: Yup.array() diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 2fc3b267..8c04d594 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -41,6 +41,8 @@ import { cn } from '@/lib/helper'; import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { Supplier } from '@/types/api/master-data/supplier'; +import Card from '@/components/Card'; +import { removeArrayItemAndSync } from '@/lib/utils/formik'; interface ProductFormProps { type?: 'add' | 'edit' | 'detail'; @@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { selling_price: initialValues?.selling_price ?? '', tax: initialValues?.tax ?? '', expiry_period: initialValues?.expiry_period ?? '', - supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [], + suppliers: initialValues?.suppliers + ? initialValues.suppliers.map((supplier) => ({ + supplier: { + value: supplier.id, + label: supplier.name, + }, + price: supplier.price, + })) + : [], flags: initialValues?.flags ?? [], }), [initialValues] @@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { uom_id: values.uom_id, product_category_id: values.product_category_id, product_price: parseInt(values.product_price.toString()) || 0, - selling_price: parseInt(values.selling_price.toString()) || 0, - tax: parseInt(values.tax.toString()) || 0, - expiry_period: parseInt(values.expiry_period.toString()) || 0, - supplier_ids: values.supplier_ids.filter( - (id): id is number => typeof id === 'number' - ), + selling_price: values.selling_price + ? parseInt(values.selling_price.toString()) || 0 + : undefined, + tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined, + expiry_period: values.expiry_period + ? parseInt(values.expiry_period.toString()) || 0 + : undefined, + suppliers: values.suppliers.map((s) => ({ + supplier_id: s.supplier?.value as number, + price: parseInt(s.price.toString()) || 0, + })), flags: values.flags.filter((f): f is string => typeof f === 'string'), }; switch (type) { @@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { category: 'SAPRONAK', }); - const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - formik.setFieldTouched('supplier_ids', true); - formik.setFieldValue( - 'supplier_ids', - arr.map((v) => (v as OptionType).value) - ); + const filteredSupplierOptions = useMemo(() => { + return supplierOptions.filter((opt) => { + return !formik.values.suppliers.some( + (s) => s.supplier?.value === opt.value + ); + }); + }, [supplierOptions, formik.values.suppliers]); + + const addSupplierHandler = () => { + formik.setFieldValue('suppliers', [ + ...formik.values.suppliers, + { + supplier_id: '', + price: formik.values.product_price, + }, + ]); + }; + + const deleteSupplierItemHandler = (idx: number) => { + const path = 'suppliers'; + + // trims values, errors, and touched at idx + removeArrayItemAndSync(formik, path, idx); }; const deleteProductClickHandler = () => { @@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { router.push('/master-data/product'); }; + const isSupplierRepeaterError = ( + column: 'supplier' | 'price', + supplierIdx: number + ) => { + return ( + formik.touched.suppliers?.[supplierIdx]?.[column] && + Boolean( + formik.errors.suppliers?.[supplierIdx] instanceof Object && + formik.errors.suppliers?.[supplierIdx]?.[column] + ) + ); + }; + useEffect(() => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); @@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { readOnly={type === 'detail'} /> { readOnly={type === 'detail'} /> {
{ readOnly={type === 'detail'} /> { readOnly={type === 'detail'} />
-
- - (formik.values.supplier_ids || []).includes(opt.value) - )} - onChange={supplierChangeHandler} - options={supplierOptions} - onInputChange={setSupplierSelectInputValue} - onMenuScrollToBottom={loadMoreSuppliers} - isLoading={isLoadingSuppliers} - isError={ - formik.touched.supplier_ids && - Boolean(formik.errors.supplier_ids) - } - errorMessage={formik.errors.supplier_ids as string} - isDisabled={type === 'detail'} - isClearable - /> +
{ isClearable />
+ +
+ {type !== 'detail' && formik.values.suppliers.length === 0 && ( + + )} + + {formik.values.suppliers.length > 0 && ( + +
+

Supplier

+
+ +
+ + + + + + + + + + + {formik.values.suppliers.map((supplier, idx) => ( + + + + {type !== 'detail' && ( + + )} + + ))} + +
+ Supplier + + Harga + Aksi
+ { + formik.setFieldValue( + `suppliers.${idx}.supplier`, + val + ); + }} + isError={isSupplierRepeaterError( + 'supplier', + idx + )} + isClearable + isDisabled={type === 'detail'} + className={{ + wrapper: 'min-w-48 w-full', + }} + /> + + + + +
+
+ + {type !== 'detail' && ( +
+ +
+ )} +
+ )} +
{type !== 'add' && ( diff --git a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx index cab9f750..a6a53e3f 100644 --- a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx +++ b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx @@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama warehouse' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4f9018fc..80549f4e 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -563,7 +563,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { todayRecordings.forEach((recording) => { const recordingDate = recording.record_datetime?.split('T')[0]; if (recordingDate === today) { - recordedIds.add(recording.project_flock.project_flock_kandang_id); + recordedIds.add(recording.project_flock?.project_flock_kandang_id); } }); diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx index d17df01e..01c360d0 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -87,6 +87,7 @@ const DailyMarketingReportContent = () => { setInputValue: setAreaInputValue, options: areaOptions, isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, } = useSelect(AreaApi.basePath, 'id', 'name'); const areaChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -101,6 +102,7 @@ const DailyMarketingReportContent = () => { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); const locationChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -118,6 +120,7 @@ const DailyMarketingReportContent = () => { setInputValue: setWarehouseInputValue, options: warehouseOptions, isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, } = useSelect(WarehouseApi.basePath, 'id', 'name'); const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -135,6 +138,7 @@ const DailyMarketingReportContent = () => { setInputValue: setCustomerInputValue, options: customerOptions, isLoadingOptions: isLoadingCustomerOptions, + loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name'); const customerChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -298,6 +302,7 @@ const DailyMarketingReportContent = () => { value={selectedArea} onChange={areaChangeHandler} onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -312,6 +317,7 @@ const DailyMarketingReportContent = () => { value={selectedLocation} onChange={locationChangeHandler} onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -326,6 +332,7 @@ const DailyMarketingReportContent = () => { value={selectedWarehouse} onChange={warehouseChangeHandler} onInputChange={setWarehouseInputValue} + onMenuScrollToBottom={loadMoreWarehouses} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -340,6 +347,7 @@ const DailyMarketingReportContent = () => { value={selectedCustomer} onChange={customerChangeHandler} onInputChange={setCustomerInputValue} + onMenuScrollToBottom={loadMoreCustomers} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', diff --git a/src/components/pages/report/expense/ReportExpenseTable.tsx b/src/components/pages/report/expense/ReportExpenseTable.tsx index c34072a2..c809c153 100644 --- a/src/components/pages/report/expense/ReportExpenseTable.tsx +++ b/src/components/pages/report/expense/ReportExpenseTable.tsx @@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem'; import * as XLSX from 'xlsx'; import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; import toast from 'react-hot-toast'; +import { + KandangApi, + LocationApi, + NonstockApi, + SupplierApi, +} from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Nonstock } from '@/types/api/master-data/nonstock'; const ReportExpenseTable = () => { // ===== STATE MANAGEMENT ===== @@ -64,16 +73,33 @@ const ReportExpenseTable = () => { }); // ===== SELECT OPTIONS ===== - const { options: optionsLocation, isLoadingOptions: isLoadingLocation } = - useSelect(`/master-data/locations`, 'id', 'name'); - const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } = - useSelect(`/master-data/suppliers`, 'id', 'name'); - const { options: optionsKandang, isLoadingOptions: isLoadingKandang } = - useSelect(`/master-data/kandangs`, 'id', 'name', '', { - location_id: filterState.location_id, - }); - const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } = - useSelect(`/master-data/nonstocks`, 'id', 'name'); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name'); + + const { + setInputValue: setNonstockInputValue, + options: nonstockOptions, + isLoadingOptions: isLoadingNonstockOptions, + loadMore: loadMoreNonstocks, + } = useSelect(NonstockApi.basePath, 'id', 'name'); const categoryOptions = useMemo( () => [ @@ -86,31 +112,31 @@ const ReportExpenseTable = () => { // Mendapatkan value option select dari filter state const selectedLocation = useMemo( () => - optionsLocation.find( + locationOptions.find( (opt) => String(opt.value) === filterState.location_id ) || null, - [optionsLocation, filterState.location_id] + [locationOptions, filterState.location_id] ); const selectedSupplier = useMemo( () => - optionsSupplier.find( + supplierOptions.find( (opt) => String(opt.value) === filterState.supplier_id ) || null, - [optionsSupplier, filterState.supplier_id] + [supplierOptions, filterState.supplier_id] ); const selectedKandang = useMemo( () => - optionsKandang.find( + kandangOptions.find( (opt) => String(opt.value) === filterState.kandang_id ) || null, - [optionsKandang, filterState.kandang_id] + [kandangOptions, filterState.kandang_id] ); const selectedNonstock = useMemo( () => - optionsNonstock.find( + nonstockOptions.find( (opt) => String(opt.value) === filterState.nonstock_id ) || null, - [optionsNonstock, filterState.nonstock_id] + [nonstockOptions, filterState.nonstock_id] ); const selectedCategory = useMemo( () => @@ -756,38 +782,46 @@ const ReportExpenseTable = () => { = { + 'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red + 'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green + 'Mendekati Jatuh Tempo': { + bg: '#FEF3C7', + text: '#92400E', + border: '#FBBF24', + }, // warning/yellow +}; + +const paymentStatusColors: Record< + string, + { bg: string; text: string; border: string } +> = { + 'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow + Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue + Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green +}; + +/** + * Get badge style for PDF rendering + * @param statusText - The status text + * @param type - Type of status: 'due' or 'payment' + * @returns Style object with background and text colors + */ +const getPDFBadgeStyle = ( + statusText: string, + type: 'due' | 'payment' = 'payment' +) => { + const colors = + type === 'due' + ? dueStatusColors[statusText] + : paymentStatusColors[statusText]; + + return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback +}; + const pdfStyles = StyleSheet.create({ page: { fontSize: 10, @@ -136,10 +177,40 @@ const pdfStyles = StyleSheet.create({ backgroundColor: '#F0F0F0', fontWeight: 'bold', }, + badge: { + paddingVertical: 2, + paddingHorizontal: 4, + borderRadius: 12, + fontSize: 5, + fontWeight: 'bold', + borderWidth: 1, + textAlign: 'center', + whiteSpace: 'nowrap', + }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, }); interface DebtSupplierExportPDFParams { data: DebtSupplier[]; + params?: { + supplier_name?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + }; } const createPDFDocument = (params: DebtSupplierExportPDFParams) => { @@ -157,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Laporan > Rekapitulasi Hutang ke Supplier + + + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + + + {params.params?.filter_by && ( + + + Filter Tanggal:{' '} + {params.params.filter_by === 'po_date' + ? 'Tanggal PO' + : params.params.filter_by === 'received_date' + ? 'Tanggal Terima' + : params.params.filter_by === 'due_date' + ? 'Tanggal Jatuh Tempo' + : params.params.filter_by} + + + )} + + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + {supplierReport.supplier.name} + + {supplierReport.supplier.category} + {/* Table */} @@ -193,7 +305,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Jatuh Tempo - + Status Jatuh Tempo @@ -205,7 +317,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Sisa Saldo Hutang (Rp) - + Status @@ -216,40 +328,40 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* Initial Balance Row */} - + {/* NO */} - + {/* No. PR */} - + {/* No. PO */} - + {/* Tgl Terima/Bayar */} - + {/* Tgl PO */} - + {/* Aging */} - + {/* Area */} - + {/* Gudang */} - + {/* Jatuh Tempo */} - - + + {/* Status Jatuh Tempo */} - + {/* Nominal Pembelian (Rp) */} - + {/* Pembayaran (Rp) */} { ]} > + {' '} + {/* Sisa Saldo Hutang (Rp) */} {formatCurrency(supplierReport.initial_balance || 0)} - - + + {/* Status */} - + {/* No. Perjalanan */} @@ -328,8 +442,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { : '-'} - - {item.due_status || '-'} + + {item.due_status && item.due_status !== '-' ? ( + + + {item.due_status} + + + ) : ( + - + )} { > {formatCurrency(item.balance)} - - {item.status || '-'} + + {item.status && item.status !== '-' ? ( + + + {item.status} + + + ) : ( + - + )} {item.travel_number || '-'} @@ -400,7 +562,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { - + { > {formatCurrency(supplierReport.total.debt_price)} - + diff --git a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx index 3ba96a22..39e0cec4 100644 --- a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -64,7 +64,7 @@ export const generateDebtSupplierExcel = ( 'Status Jatuh Tempo': item.due_status || '', 'Nominal Pembelian (Rp)': item.total_price || 0, 'Pembayaran (Rp)': item.payment_price || 0, - 'Sisa Saldo Hutang (Rp)': item.debt_price || 0, + 'Sisa Saldo Hutang (Rp)': item.balance || 0, Status: item.status || '', 'Nomor Perjalanan': item.travel_number || '', })), @@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = ( const colWidths = [ { wch: 5 }, // No - { wch: 15 }, // Nomor PR - { wch: 15 }, // Nomor PO - { wch: 15 }, // Tanggal Terima/Bayar - { wch: 15 }, // Tanggal PO - { wch: 12 }, // Aging + { wch: 10 }, // Nomor PR + { wch: 10 }, // Nomor PO + { wch: 20 }, // Tanggal Terima/Bayar + { wch: 10 }, // Tanggal PO + { wch: 10 }, // Aging { wch: 15 }, // Area { wch: 15 }, // Gudang - { wch: 18 }, // Jatuh Tempo - { wch: 18 }, // Status Jatuh Tempo - { wch: 15 }, // Nominal Pembelian (Rp) + { wch: 12 }, // Jatuh Tempo + { wch: 20 }, // Status Jatuh Tempo + { wch: 20 }, // Nominal Pembelian (Rp) { wch: 15 }, // Pembayaran (Rp) - { wch: 15 }, // Sisa Saldo Hutang (Rp) + { wch: 20 }, // Sisa Saldo Hutang (Rp) { wch: 12 }, // Status { wch: 15 }, // Nomor Perjalanan ]; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 9119e80d..adc5b375 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -55,6 +55,7 @@ const CustomerPaymentTab = () => { const { options: customerOptions, + setInputValue: setCustomerInputValue, isLoadingOptions: isLoadingCustomers, loadMore: loadMoreCustomers, hasMore: hasMoreCustomers, @@ -62,6 +63,7 @@ const CustomerPaymentTab = () => { const { options: salesOptions, + setInputValue: setSalesInputValue, isLoadingOptions: isLoadingSales, loadMore: loadMoreSales, hasMore: hasMoreSales, @@ -654,6 +656,7 @@ const CustomerPaymentTab = () => { Array.isArray(val) ? val : val ? [val] : [] ); }} + onInputChange={setCustomerInputValue} isLoading={isLoadingCustomers} isClearable onMenuScrollToBottom={loadMoreCustomers} @@ -670,6 +673,7 @@ const CustomerPaymentTab = () => { onChange={(val) => { setFilterSales(Array.isArray(val) ? val : val ? [val] : []); }} + onInputChange={setSalesInputValue} isLoading={isLoadingSales} isClearable onMenuScrollToBottom={loadMoreSales} diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 2214ecd6..9fefa9c7 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -9,9 +9,9 @@ import SelectInput, { import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; -import Table from '@/components/Table'; +import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { SupplierApi } from '@/services/api/master-data'; import { DebtRow, @@ -31,8 +31,47 @@ import { DebtSupplierFilterSchema, DebtSupplierFilterType, } from '@/components/pages/report/finance/filter/DebtSupplierFilter'; -import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import Badge from '@/components/Badge'; +import { Color } from '@/types/theme'; +import { Supplier } from '@/types/api/master-data/supplier'; + +const dueStatus: Record = { + 'Sudah Jatuh Tempo': 'error', + 'Belum Jatuh Tempo': 'success', + 'Mendekati Jatuh Tempo': 'warning', +}; + +const paymentStatus: Record = { + 'Belum Lunas': 'warning', + Lunas: 'primary', + Pembayaran: 'success', +}; + +const getPillBadge = ( + statusText: string, + type: 'due' | 'payment' = 'payment' +) => { + // Get color based on type + const color = + type === 'due' + ? dueStatus[statusText] || 'neutral' + : paymentStatus[statusText] || 'neutral'; + + return ( + + {statusText} + + ); +}; const DebtSupplierTab = () => { // ===== STATE MANAGEMENT ===== @@ -51,10 +90,12 @@ const DebtSupplierTab = () => { const filterModal = useModal(); - const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = - useSelect(SupplierApi.basePath, 'id', 'name', '', { - limit: 'limit', - }); + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name'); const dataTypeOptions = useMemo( () => [ @@ -209,7 +250,17 @@ const DebtSupplierTab = () => { return; } - await generateDebtSupplierPDF({ data: allDataForExport }); + await generateDebtSupplierPDF({ + data: allDataForExport, + params: { + supplier_name: formik.values.supplierIds + ?.map((v) => v.label) + .join(', '), + filter_by: formik.values.filterBy?.label, + start_date: formik.values.startDate || undefined, + end_date: formik.values.endDate || undefined, + }, + }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); @@ -224,6 +275,7 @@ const DebtSupplierTab = () => { header: 'No', enableSorting: false, cell: (props) => props.row.index, + footer: () => 'Total', }, { id: 'pr_number', @@ -328,7 +380,7 @@ const DebtSupplierTab = () => { enableSorting: false, cell: (props) => { const value = props.row.original.due_status; - return value || '-'; + return value ? (value != '-' ? getPillBadge(value, 'due') : '-') : '-'; }, }, { @@ -404,7 +456,11 @@ const DebtSupplierTab = () => { enableSorting: false, cell: (props) => { const value = props.row.original.status; - return value || '-'; + return value + ? value != '-' + ? getPillBadge(value, 'payment') + : '-' + : '-'; }, }, { @@ -472,9 +528,15 @@ const DebtSupplierTab = () => { { renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto mt-4', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableWrapperClassName: 'overflow-x-auto', + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'whitespace-nowrap' + ), + bodyColumnClassName: cn( + TABLE_DEFAULT_STYLING.bodyColumnClassName, + 'whitespace-nowrap' + ), + footerRowClassName: cn( + TABLE_DEFAULT_STYLING.footerRowClassName, + 'bg-white' + ), + footerColumnClassName: cn( + TABLE_DEFAULT_STYLING.footerColumnClassName, + 'whitespace-nowrap p-3' + ), paginationClassName: 'hidden', }} renderCustomRow={(row) => { if (row.index == 0) { return ( - @@ -610,7 +683,9 @@ const DebtSupplierTab = () => { Array.isArray(val) ? val : val ? [val] : null ); }} - isLoading={isLoadingSuppliers} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isLoading={isLoadingSupplierOptions} isClearable className={{ wrapper: 'w-full' }} isError={ diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx index ae6f744b..7820ff53 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/ProductionResultContent.tsx @@ -62,6 +62,7 @@ const ProductionResultContent = () => { setInputValue: setAreaInputValue, options: areaOptions, isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, } = useSelect(AreaApi.basePath, 'id', 'name'); const areaChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -78,6 +79,7 @@ const ProductionResultContent = () => { setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', }); @@ -94,6 +96,7 @@ const ProductionResultContent = () => { setInputValue: setProjectFlockInputValue, options: projectFlockOptions, isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, } = useSelect( ProjectFlockApi.basePath, 'id', @@ -120,6 +123,7 @@ const ProductionResultContent = () => { setInputValue: setProjectFlockKandangInputValue, options: projectFlockKandangOptions, isLoadingOptions: isLoadingProjectFlockKandangOptions, + loadMore: loadMoreProjectFlockKandangs, } = useSelect( ProjectFlockKandangApi.basePath, 'id', @@ -235,6 +239,7 @@ const ProductionResultContent = () => { value={selectedArea} onChange={areaChangeHandler} onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} isClearable className={{ wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', @@ -251,6 +256,7 @@ const ProductionResultContent = () => { value={selectedLocation} onChange={locationChangeHandler} onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isClearable isDisabled={!selectedArea} className={{ @@ -270,6 +276,7 @@ const ProductionResultContent = () => { value={selectedProjectFlock} onChange={projectFlockChangeHandler} onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} isClearable isDisabled={!selectedArea || !selectedLocation} className={{ @@ -289,6 +296,7 @@ const ProductionResultContent = () => { value={selectedProjectFlockKandang} onChange={projectFlockKandangChangeHandler} onInputChange={setProjectFlockKandangInputValue} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} isClearable isDisabled={!selectedProjectFlock} className={{ diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7d6f0951..eda88d8c 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -58,18 +58,26 @@ const HppPerKandangTab = () => { }, }); - const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( - AreaApi.basePath, - 'id', - 'name', - 'search' - ); + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreas, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); - const { options: locationOptions, isLoadingOptions: isLoadingLocations } = - useSelect(LocationApi.basePath, 'id', 'name', 'search'); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search'); + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); const showUnrecordedOptions: OptionType[] = [ { value: 'false', label: 'Sembunyikan' }, @@ -810,6 +818,8 @@ const HppPerKandangTab = () => { .includes(String(opt.value)) )} onChange={areaChangeHandler} + onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} isLoading={isLoadingAreas} isClearable /> @@ -824,6 +834,8 @@ const HppPerKandangTab = () => { .includes(String(opt.value)) )} onChange={locationChangeHandler} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} isLoading={isLoadingLocations} isClearable /> @@ -838,6 +850,8 @@ const HppPerKandangTab = () => { .includes(String(opt.value)) )} onChange={kandangChangeHandler} + onInputChange={setKandangInputValue} + onMenuScrollToBottom={loadMoreKandangs} isLoading={isLoadingKandangs} isClearable /> diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 9a0c9d2e..165bc8ee 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record = { '/dashboard/': ['lti.dashboard.list'], // Daily Checklist - // TODO: use real daily checklist permission name - // '/daily-checklist/': ['lti.daily_checklist.list'], - // '/daily-checklist/dashboard/': ['lti.daily_checklist.list'], - // '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], - // '/daily-checklist/list-daily-checklist/detail/': ['lti.daily_checklist.detail'], - // '/daily-checklist/reports/': ['lti.daily_checklist.reports'], - // '/daily-checklist/master-data/employee/': ['lti.dashboard.master_data.employee'], - // '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'], - '/daily-checklist/dashboard/': ['lti.dashboard.list'], - '/daily-checklist/daily-checklist/': ['lti.dashboard.list'], - '/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'], - '/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'], - '/daily-checklist/reports/': ['lti.dashboard.list'], - '/daily-checklist/master-data/employee/': ['lti.dashboard.list'], - '/daily-checklist/master-data/activity/': ['lti.dashboard.list'], - '/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], + '/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'], + '/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'], + '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], + '/daily-checklist/list-daily-checklist/detail/': [ + 'lti.daily_checklist.detail', + ], + '/daily-checklist/reports/': ['lti.daily_checklist.reports'], + '/daily-checklist/master-data/employee/': [ + 'lti.daily_checklist.master_data.employee', + ], + '/daily-checklist/master-data/activity/': [ + 'lti.daily_checklist.master_data.activity', + ], + '/daily-checklist/master-data/configuration/': [ + 'lti.daily_checklist.master_data.configuration', + ], // Production // Production - Project Flock diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index ff6a0bcb..b2ba2b8f 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -148,10 +148,11 @@ export class ClosingApiService extends BaseApiService { } async getFinance( - id: number + id: number, + kandangId?: number ): Promise | undefined> { try { - const path = `${this.basePath}/${id}/keuangan`; + const path = `${this.basePath}/${id}${kandangId ? `/${kandangId}` : ''}/keuangan`; return await httpClient>(path, { method: 'GET', }); diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts index e82f857e..7fd2c7c1 100644 --- a/src/types/api/master-data/product.d.ts +++ b/src/types/api/master-data/product.d.ts @@ -1,20 +1,20 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Uom } from '@/types/api/master-data/uom'; import { ProductCategory } from '@/types/api/master-data/product-category'; -import { Supplier } from '@/types/api/master-data/supplier'; +import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; export type BaseProduct = { id: number; name: string; brand: string; - sku: string; + sku?: string; product_price: number; selling_price?: number; tax?: number; - expiry_period: number; + expiry_period?: number; uom: Uom; product_category: ProductCategory; - suppliers: Supplier[]; + suppliers: (BaseSupplier & { price: number })[]; flags: string[]; }; @@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct; export type CreateProductPayload = { name: string; brand: string; - sku: string; + sku?: string; uom_id: number; product_category_id: number; product_price: number; - selling_price: number; - tax: number; - expiry_period: number; - supplier_ids: number[]; + selling_price?: number; + tax?: number; + expiry_period?: number; + suppliers: { + supplier_id: number; + price: number; + }[]; flags: string[]; };
+
@@ -523,7 +594,9 @@ const DebtSupplierTab = () => {