Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/create-pdf-component

This commit is contained in:
rstubryan
2026-01-28 09:14:49 +07:00
7 changed files with 235 additions and 56 deletions
+20 -18
View File
@@ -176,24 +176,26 @@ const ApprovalStepsV2 = ({
})}
</div>
<Button
variant='outline'
color='none'
onClick={seeMoreClickHandler}
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'
width={20}
height={20}
className={cn('transition-all duration-300', {
'-rotate-180': isSeeAll,
})}
/>
See {isSeeAll ? 'Less' : 'More'}
</Button>
{formattedApprovals.length > maxVisibleSteps && (
<Button
variant='outline'
color='none'
onClick={seeMoreClickHandler}
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'
width={20}
height={20}
className={cn('transition-all duration-300', {
'-rotate-180': isSeeAll,
})}
/>
See {isSeeAll ? 'Less' : 'More'}
</Button>
)}
</div>
);
};
+2 -2
View File
@@ -118,7 +118,7 @@ const TextInput = ({
<div
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-success!': isValid,
@@ -182,7 +182,7 @@ const TextInput = ({
) : (
<div
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-success!': isValid,
@@ -288,6 +288,48 @@ const TransferToLayingFormModal = () => {
return { available: countAvailable, unavailable: countUnavailable };
}, [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: {
available: number;
unavailable: number;
@@ -298,9 +340,8 @@ const TransferToLayingFormModal = () => {
let countAvailable = 0;
let countUnavailable = 0;
selectedFlockDestinationRawData?.kandangs.forEach((item) => {
// TODO: change this to real available quota later
if (item.capacity > 0) {
mappedFlockDestinationKandangsMaxTargetQty.forEach((item) => {
if (item.max_target_qty > 0) {
countAvailable += 1;
} else {
countUnavailable += 1;
@@ -308,7 +349,7 @@ const TransferToLayingFormModal = () => {
});
return { available: countAvailable, unavailable: countUnavailable };
}, [selectedFlockDestinationRawData]);
}, [mappedFlockDestinationKandangsMaxTargetQty]);
const totalEnteredChickenForTransfer =
formik.values.flockSourceKandangs.reduce(
@@ -648,10 +689,9 @@ const TransferToLayingFormModal = () => {
</div>
<div className='w-full rounded-xl border border-base-content/10'>
{selectedFlockDestinationRawData?.kandangs.map(
{mappedFlockDestinationKandangsMaxTargetQty.map(
(item, itemIdx) => {
// TODO: change this to real available quota later
const isAvailable = item.capacity > 0;
const isAvailable = item.max_target_qty > 0;
const isChecked =
formik.values.flockDestinationKandangs.some(
(k) =>
@@ -669,11 +709,10 @@ const TransferToLayingFormModal = () => {
{
kandang: {
value: item.project_flock_kandang_id,
label: item.name,
label: item.kandang_name,
},
quantity: '',
// TODO: change this to real available quota later
maxQuantity: item.capacity,
maxQuantity: item.max_target_qty,
},
]);
} else {
@@ -718,9 +757,8 @@ const TransferToLayingFormModal = () => {
'cursor-not-allowed': !isAvailable,
})}
>
{item.name}{' '}
{/* TODO: change this to real available quota later */}
<span className='text-base-content/20'>{`(Max: ${item.capacity})`}</span>
{item.kandang_name}{' '}
<span className='text-base-content/20'>{`(Max: ${item.max_target_qty})`}</span>
</label>
</div>
@@ -1,6 +1,6 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
import {
CellContext,
@@ -33,6 +33,7 @@ import { cn, formatDate, formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Color } from '@/types/theme';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
const RowOptionsMenu = ({
props,
@@ -182,6 +183,9 @@ const TransferToLayingsTable = () => {
const isFilterActive = filterCount > 0;
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
// Modal hooks
const filterModal = useModal();
const deleteModal = useModal();
@@ -432,6 +436,10 @@ const TransferToLayingsTable = () => {
setIsRejectLoading(false);
};
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const filterSubmitHandler = (values: TransferToLayingFilter) => {
updateFilter('startDate', values.startDate);
updateFilter('endDate', values.endDate);
@@ -448,9 +456,12 @@ const TransferToLayingsTable = () => {
updateFilter('status', '');
};
// TODO: add export to excel functionality
const exportToExcelHandler = () => {
toast.error('Not implemented yet');
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
await TransferToLayingApi.exportToExcel(getTableFilterQueryString());
setIsLoadingExportingToExcel(false);
};
useEffect(() => {
@@ -527,7 +538,27 @@ const TransferToLayingsTable = () => {
)}
</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
variant='outline'
color='none'
@@ -590,6 +621,7 @@ const TransferToLayingsTable = () => {
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isLoadingExportingToExcel}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
@@ -176,6 +176,11 @@ export const getFilledTransferToLayingFormInitialValues = async (
initialValues?.from_project_flock.id as number
);
const mappedFlockDestinationKandangsMaxTargetQty =
await TransferToLayingApi.getMappedFlockKandangsMaxTargetQty(
initialValues?.to_project_flock.id as number
);
const formattedFlockSourceKandangs = initialValues?.sources
? initialValues.sources.map((sourceKandang) => ({
kandang: {
@@ -197,20 +202,8 @@ export const getFilledTransferToLayingFormInitialValues = async (
maxTotalQuantity += item.quantity;
});
const flockDestination = await ProjectFlockApi.getSingle(
initialValues?.to_project_flock.id as number
);
const formattedFlockDestinationKandangs = initialValues?.targets
? 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 {
kandang: {
value: targetKandang.target_project_flock_kandang.id,
@@ -218,7 +211,12 @@ export const getFilledTransferToLayingFormInitialValues = async (
},
quantity: targetKandang.qty,
maxQuantity: kandang?.capacity ?? 0,
maxQuantity:
(mappedFlockDestinationKandangsMaxTargetQty &&
mappedFlockDestinationKandangsMaxTargetQty[
targetKandang.target_project_flock_kandang.id
].max_target_qty) ??
0,
};
})
: [];
@@ -1,3 +1,4 @@
import * as XLSX from 'xlsx';
import axios from 'axios';
import { BaseApiService } from '@/services/api/base';
import {
@@ -11,9 +12,14 @@ import {
TransferToLaying,
UpdateTransferToLayingPayload,
} from '@/types/api/production/transfer-to-laying';
import { httpClient } from '@/services/http/client';
import { ProjectFlockAvailableQuantity } from '@/types/api/production/project-flock';
import { isResponseSuccess } from '@/lib/api-helper';
import { httpClient, httpClientFetcher } from '@/services/http/client';
import {
ProjectFlockAvailableQuantity,
ProjectFlockMaxQuantity,
} from '@/types/api/production/project-flock';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import toast from 'react-hot-toast';
import { formatDate } from '@/lib/helper';
export class TransferToLayingService extends BaseApiService<
TransferToLaying,
@@ -132,7 +138,7 @@ export class TransferToLayingService extends BaseApiService<
}
}
async getAvailabelQty(projectFlockId: number) {
async getAvailableQty(projectFlockId: number) {
try {
const availableQtyRes = await httpClient<
BaseApiResponse<ProjectFlockAvailableQuantity>
@@ -154,7 +160,7 @@ export class TransferToLayingService extends BaseApiService<
async getMappedFlockKandangsAvailability(projectFlockId: number) {
try {
const flockAvailableQty = await this.getAvailabelQty(projectFlockId);
const flockAvailableQty = await this.getAvailableQty(projectFlockId);
const flockKandangsAvailableQty = isResponseSuccess(flockAvailableQty)
? flockAvailableQty.data.kandangs
@@ -177,6 +183,101 @@ 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 exportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
params.set('limit', '9999999');
const queryString = `?${params.toString()}`;
try {
const transferToLayings = await httpClientFetcher<
BaseApiResponse<TransferToLaying[]>
>(`${this.basePath}${queryString}`);
if (isResponseError(transferToLayings)) {
toast.error('Gagal melakukan export transfer to laying! Coba lagi.');
return;
}
const rows = transferToLayings.data;
const formattedRows = [];
for (let i = 0; i < rows.length; i++) {
formattedRows.push({
id: rows[i].id,
transfer_number: rows[i].transfer_number,
transfer_date: formatDate(rows[i].transfer_date, 'DD-MM-YYYY'),
project_flock_source: rows[i].from_project_flock.flock_name,
project_flock_target: rows[i].to_project_flock.flock_name,
pending_usage_qty: rows[i].pending_usage_qty,
usage_qty: rows[i].usage_qty,
project_flock_source_kandang: rows[i].sources
.map((item) => item.source_project_flock_kandang.kandang.name)
.join(', '),
project_flock_target_kandang: rows[i].targets
.map((item) => item.target_project_flock_kandang.kandang.name)
.join(', '),
created_user: rows[i].created_user.name,
created_at: formatDate(rows[i].created_at, 'DD-MM-YYYY'),
updated_at: formatDate(rows[i].updated_at, 'DD-MM-YYYY'),
notes: rows[i].notes,
});
}
const ws = XLSX.utils.json_to_sheet(formattedRows);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'transfer-ke-laying');
// triggers download in browser
XLSX.writeFile(wb, 'transfer-ke-laying.xlsx');
} catch (error) {
toast.error('Gagal melakukan export transfer to laying! Coba lagi.');
}
}
async getApprovalHistory(
transferToLayingId: number,
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 = {
id: number;
name: string;