Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu

This commit is contained in:
rstubryan
2026-01-15 16:17:22 +07:00
8 changed files with 124 additions and 71 deletions
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useState } from 'react'; import { ChangeEventHandler, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table'; import { ColumnDef, SortingState } from '@tanstack/react-table';
@@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps {
const ClosingIncomingSapronaksTable = ({ const ClosingIncomingSapronaksTable = ({
projectFlockId, projectFlockId,
}: ClosingIncomingSapronaksTableProps) => { }: ClosingIncomingSapronaksTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { const {
state: tableFilterState, state: tableFilterState,
updateFilter, updateFilter,
@@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
useSWR( useSWR(
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`, `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
ClosingApi.getAllIncomingSapronakFetcher, ClosingApi.getAllIncomingSapronakFetcher,
{ {
keepPreviousData: true, keepPreviousData: true,
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useState } from 'react'; import { ChangeEventHandler, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table'; import { ColumnDef, SortingState } from '@tanstack/react-table';
@@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps {
const ClosingOutgoingSapronaksTable = ({ const ClosingOutgoingSapronaksTable = ({
projectFlockId, projectFlockId,
}: ClosingOutgoingSapronaksTableProps) => { }: ClosingOutgoingSapronaksTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { const {
state: tableFilterState, state: tableFilterState,
updateFilter, updateFilter,
@@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
useSWR( useSWR(
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`, `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
ClosingApi.getAllOutgoingSapronakFetcher, ClosingApi.getAllOutgoingSapronakFetcher,
{ {
keepPreviousData: true, keepPreviousData: true,
@@ -1,5 +1,6 @@
'use client'; 'use client';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing'; import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
@@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps {
const ClosingProductionDataTabContent = ({ const ClosingProductionDataTabContent = ({
projectFlockId, projectFlockId,
}: ClosingProductionDataTabContentProps) => { }: ClosingProductionDataTabContentProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const { data: productionData, isLoading } = useSWR( const { data: productionData, isLoading } = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/production-data`, `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
() => ClosingApi.getProductionData(projectFlockId) () => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
); );
if (isLoading) { if (isLoading) {
@@ -83,7 +83,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
const formikInitialValues = useMemo<NonstockFormValues>(() => { const formikInitialValues = useMemo<NonstockFormValues>(() => {
return { return {
name: initialValues?.name ?? '', name: initialValues?.name ?? '',
uomId: initialValues?.uom_id ?? 0, uomId: initialValues?.uom?.id ?? 0,
uom: initialValues?.uom uom: initialValues?.uom
? { ? {
value: initialValues?.uom?.id, value: initialValues?.uom?.id,
@@ -2,34 +2,30 @@ import * as Yup from 'yup';
// Schema for LAYING category (production_standard_details is required) // Schema for LAYING category (production_standard_details is required)
const LayingRepeaterFormSchema = Yup.object({ const LayingRepeaterFormSchema = Yup.object({
week: Yup.number().required('Minggu wajib diisi!'), week: Yup.number().required('Wajib diisi!'),
production_standard_uniformity_details: Yup.object({ production_standard_uniformity_details: Yup.object({
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), target_mean_bw: Yup.number().required('Wajib diisi!'),
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), max_depletion: Yup.number().required('Wajib diisi!'),
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), min_uniformity: Yup.number().required('Wajib diisi!'),
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), feed_intake: Yup.number().required('Wajib diisi!'),
}), }),
production_standard_details: Yup.object({ production_standard_details: Yup.object({
target_hen_day_production: Yup.number().required( target_hen_day_production: Yup.number().required('Wajib diisi!'),
'Produksi telur per hari wajib diisi!' target_hen_house_production: Yup.number().required('Wajib diisi!'),
), target_egg_weight: Yup.number().required('Wajib diisi!'),
target_hen_house_production: Yup.number().required( target_egg_mass: Yup.number().required('Wajib diisi!'),
'Produksi telur per kandang wajib diisi!' standard_fcr: Yup.number().required('Wajib diisi!'),
),
target_egg_weight: Yup.number().required('Berat telur wajib diisi!'),
target_egg_mass: Yup.number().required('Massa telur wajib diisi!'),
standard_fcr: Yup.number().required('FCR wajib diisi!'),
}).required(), }).required(),
}); });
// Schema for GROWING category (production_standard_details is optional) // Schema for GROWING category (production_standard_details is optional)
const GrowingRepeaterFormSchema = Yup.object({ const GrowingRepeaterFormSchema = Yup.object({
week: Yup.number().required('Minggu wajib diisi!'), week: Yup.number().required('Wajib diisi!'),
production_standard_uniformity_details: Yup.object({ production_standard_uniformity_details: Yup.object({
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), target_mean_bw: Yup.number().required('Wajib diisi!'),
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), max_depletion: Yup.number().required('Wajib diisi!'),
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), min_uniformity: Yup.number().required('Wajib diisi!'),
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), feed_intake: Yup.number().required('Wajib diisi!'),
}), }),
production_standard_details: Yup.object({ production_standard_details: Yup.object({
target_hen_day_production: Yup.number().optional(), target_hen_day_production: Yup.number().optional(),
@@ -344,7 +344,7 @@ const ProductionStandardForm = ({
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => { const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
const baseColumns: ColumnDef<TableRowsType>[] = [ const baseColumns: ColumnDef<TableRowsType>[] = [
{ {
header: 'Minggu', header: 'Week',
accessorKey: 'week', accessorKey: 'week',
enableSorting: false, enableSorting: false,
}, },
@@ -358,30 +358,40 @@ const ProductionStandardForm = ({
header: 'Hen Day', header: 'Hen Day',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.target_hen_day_production, row.production_standard_details?.target_hen_day_production,
cell: ({ row }) =>
`${row.original.production_standard_details?.target_hen_day_production}%`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'Hen House', header: 'Hen House',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.target_hen_house_production, row.production_standard_details?.target_hen_house_production,
cell: ({ row }) =>
`${row.original.production_standard_details?.target_hen_house_production} pc`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'Egg Weight', header: 'Egg Weight',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.target_egg_weight, row.production_standard_details?.target_egg_weight,
cell: ({ row }) =>
`${row.original.production_standard_details?.target_egg_weight} g`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'Egg Mass', header: 'Egg Mass',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.target_egg_mass, row.production_standard_details?.target_egg_mass,
cell: ({ row }) =>
`${row.original.production_standard_details?.target_egg_mass} g`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'FCR', header: 'FCR',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_details?.standard_fcr, row.production_standard_details?.standard_fcr,
cell: ({ row }) =>
`${row.original.production_standard_details?.standard_fcr} g`,
enableSorting: false, enableSorting: false,
}, },
] ]
@@ -393,24 +403,32 @@ const ProductionStandardForm = ({
header: 'Mean BW', header: 'Mean BW',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_uniformity_details?.target_mean_bw, row.production_standard_uniformity_details?.target_mean_bw,
cell: ({ row }) =>
`${row.original.production_standard_uniformity_details?.target_mean_bw} g`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'Max Depletion', header: 'Max Depletion',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_uniformity_details?.max_depletion, row.production_standard_uniformity_details?.max_depletion,
cell: ({ row }) =>
`${row.original.production_standard_uniformity_details?.max_depletion}%`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'Min Uniformity', header: 'Min Uniformity',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_uniformity_details?.min_uniformity, row.production_standard_uniformity_details?.min_uniformity,
cell: ({ row }) =>
`${row.original.production_standard_uniformity_details?.min_uniformity}%`,
enableSorting: false, enableSorting: false,
}, },
{ {
header: 'Feed Intake', header: 'Feed Intake',
accessorFn: (row) => accessorFn: (row) =>
row.production_standard_uniformity_details?.feed_intake, row.production_standard_uniformity_details?.feed_intake,
cell: ({ row }) =>
`${row.original.production_standard_uniformity_details?.feed_intake} g`,
enableSorting: false, enableSorting: false,
}, },
]; ];
@@ -728,7 +746,52 @@ const ProductionStandardForm = ({
}; };
// ===== Formik Error List ===== // ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
formik,
{
onBeforeSubmit: (e) => {
e.preventDefault();
// For GROWING category, clear production_standard_details errors and set default values
if (formik.values.project_category === 'GROWING') {
// Set default values for production_standard_details
formik.values.details?.forEach((detail) => {
detail.production_standard_details = {
target_hen_day_production: 0,
target_hen_house_production: 0,
target_egg_weight: 0,
target_egg_mass: 0,
standard_fcr: 0,
};
});
// Clear any errors related to production_standard_details
const currentErrors = { ...formik.errors };
if (currentErrors.details && Array.isArray(currentErrors.details)) {
const cleanedDetails = currentErrors.details
.map((detailError) => {
if (detailError && typeof detailError === 'object') {
const { production_standard_details, ...rest } = detailError;
return Object.keys(rest).length > 0 ? rest : undefined;
}
return detailError;
})
.filter(
(error): error is Exclude<typeof error, undefined> =>
error !== undefined
);
currentErrors.details = (
cleanedDetails.length > 0 ? cleanedDetails : undefined
) as typeof currentErrors.details;
}
formik.setErrors(currentErrors);
}
return true;
},
}
);
return ( return (
<> <>
@@ -821,19 +884,20 @@ const ProductionStandardForm = ({
key={`row-${row.index}`} key={`row-${row.index}`}
className='sticky bottom-0 bg-base-100 shadow-lg' className='sticky bottom-0 bg-base-100 shadow-lg'
> >
<td colSpan={colSpan} className='p-6'> <td colSpan={colSpan} className='p-2'>
<form <form
className='h-full w-full flex flex-col justify-end' className='h-full w-full flex flex-col justify-end'
onSubmit={repeaterFormik.handleSubmit} onSubmit={repeaterFormik.handleSubmit}
onReset={repeaterFormik.handleReset} onReset={repeaterFormik.handleReset}
> >
<div <div
className={cn( className='grid gap-2 items-start w-full'
'grid gap-4 items-start', style={{
formik.values.project_category === 'LAYING' gridTemplateColumns:
? 'grid-cols-10' formik.values.project_category === 'LAYING'
: 'grid-cols-5' ? 'repeat(10, minmax(auto, 1fr)) minmax(auto, auto)'
)} : 'repeat(4, minmax(auto, 1fr)) minmax(auto, auto)',
}}
> >
<NumberInput <NumberInput
name='week' name='week'
@@ -862,7 +926,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={<Icon icon='mdi:percent' />} bottomLabel='Persen (%)'
errorMessage={getProductionDetailsError( errorMessage={getProductionDetailsError(
repeaterFormik.errors repeaterFormik.errors
.production_standard_details, .production_standard_details,
@@ -894,11 +958,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={ bottomLabel='Butir (pc)'
<div className='w-full h-full flex items-center justify-center'>
Butir
</div>
}
errorMessage={getProductionDetailsError( errorMessage={getProductionDetailsError(
repeaterFormik.errors repeaterFormik.errors
.production_standard_details, .production_standard_details,
@@ -930,11 +990,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={ bottomLabel='Gram (g)'
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
errorMessage={getProductionDetailsError( errorMessage={getProductionDetailsError(
repeaterFormik.errors repeaterFormik.errors
.production_standard_details, .production_standard_details,
@@ -959,17 +1015,13 @@ const ProductionStandardForm = ({
name='production_standard_details.target_egg_mass' name='production_standard_details.target_egg_mass'
label='Egg Mass' label='Egg Mass'
placeholder='1' placeholder='1'
bottomLabel='Gram (g)'
value={ value={
repeaterFormik.values repeaterFormik.values
.production_standard_details?.target_egg_mass .production_standard_details?.target_egg_mass
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
errorMessage={getProductionDetailsError( errorMessage={getProductionDetailsError(
repeaterFormik.errors repeaterFormik.errors
.production_standard_details, .production_standard_details,
@@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={ bottomLabel='Gram (g)'
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
errorMessage={getProductionDetailsError( errorMessage={getProductionDetailsError(
repeaterFormik.errors repeaterFormik.errors
.production_standard_details, .production_standard_details,
@@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={ bottomLabel='Gram (g)'
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
errorMessage={ errorMessage={
repeaterFormik.errors repeaterFormik.errors
.production_standard_uniformity_details .production_standard_uniformity_details
@@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={<Icon icon='mdi:percent' />} bottomLabel='Persen (%)'
errorMessage={ errorMessage={
repeaterFormik.errors repeaterFormik.errors
.production_standard_uniformity_details .production_standard_uniformity_details
@@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={<Icon icon='mdi:percent' />} bottomLabel='Persen (%)'
errorMessage={ errorMessage={
repeaterFormik.errors repeaterFormik.errors
.production_standard_uniformity_details .production_standard_uniformity_details
@@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({
} }
onChange={repeaterFormik.handleChange} onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur} onBlur={repeaterFormik.handleBlur}
endAdornment={ bottomLabel='Gram/Ekor (g)'
<div className='w-full h-full flex items-center justify-center'> endAdornment
gr/ekor
</div>
}
errorMessage={ errorMessage={
repeaterFormik.errors repeaterFormik.errors
.production_standard_uniformity_details .production_standard_uniformity_details
@@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({
type='button' type='button'
color='error' color='error'
variant='outline' variant='outline'
className='min-w-24' className='min-w-xs'
onClick={handleCancelEdit} onClick={handleCancelEdit}
> >
<Icon icon='mdi:close' /> Batal <Icon icon='mdi:close' /> Batal
@@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({
<Button <Button
type='submit' type='submit'
color={editMode ? 'warning' : 'success'} color={editMode ? 'warning' : 'success'}
className='min-w-24' className='min-w-xs'
disabled={ disabled={
isAddingRow || isAddingRow ||
formik.values.project_category === '' formik.values.project_category === ''
@@ -1195,7 +1236,7 @@ const ProductionStandardForm = ({
variant='outline' variant='outline'
color='primary' color='primary'
onClick={toggleTableHeight} onClick={toggleTableHeight}
className='absolute bottom-6 right-6' className='absolute bottom-2 right-2'
> >
<Icon <Icon
icon={ icon={
+3 -2
View File
@@ -91,10 +91,11 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
} }
async getProductionData( async getProductionData(
id: number id: number,
kandangId?: number
): Promise<BaseApiResponse<ClosingProductionData> | undefined> { ): Promise<BaseApiResponse<ClosingProductionData> | undefined> {
try { try {
const getProductionDataPath = `${this.basePath}/${id}/production-data`; const getProductionDataPath = `${this.basePath}/${id}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`;
const getProductionDataRes = await httpClient< const getProductionDataRes = await httpClient<
BaseApiResponse<ClosingProductionData> BaseApiResponse<ClosingProductionData>
>(getProductionDataPath); >(getProductionDataPath);
+3
View File
@@ -38,6 +38,9 @@ export const useFormikErrorList = <T>(
// Validate form // Validate form
const isValid = await handleValidateForm(); const isValid = await handleValidateForm();
if (isValid) {
close();
}
// Call onAfterValidation callback if validation passed // Call onAfterValidation callback if validation passed
if (options?.onAfterValidation) { if (options?.onAfterValidation) {