Merge branch 'fix/transfer-to-laying' into 'development'

[FIX/FE] Transfer to Laying

See merge request mbugroup/lti-web-client!265
This commit is contained in:
Rivaldi A N S
2026-01-27 11:21:43 +00:00
7 changed files with 166 additions and 51 deletions
+20 -18
View File
@@ -176,24 +176,26 @@ const ApprovalStepsV2 = ({
})} })}
</div> </div>
<Button {formattedApprovals.length > maxVisibleSteps && (
variant='outline' <Button
color='none' variant='outline'
onClick={seeMoreClickHandler} color='none'
className={cn( onClick={seeMoreClickHandler}
'px-3 py-2 gap-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-lg transition-all' className={cn(
)} 'px-3 py-2 gap-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-lg transition-all'
> )}
<Icon >
icon='heroicons-outline:chevron-double-down' <Icon
width={20} icon='heroicons-outline:chevron-double-down'
height={20} width={20}
className={cn('transition-all duration-300', { height={20}
'-rotate-180': isSeeAll, className={cn('transition-all duration-300', {
})} '-rotate-180': isSeeAll,
/> })}
See {isSeeAll ? 'Less' : 'More'} />
</Button> See {isSeeAll ? 'Less' : 'More'}
</Button>
)}
</div> </div>
); );
}; };
+2 -2
View File
@@ -118,7 +118,7 @@ const TextInput = ({
<div <div
className={cn( className={cn(
'input h-fit px-3 py-2.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10', 'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
{ {
'border-error': isError, 'border-error': isError,
'border-success!': isValid, 'border-success!': isValid,
@@ -182,7 +182,7 @@ const TextInput = ({
) : ( ) : (
<div <div
className={cn( className={cn(
'input h-fit px-3 py-2.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10', 'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
{ {
'border-error': isError, 'border-error': isError,
'border-success!': isValid, 'border-success!': isValid,
@@ -288,6 +288,48 @@ const TransferToLayingFormModal = () => {
return { available: countAvailable, unavailable: countUnavailable }; return { available: countAvailable, unavailable: countUnavailable };
}, [mappedFlockSourceKandangsAvailability]); }, [mappedFlockSourceKandangsAvailability]);
const {
data: flockDestinationKandangsMaxTargetQty,
isLoading: isLoadingFlockDestinationKandangsMaxTargetQty,
} = useSWR(
formik.values.flockDestination
? [
'transfer-to-laying',
'max-target-qty',
String(formik.values.flockDestination.value),
]
: undefined,
([, , id]: string[]) =>
TransferToLayingApi.getMappedFlockKandangsMaxTargetQty(Number(id))
);
const mappedFlockDestinationKandangsMaxTargetQty: {
kandang_name: string;
max_target_qty: number;
project_flock_kandang_id: number;
}[] = useMemo(() => {
if (
!flockDestinationKandangsMaxTargetQty ||
!selectedFlockDestinationRawData
)
return [];
return selectedFlockDestinationRawData
? selectedFlockDestinationRawData.kandangs.map((kandang) => {
const maxQty =
flockDestinationKandangsMaxTargetQty[
kandang.project_flock_kandang_id
]?.max_target_qty;
return {
kandang_name: kandang.name,
max_target_qty: maxQty,
project_flock_kandang_id: kandang.project_flock_kandang_id,
};
})
: [];
}, [flockDestinationKandangsMaxTargetQty, selectedFlockDestinationRawData]);
const mappedFlockDestinationKandangsAvailabilityInfo: { const mappedFlockDestinationKandangsAvailabilityInfo: {
available: number; available: number;
unavailable: number; unavailable: number;
@@ -298,9 +340,8 @@ const TransferToLayingFormModal = () => {
let countAvailable = 0; let countAvailable = 0;
let countUnavailable = 0; let countUnavailable = 0;
selectedFlockDestinationRawData?.kandangs.forEach((item) => { mappedFlockDestinationKandangsMaxTargetQty.forEach((item) => {
// TODO: change this to real available quota later if (item.max_target_qty > 0) {
if (item.capacity > 0) {
countAvailable += 1; countAvailable += 1;
} else { } else {
countUnavailable += 1; countUnavailable += 1;
@@ -308,7 +349,7 @@ const TransferToLayingFormModal = () => {
}); });
return { available: countAvailable, unavailable: countUnavailable }; return { available: countAvailable, unavailable: countUnavailable };
}, [selectedFlockDestinationRawData]); }, [mappedFlockDestinationKandangsMaxTargetQty]);
const totalEnteredChickenForTransfer = const totalEnteredChickenForTransfer =
formik.values.flockSourceKandangs.reduce( formik.values.flockSourceKandangs.reduce(
@@ -648,10 +689,9 @@ const TransferToLayingFormModal = () => {
</div> </div>
<div className='w-full rounded-xl border border-base-content/10'> <div className='w-full rounded-xl border border-base-content/10'>
{selectedFlockDestinationRawData?.kandangs.map( {mappedFlockDestinationKandangsMaxTargetQty.map(
(item, itemIdx) => { (item, itemIdx) => {
// TODO: change this to real available quota later const isAvailable = item.max_target_qty > 0;
const isAvailable = item.capacity > 0;
const isChecked = const isChecked =
formik.values.flockDestinationKandangs.some( formik.values.flockDestinationKandangs.some(
(k) => (k) =>
@@ -669,11 +709,10 @@ const TransferToLayingFormModal = () => {
{ {
kandang: { kandang: {
value: item.project_flock_kandang_id, value: item.project_flock_kandang_id,
label: item.name, label: item.kandang_name,
}, },
quantity: '', quantity: '',
// TODO: change this to real available quota later maxQuantity: item.max_target_qty,
maxQuantity: item.capacity,
}, },
]); ]);
} else { } else {
@@ -718,9 +757,8 @@ const TransferToLayingFormModal = () => {
'cursor-not-allowed': !isAvailable, 'cursor-not-allowed': !isAvailable,
})} })}
> >
{item.name}{' '} {item.kandang_name}{' '}
{/* TODO: change this to real available quota later */} <span className='text-base-content/20'>{`(Max: ${item.max_target_qty})`}</span>
<span className='text-base-content/20'>{`(Max: ${item.capacity})`}</span>
</label> </label>
</div> </div>
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { import {
CellContext, CellContext,
@@ -33,6 +33,7 @@ import { cn, formatDate, formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
props, props,
@@ -432,6 +433,10 @@ const TransferToLayingsTable = () => {
setIsRejectLoading(false); setIsRejectLoading(false);
}; };
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const filterSubmitHandler = (values: TransferToLayingFilter) => { const filterSubmitHandler = (values: TransferToLayingFilter) => {
updateFilter('startDate', values.startDate); updateFilter('startDate', values.startDate);
updateFilter('endDate', values.endDate); updateFilter('endDate', values.endDate);
@@ -527,7 +532,27 @@ const TransferToLayingsTable = () => {
)} )}
</div> </div>
<div className='flex flex-row justify-center items-center gap-3'> <div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Search'
value={tableFilterState.search ?? ''}
onChange={searchChangeHandler}
startAdornment={
<Icon
icon='heroicons:magnifying-glass'
width={20}
height={20}
/>
}
className={{
wrapper: 'w-full min-w-24 max-w-3xs',
inputWrapper: 'rounded-xl! shadow-button-soft',
input:
'placeholder:font-semibold placeholder:text-base-content/50',
}}
/>
<Button <Button
variant='outline' variant='outline'
color='none' color='none'
@@ -176,6 +176,11 @@ export const getFilledTransferToLayingFormInitialValues = async (
initialValues?.from_project_flock.id as number initialValues?.from_project_flock.id as number
); );
const mappedFlockDestinationKandangsMaxTargetQty =
await TransferToLayingApi.getMappedFlockKandangsMaxTargetQty(
initialValues?.to_project_flock.id as number
);
const formattedFlockSourceKandangs = initialValues?.sources const formattedFlockSourceKandangs = initialValues?.sources
? initialValues.sources.map((sourceKandang) => ({ ? initialValues.sources.map((sourceKandang) => ({
kandang: { kandang: {
@@ -197,20 +202,8 @@ export const getFilledTransferToLayingFormInitialValues = async (
maxTotalQuantity += item.quantity; maxTotalQuantity += item.quantity;
}); });
const flockDestination = await ProjectFlockApi.getSingle(
initialValues?.to_project_flock.id as number
);
const formattedFlockDestinationKandangs = initialValues?.targets const formattedFlockDestinationKandangs = initialValues?.targets
? initialValues.targets.map((targetKandang) => { ? initialValues.targets.map((targetKandang) => {
const kandang = isResponseSuccess(flockDestination)
? flockDestination?.data?.kandangs.find(
(kandang) =>
String(kandang.project_flock_kandang_id) ===
String(targetKandang.target_project_flock_kandang.id)
)
: undefined;
return { return {
kandang: { kandang: {
value: targetKandang.target_project_flock_kandang.id, value: targetKandang.target_project_flock_kandang.id,
@@ -218,7 +211,12 @@ export const getFilledTransferToLayingFormInitialValues = async (
}, },
quantity: targetKandang.qty, quantity: targetKandang.qty,
maxQuantity: kandang?.capacity ?? 0, maxQuantity:
(mappedFlockDestinationKandangsMaxTargetQty &&
mappedFlockDestinationKandangsMaxTargetQty[
targetKandang.target_project_flock_kandang.id
].max_target_qty) ??
0,
}; };
}) })
: []; : [];
@@ -12,7 +12,10 @@ import {
UpdateTransferToLayingPayload, UpdateTransferToLayingPayload,
} from '@/types/api/production/transfer-to-laying'; } from '@/types/api/production/transfer-to-laying';
import { httpClient } from '@/services/http/client'; import { httpClient } from '@/services/http/client';
import { ProjectFlockAvailableQuantity } from '@/types/api/production/project-flock'; import {
ProjectFlockAvailableQuantity,
ProjectFlockMaxQuantity,
} from '@/types/api/production/project-flock';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
export class TransferToLayingService extends BaseApiService< export class TransferToLayingService extends BaseApiService<
@@ -132,7 +135,7 @@ export class TransferToLayingService extends BaseApiService<
} }
} }
async getAvailabelQty(projectFlockId: number) { async getAvailableQty(projectFlockId: number) {
try { try {
const availableQtyRes = await httpClient< const availableQtyRes = await httpClient<
BaseApiResponse<ProjectFlockAvailableQuantity> BaseApiResponse<ProjectFlockAvailableQuantity>
@@ -154,7 +157,7 @@ export class TransferToLayingService extends BaseApiService<
async getMappedFlockKandangsAvailability(projectFlockId: number) { async getMappedFlockKandangsAvailability(projectFlockId: number) {
try { try {
const flockAvailableQty = await this.getAvailabelQty(projectFlockId); const flockAvailableQty = await this.getAvailableQty(projectFlockId);
const flockKandangsAvailableQty = isResponseSuccess(flockAvailableQty) const flockKandangsAvailableQty = isResponseSuccess(flockAvailableQty)
? flockAvailableQty.data.kandangs ? flockAvailableQty.data.kandangs
@@ -177,6 +180,47 @@ export class TransferToLayingService extends BaseApiService<
} }
} }
async getMaxTargetQty(projectFlockId: number) {
try {
const availableQtyRes = await httpClient<
BaseApiResponse<ProjectFlockMaxQuantity>
>(`${this.basePath}/project-flocks/${projectFlockId}/max-target-qty`);
return availableQtyRes;
} catch (error) {
if (axios.isAxiosError<BaseApiResponse<ProjectFlockMaxQuantity>>(error)) {
return error.response?.data;
}
return undefined;
}
}
async getMappedFlockKandangsMaxTargetQty(projectFlockId: number) {
try {
const flockMaxTargetQty = await this.getMaxTargetQty(projectFlockId);
const flockKandangsMaxTargetQty = isResponseSuccess(flockMaxTargetQty)
? flockMaxTargetQty.data.project_flock_kandangs
: [];
const mappedFlockKandangsMaxTargetQty: Record<
number,
(typeof flockKandangsMaxTargetQty)[0]
> = {};
flockKandangsMaxTargetQty.forEach((item) => {
if (!mappedFlockKandangsMaxTargetQty[item.project_flock_kandang_id]) {
mappedFlockKandangsMaxTargetQty[item.project_flock_kandang_id] = item;
}
});
return mappedFlockKandangsMaxTargetQty;
} catch (error) {
return undefined;
}
}
async getApprovalHistory( async getApprovalHistory(
transferToLayingId: number, transferToLayingId: number,
group: boolean = true, group: boolean = true,
+8
View File
@@ -89,6 +89,14 @@ export type ProjectFlockAvailableQuantity = {
}[]; }[];
}; };
export type ProjectFlockMaxQuantity = {
project_flock_id: number;
project_flock_kandangs: {
project_flock_kandang_id: number;
max_target_qty: number;
}[];
};
export type ProjectFlockPeriods = { export type ProjectFlockPeriods = {
id: number; id: number;
name: string; name: string;