fix(FE): resolve conflict

This commit is contained in:
randy-ar
2026-01-15 14:39:06 +07:00
24 changed files with 833 additions and 560 deletions
+5 -3
View File
@@ -1,5 +1,6 @@
import Button, { ButtonProps } from '@/components/Button'; import Button, { ButtonProps } from '@/components/Button';
import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { FormikValues } from 'formik'; import { FormikValues } from 'formik';
@@ -13,11 +14,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
<Button <Button
{...props} {...props}
onClick={onClick} onClick={onClick}
className={ className={cn(
getFilledFormikValuesCount(values) > 0 getFilledFormikValuesCount(values) > 0
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200' ? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
: '' : '',
} props.className
)}
> >
<Icon <Icon
icon='heroicons:funnel' icon='heroicons:funnel'
+1 -1
View File
@@ -18,7 +18,7 @@ const AlertErrorList = ({
if (formErrorList.length === 0) return null; if (formErrorList.length === 0) return null;
return ( return (
<Alert color='error' className='w-full flex flex-col gap-2 px-4 m-4'> <Alert color='error' className='w-full flex flex-col gap-2 px-4'>
<div className='flex justify-between items-center gap-2 w-full'> <div className='flex justify-between items-center gap-2 w-full'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Icon icon='material-symbols:error-outline' width={24} height={24} /> <Icon icon='material-symbols:error-outline' width={24} height={24} />
+2 -2
View File
@@ -325,7 +325,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
}; };
const useSelect = <T,>( const useSelect = <T,>(
basePath: string, basePath: string | null,
valueKey: keyof T | string, valueKey: keyof T | string,
labelKey: keyof T | string, labelKey: keyof T | string,
searchKey: string = 'search', searchKey: string = 'search',
@@ -354,7 +354,7 @@ const useSelect = <T,>(
[limitKey]: String(limit), [limitKey]: String(limit),
}).toString(); }).toString();
return `${basePath}?${qs}`; return basePath ? `${basePath}?${qs}` : null;
}; };
const { const {
@@ -8,6 +8,7 @@ import {
HppPurchaseData, HppPurchaseData,
ProfitLossDataAmount, ProfitLossDataAmount,
} from '@/types/api/closing'; } from '@/types/api/closing';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
type HppTableRow = type HppTableRow =
@@ -55,9 +56,16 @@ const ClosingFinanceTable = ({
}: { }: {
projectFlockId: number; projectFlockId: number;
}) => { }) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: finance, isLoading } = useSWR( const { data: finance, isLoading } = useSWR(
`/closing/finance/${projectFlockId}`, `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
() => ClosingApi.getFinance(projectFlockId) () =>
ClosingApi.getFinance(
projectFlockId,
kandangId ? Number(kandangId) : undefined
)
); );
const staticHppRows: Array<{ const staticHppRows: Array<{
@@ -283,6 +291,7 @@ const ClosingFinanceTable = ({
<div className='mt-6 p-0 mb-0'> <div className='mt-6 p-0 mb-0'>
<Table<HppTableRow> <Table<HppTableRow>
data={hppTableData} data={hppTableData}
isLoading={isLoading}
columns={[ columns={[
{ {
header: 'No.', header: 'No.',
@@ -455,6 +464,7 @@ const ClosingFinanceTable = ({
<div className='mt-6 p-0 mb-0'> <div className='mt-6 p-0 mb-0'>
<Table<ProfitLossTableRow> <Table<ProfitLossTableRow>
data={profitLossTableData} data={profitLossTableData}
isLoading={isLoading}
columns={[ columns={[
{ {
header: 'Jenis', header: 'Jenis',
@@ -163,6 +163,7 @@ const ClosingsTable = () => {
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name'); } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>( const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
@@ -228,6 +229,7 @@ const ClosingsTable = () => {
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6', wrapper: 'col-span-12 sm:col-span-6',
@@ -8,7 +8,7 @@ import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { DashboardApi } from '@/services/api/dashboard'; import { DashboardApi } from '@/services/api/dashboard';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
@@ -30,6 +30,11 @@ import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import ButtonFilter from '@/components/helper/ButtonFilter';
import toast from 'react-hot-toast';
import Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
// Helper function to normalize values to array // Helper function to normalize values to array
const normalizeToArray = ( const normalizeToArray = (
@@ -49,6 +54,7 @@ const DashboardProduction = () => {
); );
const [endpointUrl, setEndpointUrl] = useState('/dashboards'); const [endpointUrl, setEndpointUrl] = useState('/dashboards');
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]); const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]);
const [exporting, setExporting] = useState(false);
// ===== FETCH DATA ===== // ===== FETCH DATA =====
const { const {
@@ -64,22 +70,32 @@ const DashboardProduction = () => {
: undefined; : undefined;
// ===== SELECT ===== // ===== SELECT =====
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
limit: 'limit',
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const { const {
setInputValue: setInputValueFlock,
options: flockOptions,
isLoadingOptions: isLoadingFlockOptions,
loadMore: loadMoreFlock,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
limit: 'limit',
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const {
setInputValue: setInputValueLocation,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocation,
} = useSelect(LocationApi.basePath, 'id', 'name', '', { } = useSelect(LocationApi.basePath, 'id', 'name', '', {
limit: 'limit', limit: 'limit',
}); });
const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } = const {
useSelect(KandangApi.basePath, 'id', 'name', '', { setInputValue: setInputValueKandang,
limit: 'limit', options: kandangOptions,
location_id: selectedLocationIds ? selectedLocationIds.toString() : '', isLoadingOptions: isLoadingKandangOptions,
}); loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
limit: 'limit',
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
});
const comparisonTypeOptions = [ const comparisonTypeOptions = [
{ value: 'FARM', label: 'Farm' }, { value: 'FARM', label: 'Farm' },
{ value: 'FLOCK', label: 'Flock' }, { value: 'FLOCK', label: 'Flock' },
@@ -143,12 +159,27 @@ const DashboardProduction = () => {
console.log(endpointUrl); console.log(endpointUrl);
filterModal.closeModal(); filterModal.closeModal();
refreshDashboardProductionData(); refreshDashboardProductionData();
formik.resetForm();
}; };
// ===== Formik Error List ===== // ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
// ===== Export PDF =====
const handleExportPDF = () => {
setExporting(true);
};
// Wait for state to render, then trigger print
useEffect(() => {
if (exporting) {
const timer = setTimeout(() => {
window.print();
setExporting(false);
}, 100);
return () => clearTimeout(timer);
}
}, [exporting]);
if (isLoadingDashboardProductionData) { if (isLoadingDashboardProductionData) {
return ( return (
<div className='w-full min-h-screen flex items-center justify-center'> <div className='w-full min-h-screen flex items-center justify-center'>
@@ -156,71 +187,39 @@ const DashboardProduction = () => {
</div> </div>
); );
} }
return ( return (
<> <>
<section className='w-full p-4 space-y-6'> <section className='w-full p-4 space-y-6'>
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'> <div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
<div></div> <div></div>
<div className='flex flex-row justify-end gap-2'> <div className='flex flex-row justify-end gap-2'>
<Button <ButtonFilter
values={{
...formik.values,
analysisMode: undefined,
}}
variant='outline' variant='outline'
className={`min-w-28 rounded-lg ${
isResponseSuccess(dashboardProductionResponse) &&
(dashboardProductionResponse.meta as unknown as DashboardMeta)
.filters
? 'bg-gradient-to-r from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
: ''
}`}
onClick={() => filterModal.openModal()}
>
<Icon
icon='heroicons:funnel'
width={20}
height={20}
className={
isResponseSuccess(dashboardProductionResponse) &&
(dashboardProductionResponse.meta as unknown as DashboardMeta)
.filters
? 'text-blue-600'
: ''
}
/>
Filter
{isResponseSuccess(dashboardProductionResponse) &&
dashboardProductionResponse.meta &&
(dashboardProductionResponse.meta as unknown as DashboardMeta)
.filters && (
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
{(() => {
const meta =
dashboardProductionResponse.meta as unknown as DashboardMeta;
if (!meta.filters) return 0;
const count =
(meta.filters.location_ids.length > 1
? meta.filters.location_ids.length
: 0) +
(meta.filters.flock_ids.length > 1
? meta.filters.flock_ids.length
: 0) +
(meta.filters.kandang_ids.length > 1
? meta.filters.kandang_ids.length
: 0);
return meta.filters.analysis_mode === 'OVERVIEW'
? 1
: count;
})()}
</span>
)}
</Button>
<Button
variant='outline'
color='neutral'
className='min-w-28 rounded-lg' className='min-w-28 rounded-lg'
onClick={() => filterModal.openModal()}
/>
<Dropdown
trigger={
<Button variant='outline' className='min-w-28 rounded-lg z-50'>
<Icon icon='heroicons:arrow-down-tray' />
Export
<Icon icon='heroicons:chevron-down' />
</Button>
}
className={{
content: 'w-full',
}}
> >
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} /> <Menu className={exporting ? 'hidden' : ''}>
Export <MenuItem title='PDF' onClick={handleExportPDF} />
<Icon icon='heroicons:chevron-down' width={20} height={20} /> </Menu>
</Button> </Dropdown>
</div> </div>
</div> </div>
@@ -287,7 +286,7 @@ const DashboardProduction = () => {
{/* Rentang Waktu */} {/* Rentang Waktu */}
<div className='px-4'> <div className='px-4'>
<label className='flex items-center gap-2 mb-3'>Tanggal</label> <label className='flex items-center gap-2 mb-3'>Tanggal</label>
<div className='flex items-center gap-2'> <div className='flex items-start gap-2'>
<DateInput <DateInput
name='startDate' name='startDate'
placeholder='Tanggal Mulai' placeholder='Tanggal Mulai'
@@ -302,7 +301,7 @@ const DashboardProduction = () => {
Boolean(formik.touched.startDate) Boolean(formik.touched.startDate)
} }
/> />
<span className='hidden md:block text-center'></span> <div className='hidden md:block mt-3 text-center'></div>
<DateInput <DateInput
name='endDate' name='endDate'
placeholder='Tanggal Akhir' placeholder='Tanggal Akhir'
@@ -383,6 +382,8 @@ const DashboardProduction = () => {
<SelectInput <SelectInput
label='Farm' label='Farm'
value={formik.values.location} value={formik.values.location}
onInputChange={setInputValueLocation}
onMenuScrollToBottom={loadMoreLocation}
onChange={(selected) => { onChange={(selected) => {
formik.setFieldValue('location', selected); formik.setFieldValue('location', selected);
// Update selectedLocationIds for kandang filter // Update selectedLocationIds for kandang filter
@@ -422,6 +423,8 @@ const DashboardProduction = () => {
formik.setFieldValue('flock', selected) formik.setFieldValue('flock', selected)
} }
errorMessage={formik.errors.flock as string} errorMessage={formik.errors.flock as string}
onInputChange={setInputValueFlock}
onMenuScrollToBottom={loadMoreFlock}
options={flockOptions} options={flockOptions}
isLoading={isLoadingFlockOptions} isLoading={isLoadingFlockOptions}
isMulti={ isMulti={
@@ -450,6 +453,8 @@ const DashboardProduction = () => {
formik.setFieldValue('kandang', selected) formik.setFieldValue('kandang', selected)
} }
errorMessage={formik.errors.kandang as string} errorMessage={formik.errors.kandang as string}
onInputChange={setInputValueKandang}
onMenuScrollToBottom={loadMoreKandang}
options={kandangOptions} options={kandangOptions}
isLoading={isLoadingKandangOptions} isLoading={isLoadingKandangOptions}
isMulti={ isMulti={
@@ -465,7 +470,9 @@ const DashboardProduction = () => {
</div> </div>
)} )}
<AlertErrorList formErrorList={formErrorList} onClose={close} /> <div className='w-full p-4'>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
</div>
{/* Action Buttons */} {/* Action Buttons */}
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'> <div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
@@ -283,261 +283,311 @@ const DashboardLineChart = ({
})()} })()}
</div> </div>
{/* Chart */} {/* Chart Container with Empty State Overlay */}
<ResponsiveContainer width='100%' height={350}> <div className='relative'>
<LineChart {/* Chart */}
data={(() => { <ResponsiveContainer width='100%' height={350}>
// Transform data based on analysisMode <LineChart
if (analysisMode === 'OVERVIEW') { data={(() => {
// For OVERVIEW mode, use the selected chart data // Transform data based on analysisMode
if (isOverviewCharts(data.charts)) { if (analysisMode === 'OVERVIEW') {
const selectedChartData = data.charts[chartData]; // For OVERVIEW mode, use the selected chart data
if (!selectedChartData || !selectedChartData.dataset) return []; if (isOverviewCharts(data.charts)) {
return selectedChartData.dataset; 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,
}}
>
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
<XAxis
dataKey='week'
tick={{ fontSize: 11, fill: '#9ca3af' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
label={{
value: 'Weeks',
position: 'insideBottom',
offset: -5,
style: { fontSize: 12, fill: '#9ca3af' },
}}
/>
<YAxis
tick={{ fontSize: 11, fill: '#9ca3af' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
domain={(() => {
// 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={(() => { margin={{
// Calculate dynamic ticks based on domain top: 5,
let seriesData: DashboardChartsSeries[] = []; right: 10,
let dataset: DashboardChartsDataset[] = []; left: 0,
bottom: 5,
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,
];
})()}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1f2937',
border: 'none',
borderRadius: '8px',
padding: '8px 12px',
color: 'white',
}} }}
labelStyle={{ color: 'white', marginBottom: '4px' }} >
itemStyle={{ color: 'white', fontSize: '12px' }} <CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
labelFormatter={(value) => `Week ${value}`} <XAxis
formatter={( dataKey='week'
value: number | undefined, tick={{ fontSize: 11, fill: '#9ca3af' }}
name: string | undefined tickLine={false}
) => { axisLine={{ stroke: '#e5e7eb' }}
if (value === undefined || name === undefined) return ['', '']; label={{
value: 'Weeks',
position: 'insideBottom',
offset: -5,
style: { fontSize: 12, fill: '#9ca3af' },
}}
/>
<YAxis
tick={{ fontSize: 11, fill: '#9ca3af' }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
domain={(() => {
// Calculate dynamic domain based on visible data
let seriesData: DashboardChartsSeries[] = [];
let dataset: DashboardChartsDataset[] = [];
// Get series data to find the unit if (
let seriesData: DashboardChartsSeries[] = []; analysisMode === 'OVERVIEW' &&
if ( isOverviewCharts(data.charts)
analysisMode === 'OVERVIEW' && ) {
isOverviewCharts(data.charts) seriesData = data.charts[chartData]?.series || [];
) { dataset = data.charts[chartData]?.dataset || [];
seriesData = data.charts[chartData]?.series || []; } else if (
} else if ( analysisMode === 'COMPARISON' &&
analysisMode === 'COMPARISON' && isComparisonCharts(data.charts)
isComparisonCharts(data.charts) ) {
) { const comparisonChart =
const comparisonChart = data.charts.location ||
data.charts.location || data.charts.flock ||
data.charts.flock || data.charts.kandang;
data.charts.kandang; seriesData = comparisonChart?.series || [];
seriesData = comparisonChart?.series || []; dataset = comparisonChart?.dataset || [];
} }
// Find the series that matches this line's name // Get all values from visible series
const series = seriesData.find((s) => s.label === name); const visibleSeriesIds = Array.from(visibleSeries);
const unit = series?.unit || ''; const allValues: number[] = [];
return [`${value} ${unit}`, name]; dataset.forEach((item: DashboardChartsDataset) => {
}} visibleSeriesIds.forEach((seriesId) => {
/> const value = item[seriesId];
{/* Dynamic Line rendering based on visible series */} if (typeof value === 'number') {
{(() => { allValues.push(value);
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 (
<Line
key={series.id}
type='monotone'
dataKey={dataKey}
name={series.label}
stroke={getLineColor(series.id, index, analysisMode)}
opacity={isStandard ? 0.5 : 1}
strokeWidth={2}
strokeDasharray={isStandard ? '5 5' : undefined}
dot={
isStandard
? false
: {
r: 3,
fill: '#fff',
stroke: getLineColor(
series.id,
index,
analysisMode
),
strokeWidth: 2,
}
} }
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,
];
})()}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1f2937',
border: 'none',
borderRadius: '8px',
padding: '8px 12px',
color: 'white',
}}
labelStyle={{ color: 'white', marginBottom: '4px' }}
itemStyle={{ color: 'white', fontSize: '12px' }}
labelFormatter={(value) => `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 (
<Line
key={series.id}
type='monotone'
dataKey={dataKey}
name={series.label}
stroke={getLineColor(series.id, index, analysisMode)}
opacity={isStandard ? 0.5 : 1}
strokeWidth={2}
strokeDasharray={isStandard ? '5 5' : undefined}
dot={
isStandard
? false
: {
r: 3,
fill: '#fff',
stroke: getLineColor(
series.id,
index,
analysisMode
),
strokeWidth: 2,
}
}
activeDot={isStandard ? undefined : { r: 5 }}
/>
);
});
})()}
</LineChart>
</ResponsiveContainer>
{/* 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 (
<div className='absolute inset-x-0 inset-y-15 z-10 flex flex-col items-center justify-center rounded-lg'>
{/* Chart icon */}
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
<Icon
icon='heroicons:chart-bar'
className='text-white'
width={24}
height={24}
/> />
); </div>
});
})()} {/* Empty state text */}
</LineChart> <h3 className='text-gray-900 font-semibold text-base mb-2'>
</ResponsiveContainer> Data Not Yet Available
</h3>
<p className='text-gray-500 text-sm text-center max-w-xs'>
Please change your filters to get the data.
</p>
</div>
);
}
return null;
})()}
</div>
</Card> </Card>
); );
}; };
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError } from '@/lib/api-helper';
import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { import {
CreateInventoryAdjustmentPayload, CreateInventoryAdjustmentPayload,
@@ -22,12 +22,18 @@ import {
} from '@/services/api/master-data'; } from '@/services/api/master-data';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { Icon } from '@iconify/react'; 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 TextInput from '@/components/input/TextInput';
import { RadioGroup } from '@/components/input/RadioInput'; import { RadioGroup } from '@/components/input/RadioInput';
import TextArea from '@/components/input/TextArea'; import TextArea from '@/components/input/TextArea';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; 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 { interface InventoryAdjustmentFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({
InventoryAdjustmentFormErrorMessage, InventoryAdjustmentFormErrorMessage,
setInventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage,
] = useState(''); ] = useState('');
const [selectedProductCategories, setSelectedProductCategories] =
useState('');
const [disabledProduct, setDisabledProduct] = useState(true); const [disabledProduct, setDisabledProduct] = useState(true);
const [optionsProduct, setOptionsProduct] = useState<OptionType[]>([]);
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
// Submit Handler // Submit Handler
@@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({
}); });
// Fetch Data // Fetch Data
const productCategoriesUrl = `${ const {
ProductCategoryApi.basePath setInputValue: setProductCategoryInputValue,
}?${new URLSearchParams({ options: productCategoryOptions,
search: '', isLoadingOptions: isLoadingProductCategoryOptions,
}).toString()}`; loadMore: loadMoreProductCategories,
const { data: productCategories, isLoading: isLoadingProductCategories } = } = useSelect<ProductCategory>(ProductCategoryApi.basePath, 'id', 'name');
useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher);
const productUrl = `${ProductApi.basePath}?${new URLSearchParams({ const {
search: '', setInputValue: setProductInputValue,
product_category_id: selectedProductCategories, options: productOptions,
}).toString()}`; isLoadingOptions: isLoadingProductOptions,
const { data: products, isLoading: isLoadingProducts } = useSWR( loadMore: loadMoreProducts,
productUrl, } = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
ProductApi.getAllFetcher product_category_id: formik.values.product_category_id
); ? String(formik.values.product_category_id)
: '',
});
const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ const {
search: '', setInputValue: setWarehouseInputValue,
limit: '100', options: warehouseOptions,
}).toString()}`; isLoadingOptions: isLoadingWarehouseOptions,
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( loadMore: loadMoreWarehouses,
warehouseUrl, } = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
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,
}))
: [];
// Options Handler // Options Handler
const productCategoryChangeHandler = ( const productCategoryChangeHandler = (
@@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({
formik.setFieldValue('product_category', val); formik.setFieldValue('product_category', val);
setSelectedProductCategories((val as OptionType)?.value as string);
const disabled = (val as OptionType)?.value == null; const disabled = (val as OptionType)?.value == null;
setDisabledProduct(disabled); setDisabledProduct(disabled);
formik.setFieldValue('product_id', 0); formik.setFieldValue('product_id', 0);
@@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({
// Effect // Effect
useEffect(() => { useEffect(() => {
if (initialValues?.product_warehouse?.product?.id) { if (initialValues?.product_warehouse?.product?.id) {
setSelectedProductCategories(
String(initialValues.product_warehouse.product.id)
);
setDisabledProduct(false); setDisabledProduct(false);
formik.setFieldValue( formik.setFieldValue(
'product_id', 'product_id',
@@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({
); );
formik.setFieldValue('note', initialValues.note); formik.setFieldValue('note', initialValues.note);
} }
}, [ }, [formik, initialValues, setQuantityLabel, setDisabledProduct]);
formik,
initialValues,
setQuantityLabel,
setDisabledProduct,
setSelectedProductCategories,
]);
useEffect(() => { useEffect(() => {
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
}, [formikSetValues, formikInitialValues]); }, [formikSetValues, formikInitialValues]);
useEffect(() => {
if (isResponseSuccess(products)) {
const options = products.data.map((p) => ({
value: p.id,
label: p.name,
}));
setOptionsProduct(options);
}
}, [products]);
// Utils Function // Utils Function
const formatNumber = (value: string) => { const formatNumber = (value: string) => {
@@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({
label='Kategori Produk' label='Kategori Produk'
value={formik.values.product_category as OptionType} value={formik.values.product_category as OptionType}
onChange={productCategoryChangeHandler} onChange={productCategoryChangeHandler}
onInputChange={setSelectedProductCategories} onInputChange={setProductCategoryInputValue}
options={optionsProductCategory} options={productCategoryOptions}
isLoading={isLoadingProductCategories} onMenuScrollToBottom={loadMoreProductCategories}
isLoading={isLoadingProductCategoryOptions}
isError={ isError={
formik.touched.product_category && formik.touched.product_category &&
Boolean(formik.errors.product_category) Boolean(formik.errors.product_category)
@@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({
label='Produk' label='Produk'
value={formik.values.product as OptionType} value={formik.values.product as OptionType}
onChange={productChangeHandler} onChange={productChangeHandler}
options={optionsProduct} onInputChange={setProductInputValue}
isLoading={isLoadingProducts} options={productOptions}
onMenuScrollToBottom={loadMoreProducts}
isLoading={isLoadingProductOptions}
isError={formik.touched.product && Boolean(formik.errors.product)} isError={formik.touched.product && Boolean(formik.errors.product)}
errorMessage={formik.errors.product as string} errorMessage={formik.errors.product as string}
isDisabled={type === 'detail' || disabledProduct} isDisabled={type === 'detail' || disabledProduct}
@@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({
label='Warehouse' label='Warehouse'
value={formik.values.warehouse as OptionType} value={formik.values.warehouse as OptionType}
onChange={warehouseChangeHandler} onChange={warehouseChangeHandler}
options={optionsWarehouse} onInputChange={setWarehouseInputValue}
isLoading={isLoadingWarehouses} options={warehouseOptions}
onMenuScrollToBottom={loadMoreWarehouses}
isLoading={isLoadingWarehouseOptions}
isError={ isError={
formik.touched.warehouse && Boolean(formik.errors.warehouse) formik.touched.warehouse && Boolean(formik.errors.warehouse)
} }
@@ -38,6 +38,8 @@ import Card from '@/components/Card';
import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { S3_PUBLIC_BASE_URL } from '@/config/constant';
import { getUniqueFormikErrors } from '@/lib/formik-helper'; import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors'; 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 { interface MovementFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState(''); const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
const [
productWarehouseSelectInputValue,
setProductWarehouseSelectInputValue,
] = useState('');
const [selectedProducts, setSelectedProducts] = useState<number[]>([]); const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]); const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
const [formErrorList, setFormErrorList] = useState<string[]>([]); const [formErrorList, setFormErrorList] = useState<string[]>([]);
@@ -93,10 +91,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== USE SELECT HOOKS ===== // ===== USE SELECT HOOKS =====
const { const {
inputValue: warehouseSelectInputValue,
setInputValue: setWarehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue,
isLoadingOptions: isLoadingWarehouses, isLoadingOptions: isLoadingWarehouses,
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); loadMore: loadMoreWarehouses,
rawData: warehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
// ===== SELECT INPUT DATA ===== // ===== SELECT INPUT DATA =====
const { const {
@@ -107,12 +106,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
category: 'BOP', category: 'BOP',
}); });
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
const { data: warehouses } = useSWR(
warehousesUrl,
WarehouseApi.getAllFetcher
);
// ===== DATA PROCESSING ===== // ===== DATA PROCESSING =====
const warehouseStockMap = useMemo(() => { const warehouseStockMap = useMemo(() => {
if (!isResponseSuccess(allProductWarehouses)) return new Map(); if (!isResponseSuccess(allProductWarehouses)) return new Map();
@@ -269,25 +262,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}); });
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
const getProductWarehousesUrl = useCallback(() => { const {
const productWarehouseParams = new URLSearchParams({ setInputValue: setProductWarehouseSelectInputValue,
search: productWarehouseSelectInputValue, isLoadingOptions: isLoadingProductWarehouses,
}); loadMore: loadMoreProductWarehouses,
if (formik.values.source_warehouse_id) { rawData: productWarehouses,
productWarehouseParams.append( } = useSelect<ProductWarehouse>(
'warehouse_id', formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null,
formik.values.source_warehouse_id.toString() '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) const productWarehouseOptions = isResponseSuccess(productWarehouses)
? productWarehouses?.data.map((pw) => ({ ? productWarehouses?.data.map((pw) => ({
@@ -1006,6 +996,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}} }}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
isError={ isError={
formik.touched.source_warehouse_id && formik.touched.source_warehouse_id &&
@@ -1104,6 +1095,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
onMenuScrollToBottom={loadMoreWarehouses}
isError={ isError={
formik.touched.destination_warehouse_id && formik.touched.destination_warehouse_id &&
Boolean(formik.errors.destination_warehouse_id) Boolean(formik.errors.destination_warehouse_id)
@@ -1263,6 +1255,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}} }}
options={productWarehouseOptions} options={productWarehouseOptions}
onInputChange={setProductWarehouseSelectInputValue} onInputChange={setProductWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreProductWarehouses}
isLoading={isLoadingProductWarehouses} isLoading={isLoadingProductWarehouses}
isDisabled={ isDisabled={
type === 'detail' || type === 'detail' ||
@@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama lokasi' placeholder='Masukkan nama kandang'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama lokasi' placeholder='Masukkan nama nonstock'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -3,7 +3,7 @@ import * as Yup from 'yup';
type ProductFormSchemaType = { type ProductFormSchemaType = {
name: string; name: string;
brand: string; brand: string;
sku: string; sku?: string;
uom?: { uom?: {
value: number; value: number;
label: string; label: string;
@@ -15,10 +15,16 @@ type ProductFormSchemaType = {
} | null; } | null;
product_category_id: number; product_category_id: number;
product_price: number | string; product_price: number | string;
selling_price: number | string; selling_price?: number | string;
tax: number | string; tax?: number | string;
expiry_period: number | string; expiry_period?: number | string;
supplier_ids: number[]; suppliers: {
supplier: {
value: number;
label: string;
} | null;
price: number;
}[];
flags: string[]; flags: string[];
}; };
@@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
Yup.object({ Yup.object({
name: Yup.string().required('Nama wajib diisi!'), name: Yup.string().required('Nama wajib diisi!'),
brand: Yup.string().required('Merek wajib diisi!'), brand: Yup.string().required('Merek wajib diisi!'),
sku: Yup.string().required('SKU wajib diisi!'), sku: Yup.string(),
uom: Yup.object({ uom: Yup.object({
value: Yup.number() value: Yup.number()
@@ -58,24 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
.min(1, 'Harga produk tidak boleh kurang dari 1!'), .min(1, 'Harga produk tidak boleh kurang dari 1!'),
selling_price: Yup.number() selling_price: Yup.number()
.required('Harga jual wajib diisi!') .typeError('Harga hanya boleh angka!')
.typeError('Harga jual wajib diisi!')
.min(1, 'Harga jual tidak boleh kurang dari 1!'), .min(1, 'Harga jual tidak boleh kurang dari 1!'),
tax: Yup.number() tax: Yup.number()
.required('Pajak wajib diisi!') .typeError('Pajak hanya boleh angka!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!') .min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'), .max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number() expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!') .typeError('Periode kadaluarsa hanya boleh angka!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'), .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
supplier_ids: Yup.array() suppliers: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!')) .of(
.min(1, 'Minimal harus ada 1 supplier!') 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!'), .required('Supplier wajib diisi!'),
flags: Yup.array() flags: Yup.array()
@@ -41,6 +41,8 @@ import { cn } from '@/lib/helper';
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import { Supplier } from '@/types/api/master-data/supplier'; import { Supplier } from '@/types/api/master-data/supplier';
import Card from '@/components/Card';
import { removeArrayItemAndSync } from '@/lib/utils/formik';
interface ProductFormProps { interface ProductFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
selling_price: initialValues?.selling_price ?? '', selling_price: initialValues?.selling_price ?? '',
tax: initialValues?.tax ?? '', tax: initialValues?.tax ?? '',
expiry_period: initialValues?.expiry_period ?? '', 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 ?? [], flags: initialValues?.flags ?? [],
}), }),
[initialValues] [initialValues]
@@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
uom_id: values.uom_id, uom_id: values.uom_id,
product_category_id: values.product_category_id, product_category_id: values.product_category_id,
product_price: parseInt(values.product_price.toString()) || 0, product_price: parseInt(values.product_price.toString()) || 0,
selling_price: parseInt(values.selling_price.toString()) || 0, selling_price: values.selling_price
tax: parseInt(values.tax.toString()) || 0, ? parseInt(values.selling_price.toString()) || 0
expiry_period: parseInt(values.expiry_period.toString()) || 0, : undefined,
supplier_ids: values.supplier_ids.filter( tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
(id): id is number => typeof id === 'number' 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'), flags: values.flags.filter((f): f is string => typeof f === 'string'),
}; };
switch (type) { switch (type) {
@@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
category: 'SAPRONAK', category: 'SAPRONAK',
}); });
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const filteredSupplierOptions = useMemo(() => {
const arr = Array.isArray(val) ? val : val ? [val] : []; return supplierOptions.filter((opt) => {
formik.setFieldTouched('supplier_ids', true); return !formik.values.suppliers.some(
formik.setFieldValue( (s) => s.supplier?.value === opt.value
'supplier_ids', );
arr.map((v) => (v as OptionType).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 = () => { const deleteProductClickHandler = () => {
@@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
router.push('/master-data/product'); 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(() => { useEffect(() => {
formikSetValues(formikInitialValues); formikSetValues(formikInitialValues);
}, [formikSetValues, formikInitialValues]); }, [formikSetValues, formikInitialValues]);
@@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <TextInput
required
label='SKU' label='SKU'
name='sku' name='sku'
placeholder='Masukkan SKU...' placeholder='Masukkan SKU...'
@@ -344,7 +387,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<NumberInput <NumberInput
required
label='Harga Jual' label='Harga Jual'
name='selling_price' name='selling_price'
placeholder='Masukkan harga jual...' placeholder='Masukkan harga jual...'
@@ -366,7 +408,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
</div> </div>
<div className='grid sm:grid-cols-2 gap-4'> <div className='grid sm:grid-cols-2 gap-4'>
<NumberInput <NumberInput
required
label='Pajak (%)' label='Pajak (%)'
name='tax' name='tax'
placeholder='Masukkan pajak...' placeholder='Masukkan pajak...'
@@ -383,7 +424,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<NumberInput <NumberInput
required
label='Periode Kadaluarsa (hari)' label='Periode Kadaluarsa (hari)'
name='expiry_period' name='expiry_period'
placeholder='Masukkan periode kadaluarsa...' placeholder='Masukkan periode kadaluarsa...'
@@ -403,28 +443,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
</div> </div>
<div className='grid sm:grid-cols-2 gap-4'> <div className='grid sm:grid-cols-1 gap-4'>
<SelectInput
required
label='Supplier'
placeholder='Pilih supplier...'
isMulti
value={supplierOptions.filter((opt) =>
(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
/>
<SelectInput <SelectInput
required required
label='Flags' label='Flags'
@@ -447,6 +466,129 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
isClearable isClearable
/> />
</div> </div>
<div className='grid sm:grid-cols-1 gap-4'>
{type !== 'detail' && formik.values.suppliers.length === 0 && (
<Button
type='button'
color='success'
onClick={addSupplierHandler}
className='w-fit mx-auto'
>
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah
Supplier
</Button>
)}
{formik.values.suppliers.length > 0 && (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<div className='mb-4 text-center'>
<h4 className='font-bold text-xl'>Supplier</h4>
</div>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Supplier
</th>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Harga
</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
{formik.values.suppliers.map((supplier, idx) => (
<tr key={idx}>
<td className='p-2 w-full max-w-1/2'>
<SelectInput
placeholder='Pilih Supplier'
options={filteredSupplierOptions}
onInputChange={setSupplierSelectInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSuppliers}
value={formik.values.suppliers[idx].supplier}
onChange={(val) => {
formik.setFieldValue(
`suppliers.${idx}.supplier`,
val
);
}}
isError={isSupplierRepeaterError(
'supplier',
idx
)}
isClearable
isDisabled={type === 'detail'}
className={{
wrapper: 'min-w-48 w-full',
}}
/>
</td>
<td className='p-2 w-full max-w-1/2'>
<NumberInput
required
name={`suppliers.${idx}.price`}
placeholder='Masukkan harga...'
value={formik.values.suppliers[idx].price}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={isSupplierRepeaterError('price', idx)}
readOnly={type === 'detail'}
className={{
wrapper: 'min-w-48 w-full',
}}
/>
</td>
{type !== 'detail' && (
<td>
<Button
type='button'
color='error'
onClick={() => deleteSupplierItemHandler(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{type !== 'detail' && (
<div className='w-full flex flex-row justify-center'>
<Button
type='button'
color='success'
onClick={addSupplierHandler}
>
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
Tambah Supplier
</Button>
</div>
)}
</Card>
)}
</div>
</div> </div>
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && ( {type !== 'add' && (
@@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama lokasi' placeholder='Masukkan nama warehouse'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -87,6 +87,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setAreaInputValue, setInputValue: setAreaInputValue,
options: areaOptions, options: areaOptions,
isLoadingOptions: isLoadingAreaOptions, isLoadingOptions: isLoadingAreaOptions,
loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name'); } = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -101,6 +102,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name'); } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -118,6 +120,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setWarehouseInputValue, setInputValue: setWarehouseInputValue,
options: warehouseOptions, options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions, isLoadingOptions: isLoadingWarehouseOptions,
loadMore: loadMoreWarehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name'); } = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -135,6 +138,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setCustomerInputValue, setInputValue: setCustomerInputValue,
options: customerOptions, options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions, isLoadingOptions: isLoadingCustomerOptions,
loadMore: loadMoreCustomers,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name'); } = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const customerChangeHandler = (val: OptionType | OptionType[] | null) => { const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -298,6 +302,7 @@ const DailyMarketingReportContent = () => {
value={selectedArea} value={selectedArea}
onChange={areaChangeHandler} onChange={areaChangeHandler}
onInputChange={setAreaInputValue} onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -312,6 +317,7 @@ const DailyMarketingReportContent = () => {
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -326,6 +332,7 @@ const DailyMarketingReportContent = () => {
value={selectedWarehouse} value={selectedWarehouse}
onChange={warehouseChangeHandler} onChange={warehouseChangeHandler}
onInputChange={setWarehouseInputValue} onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -340,6 +347,7 @@ const DailyMarketingReportContent = () => {
value={selectedCustomer} value={selectedCustomer}
onChange={customerChangeHandler} onChange={customerChangeHandler}
onInputChange={setCustomerInputValue} onInputChange={setCustomerInputValue}
onMenuScrollToBottom={loadMoreCustomers}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
import toast from 'react-hot-toast'; 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 = () => { const ReportExpenseTable = () => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
@@ -64,16 +73,33 @@ const ReportExpenseTable = () => {
}); });
// ===== SELECT OPTIONS ===== // ===== SELECT OPTIONS =====
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } = const {
useSelect(`/master-data/locations`, 'id', 'name'); setInputValue: setLocationInputValue,
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } = options: locationOptions,
useSelect(`/master-data/suppliers`, 'id', 'name'); isLoadingOptions: isLoadingLocationOptions,
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } = loadMore: loadMoreLocations,
useSelect(`/master-data/kandangs`, 'id', 'name', '', { } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
location_id: filterState.location_id,
}); const {
const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } = setInputValue: setSupplierInputValue,
useSelect(`/master-data/nonstocks`, 'id', 'name'); options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const {
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
loadMore: loadMoreNonstocks,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
const categoryOptions = useMemo( const categoryOptions = useMemo(
() => [ () => [
@@ -86,31 +112,31 @@ const ReportExpenseTable = () => {
// Mendapatkan value option select dari filter state // Mendapatkan value option select dari filter state
const selectedLocation = useMemo( const selectedLocation = useMemo(
() => () =>
optionsLocation.find( locationOptions.find(
(opt) => String(opt.value) === filterState.location_id (opt) => String(opt.value) === filterState.location_id
) || null, ) || null,
[optionsLocation, filterState.location_id] [locationOptions, filterState.location_id]
); );
const selectedSupplier = useMemo( const selectedSupplier = useMemo(
() => () =>
optionsSupplier.find( supplierOptions.find(
(opt) => String(opt.value) === filterState.supplier_id (opt) => String(opt.value) === filterState.supplier_id
) || null, ) || null,
[optionsSupplier, filterState.supplier_id] [supplierOptions, filterState.supplier_id]
); );
const selectedKandang = useMemo( const selectedKandang = useMemo(
() => () =>
optionsKandang.find( kandangOptions.find(
(opt) => String(opt.value) === filterState.kandang_id (opt) => String(opt.value) === filterState.kandang_id
) || null, ) || null,
[optionsKandang, filterState.kandang_id] [kandangOptions, filterState.kandang_id]
); );
const selectedNonstock = useMemo( const selectedNonstock = useMemo(
() => () =>
optionsNonstock.find( nonstockOptions.find(
(opt) => String(opt.value) === filterState.nonstock_id (opt) => String(opt.value) === filterState.nonstock_id
) || null, ) || null,
[optionsNonstock, filterState.nonstock_id] [nonstockOptions, filterState.nonstock_id]
); );
const selectedCategory = useMemo( const selectedCategory = useMemo(
() => () =>
@@ -756,38 +782,46 @@ const ReportExpenseTable = () => {
<SelectInput <SelectInput
isClearable isClearable
label='Lokasi' label='Lokasi'
options={optionsLocation} options={locationOptions}
isLoading={isLoadingLocation} isLoading={isLoadingLocationOptions}
placeholder='Lokasi' placeholder='Lokasi'
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
/> />
<SelectInput <SelectInput
isClearable isClearable
label='Kandang' label='Kandang'
options={optionsKandang} options={kandangOptions}
isLoading={isLoadingKandang} isLoading={isLoadingKandangOptions}
placeholder='Kandang' placeholder='Kandang'
value={selectedKandang} value={selectedKandang}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
/> />
<SelectInput <SelectInput
isClearable isClearable
label='Supplier' label='Supplier'
options={optionsSupplier} options={supplierOptions}
isLoading={isLoadingSupplier} isLoading={isLoadingSupplierOptions}
placeholder='Supplier' placeholder='Supplier'
value={selectedSupplier} value={selectedSupplier}
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
/> />
<SelectInput <SelectInput
isClearable isClearable
label='Produk' label='Produk'
options={optionsNonstock} options={nonstockOptions}
isLoading={isLoadingNonstock} isLoading={isLoadingNonstockOptions}
placeholder='Produk' placeholder='Produk'
value={selectedNonstock} value={selectedNonstock}
onChange={nonstockChangeHandler} onChange={nonstockChangeHandler}
onInputChange={setNonstockInputValue}
onMenuScrollToBottom={loadMoreNonstocks}
/> />
<SelectInput <SelectInput
isClearable isClearable
@@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = (
const colWidths = [ const colWidths = [
{ wch: 5 }, // No { wch: 5 }, // No
{ wch: 15 }, // Nomor PR { wch: 10 }, // Nomor PR
{ wch: 15 }, // Nomor PO { wch: 10 }, // Nomor PO
{ wch: 15 }, // Tanggal Terima/Bayar { wch: 20 }, // Tanggal Terima/Bayar
{ wch: 15 }, // Tanggal PO { wch: 10 }, // Tanggal PO
{ wch: 12 }, // Aging { wch: 10 }, // Aging
{ wch: 15 }, // Area { wch: 15 }, // Area
{ wch: 15 }, // Gudang { wch: 15 }, // Gudang
{ wch: 18 }, // Jatuh Tempo { wch: 12 }, // Jatuh Tempo
{ wch: 18 }, // Status Jatuh Tempo { wch: 20 }, // Status Jatuh Tempo
{ wch: 15 }, // Nominal Pembelian (Rp) { wch: 20 }, // Nominal Pembelian (Rp)
{ wch: 15 }, // Pembayaran (Rp) { wch: 15 }, // Pembayaran (Rp)
{ wch: 15 }, // Sisa Saldo Hutang (Rp) { wch: 20 }, // Sisa Saldo Hutang (Rp)
{ wch: 12 }, // Status { wch: 12 }, // Status
{ wch: 15 }, // Nomor Perjalanan { wch: 15 }, // Nomor Perjalanan
]; ];
@@ -55,6 +55,7 @@ const CustomerPaymentTab = () => {
const { const {
options: customerOptions, options: customerOptions,
setInputValue: setCustomerInputValue,
isLoadingOptions: isLoadingCustomers, isLoadingOptions: isLoadingCustomers,
loadMore: loadMoreCustomers, loadMore: loadMoreCustomers,
hasMore: hasMoreCustomers, hasMore: hasMoreCustomers,
@@ -62,6 +63,7 @@ const CustomerPaymentTab = () => {
const { const {
options: salesOptions, options: salesOptions,
setInputValue: setSalesInputValue,
isLoadingOptions: isLoadingSales, isLoadingOptions: isLoadingSales,
loadMore: loadMoreSales, loadMore: loadMoreSales,
hasMore: hasMoreSales, hasMore: hasMoreSales,
@@ -654,6 +656,7 @@ const CustomerPaymentTab = () => {
Array.isArray(val) ? val : val ? [val] : [] Array.isArray(val) ? val : val ? [val] : []
); );
}} }}
onInputChange={setCustomerInputValue}
isLoading={isLoadingCustomers} isLoading={isLoadingCustomers}
isClearable isClearable
onMenuScrollToBottom={loadMoreCustomers} onMenuScrollToBottom={loadMoreCustomers}
@@ -670,6 +673,7 @@ const CustomerPaymentTab = () => {
onChange={(val) => { onChange={(val) => {
setFilterSales(Array.isArray(val) ? val : val ? [val] : []); setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
}} }}
onInputChange={setSalesInputValue}
isLoading={isLoadingSales} isLoading={isLoadingSales}
isClearable isClearable
onMenuScrollToBottom={loadMoreSales} onMenuScrollToBottom={loadMoreSales}
@@ -34,6 +34,7 @@ import {
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import { Supplier } from '@/types/api/master-data/supplier';
const dueStatus: Record<string, Color> = { const dueStatus: Record<string, Color> = {
'Sudah Jatuh Tempo': 'error', 'Sudah Jatuh Tempo': 'error',
@@ -89,10 +90,12 @@ const DebtSupplierTab = () => {
const filterModal = useModal(); const filterModal = useModal();
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = const {
useSelect(SupplierApi.basePath, 'id', 'name', '', { setInputValue: setSupplierInputValue,
limit: 'limit', options: supplierOptions,
}); isLoadingOptions: isLoadingSupplierOptions,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const dataTypeOptions = useMemo( const dataTypeOptions = useMemo(
() => [ () => [
@@ -680,7 +683,9 @@ const DebtSupplierTab = () => {
Array.isArray(val) ? val : val ? [val] : null Array.isArray(val) ? val : val ? [val] : null
); );
}} }}
isLoading={isLoadingSuppliers} onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSupplierOptions}
isClearable isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isError={ isError={
@@ -62,6 +62,7 @@ const ProductionResultContent = () => {
setInputValue: setAreaInputValue, setInputValue: setAreaInputValue,
options: areaOptions, options: areaOptions,
isLoadingOptions: isLoadingAreaOptions, isLoadingOptions: isLoadingAreaOptions,
loadMore: loadMoreAreas,
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name'); } = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -78,6 +79,7 @@ const ProductionResultContent = () => {
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', { } = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
}); });
@@ -94,6 +96,7 @@ const ProductionResultContent = () => {
setInputValue: setProjectFlockInputValue, setInputValue: setProjectFlockInputValue,
options: projectFlockOptions, options: projectFlockOptions,
isLoadingOptions: isLoadingProjectFlockOptions, isLoadingOptions: isLoadingProjectFlockOptions,
loadMore: loadMoreProjectFlocks,
} = useSelect<BaseKandang>( } = useSelect<BaseKandang>(
ProjectFlockApi.basePath, ProjectFlockApi.basePath,
'id', 'id',
@@ -120,6 +123,7 @@ const ProductionResultContent = () => {
setInputValue: setProjectFlockKandangInputValue, setInputValue: setProjectFlockKandangInputValue,
options: projectFlockKandangOptions, options: projectFlockKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangOptions, isLoadingOptions: isLoadingProjectFlockKandangOptions,
loadMore: loadMoreProjectFlockKandangs,
} = useSelect<BaseKandang>( } = useSelect<BaseKandang>(
ProjectFlockKandangApi.basePath, ProjectFlockKandangApi.basePath,
'id', 'id',
@@ -235,6 +239,7 @@ const ProductionResultContent = () => {
value={selectedArea} value={selectedArea}
onChange={areaChangeHandler} onChange={areaChangeHandler}
onInputChange={setAreaInputValue} onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -251,6 +256,7 @@ const ProductionResultContent = () => {
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable isClearable
isDisabled={!selectedArea} isDisabled={!selectedArea}
className={{ className={{
@@ -270,6 +276,7 @@ const ProductionResultContent = () => {
value={selectedProjectFlock} value={selectedProjectFlock}
onChange={projectFlockChangeHandler} onChange={projectFlockChangeHandler}
onInputChange={setProjectFlockInputValue} onInputChange={setProjectFlockInputValue}
onMenuScrollToBottom={loadMoreProjectFlocks}
isClearable isClearable
isDisabled={!selectedArea || !selectedLocation} isDisabled={!selectedArea || !selectedLocation}
className={{ className={{
@@ -289,6 +296,7 @@ const ProductionResultContent = () => {
value={selectedProjectFlockKandang} value={selectedProjectFlockKandang}
onChange={projectFlockKandangChangeHandler} onChange={projectFlockKandangChangeHandler}
onInputChange={setProjectFlockKandangInputValue} onInputChange={setProjectFlockKandangInputValue}
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
isClearable isClearable
isDisabled={!selectedProjectFlock} isDisabled={!selectedProjectFlock}
className={{ className={{
@@ -58,18 +58,26 @@ const HppPerKandangTab = () => {
}, },
}); });
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( const {
AreaApi.basePath, setInputValue: setAreaInputValue,
'id', options: areaOptions,
'name', isLoadingOptions: isLoadingAreas,
'search' loadMore: loadMoreAreas,
); } = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const { options: locationOptions, isLoadingOptions: isLoadingLocations } = const {
useSelect(LocationApi.basePath, 'id', 'name', 'search'); setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = const {
useSelect(KandangApi.basePath, 'id', 'name', 'search'); setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect(KandangApi.basePath, 'id', 'name', 'search');
const showUnrecordedOptions: OptionType[] = [ const showUnrecordedOptions: OptionType[] = [
{ value: 'false', label: 'Sembunyikan' }, { value: 'false', label: 'Sembunyikan' },
@@ -810,6 +818,8 @@ const HppPerKandangTab = () => {
.includes(String(opt.value)) .includes(String(opt.value))
)} )}
onChange={areaChangeHandler} onChange={areaChangeHandler}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreas} isLoading={isLoadingAreas}
isClearable isClearable
/> />
@@ -824,6 +834,8 @@ const HppPerKandangTab = () => {
.includes(String(opt.value)) .includes(String(opt.value))
)} )}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocations} isLoading={isLoadingLocations}
isClearable isClearable
/> />
@@ -838,6 +850,8 @@ const HppPerKandangTab = () => {
.includes(String(opt.value)) .includes(String(opt.value))
)} )}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangs} isLoading={isLoadingKandangs}
isClearable isClearable
/> />
+16 -16
View File
@@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/dashboard/': ['lti.dashboard.list'], '/dashboard/': ['lti.dashboard.list'],
// Daily Checklist // Daily Checklist
// TODO: use real daily checklist permission name '/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'],
// '/daily-checklist/': ['lti.daily_checklist.list'], '/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'],
// '/daily-checklist/dashboard/': ['lti.daily_checklist.list'], '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'],
// '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], '/daily-checklist/list-daily-checklist/detail/': [
// '/daily-checklist/list-daily-checklist/detail/': ['lti.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/reports/': ['lti.daily_checklist.reports'],
// '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'], '/daily-checklist/master-data/employee/': [
'/daily-checklist/dashboard/': ['lti.dashboard.list'], 'lti.daily_checklist.master_data.employee',
'/daily-checklist/daily-checklist/': ['lti.dashboard.list'], ],
'/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'], '/daily-checklist/master-data/activity/': [
'/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'], 'lti.daily_checklist.master_data.activity',
'/daily-checklist/reports/': ['lti.dashboard.list'], ],
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'], '/daily-checklist/master-data/configuration/': [
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'], 'lti.daily_checklist.master_data.configuration',
'/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], ],
// Production // Production
// Production - Project Flock // Production - Project Flock
+3 -2
View File
@@ -148,10 +148,11 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
} }
async getFinance( async getFinance(
id: number id: number,
kandangId?: number
): Promise<BaseApiResponse<ClosingFinance> | undefined> { ): Promise<BaseApiResponse<ClosingFinance> | undefined> {
try { try {
const path = `${this.basePath}/${id}/keuangan`; const path = `${this.basePath}/${id}${kandangId ? `/${kandangId}` : ''}/keuangan`;
return await httpClient<BaseApiResponse<ClosingFinance>>(path, { return await httpClient<BaseApiResponse<ClosingFinance>>(path, {
method: 'GET', method: 'GET',
}); });
+12 -9
View File
@@ -1,20 +1,20 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { Uom } from '@/types/api/master-data/uom'; import { Uom } from '@/types/api/master-data/uom';
import { ProductCategory } from '@/types/api/master-data/product-category'; 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 = { export type BaseProduct = {
id: number; id: number;
name: string; name: string;
brand: string; brand: string;
sku: string; sku?: string;
product_price: number; product_price: number;
selling_price?: number; selling_price?: number;
tax?: number; tax?: number;
expiry_period: number; expiry_period?: number;
uom: Uom; uom: Uom;
product_category: ProductCategory; product_category: ProductCategory;
suppliers: Supplier[]; suppliers: (BaseSupplier & { price: number })[];
flags: string[]; flags: string[];
}; };
@@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct;
export type CreateProductPayload = { export type CreateProductPayload = {
name: string; name: string;
brand: string; brand: string;
sku: string; sku?: string;
uom_id: number; uom_id: number;
product_category_id: number; product_category_id: number;
product_price: number; product_price: number;
selling_price: number; selling_price?: number;
tax: number; tax?: number;
expiry_period: number; expiry_period?: number;
supplier_ids: number[]; suppliers: {
supplier_id: number;
price: number;
}[];
flags: string[]; flags: string[];
}; };