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

This commit is contained in:
rstubryan
2026-01-19 16:00:39 +07:00
6 changed files with 446 additions and 24 deletions
@@ -0,0 +1,174 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import { cn, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
interface ClosingIncomingSapronaksSummaryTableProps {
projectFlockId: number;
}
const ClosingIncomingSapronaksSummaryTable = ({
projectFlockId,
}: ClosingIncomingSapronaksSummaryTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
nameSort: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
},
});
const {
data: incomingSapronakSummaries,
isLoading: isLoadingIncomingSapronakSummaries,
} = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/sapronak/summary${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
ClosingApi.getAllIncomingSapronakSummaryFetcher,
{
keepPreviousData: true,
}
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
[
{
header: '#',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'total_qty',
header: 'Total Kuantitas',
cell: (props) =>
`${formatNumber(props.row.original.total_qty)} ${props.row.original.uom.name}`,
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries.data.length > 0
: false
);
}
}, [incomingSapronakSummaries, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Ringkasan Sapronak Masuk</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
<Table<ClosingIncomingSapronakSummary>
data={
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries?.data
: []
}
columns={incomingSapronaksColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries?.meta?.page
: 0
}
totalItems={
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingIncomingSapronakSummaries}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(incomingSapronakSummaries) &&
incomingSapronakSummaries?.data?.length === 0,
}),
}}
/>
</div>
</Collapse>
</Card>
);
};
export default ClosingIncomingSapronaksSummaryTable;
@@ -0,0 +1,174 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import { cn, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
interface ClosingOutgoingSapronaksSummaryTableProps {
projectFlockId: number;
}
const ClosingOutgoingSapronaksSummaryTable = ({
projectFlockId,
}: ClosingOutgoingSapronaksSummaryTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
nameSort: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
},
});
const {
data: outgoingSapronakSummaries,
isLoading: isLoadingOutgoingSapronakSummaries,
} = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/sapronak/summary${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
ClosingApi.getAllIncomingSapronakSummaryFetcher,
{
keepPreviousData: true,
}
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
[
{
header: '#',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'total_qty',
header: 'Total Kuantitas',
cell: (props) =>
`${formatNumber(props.row.original.total_qty)} ${props.row.original.uom.name}`,
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries.data.length > 0
: false
);
}
}, [outgoingSapronakSummaries, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Ringkasan Sapronak Keluar</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
<Table<ClosingOutgoingSapronakSummary>
data={
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries?.data
: []
}
columns={outgoingSapronaksColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries?.meta?.page
: 0
}
totalItems={
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingOutgoingSapronakSummaries}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(outgoingSapronakSummaries) &&
outgoingSapronakSummaries?.data?.length === 0,
}),
}}
/>
</div>
</Collapse>
</Card>
);
};
export default ClosingOutgoingSapronaksSummaryTable;
@@ -2,6 +2,8 @@
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
interface ClosingSapronakTableProps {
projectFlockId?: number;
@@ -16,7 +18,15 @@ const ClosingSapronakTabContent = ({
<>
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
<ClosingIncomingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
<ClosingOutgoingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
</>
)}
</div>
@@ -79,6 +79,7 @@ import {
GROWING_RECORDING_APPROVAL_LINE,
LAYING_RECORDING_APPROVAL_LINE,
} from '@/config/approval-line';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -227,7 +228,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [, setApprovalNotes] = useState('');
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [, setNewRecordingData] = useState<Recording | null>(null);
const [nextDayRecording, setNextDayRecording] =
@@ -905,10 +905,58 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
baseValues = getRecordingGrowingFormInitialValues(initialValues);
}
if (type === 'add') {
baseValues.location = selectedLocation
? {
value: Number(selectedLocation.value),
label: selectedLocation.label,
}
: null;
baseValues.location_id = selectedLocation
? Number(selectedLocation.value)
: 0;
baseValues.project_flock = selectedProjectFlock
? {
value: Number(selectedProjectFlock.value),
label: selectedProjectFlock.label,
}
: null;
baseValues.project_flock_id = selectedProjectFlock
? Number(selectedProjectFlock.value)
: 0;
baseValues.kandang = selectedKandang
? {
value: Number(selectedKandang.value),
label: selectedKandang.label,
}
: null;
baseValues.kandang_id = selectedKandang
? Number(selectedKandang.value)
: 0;
}
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
baseValues.project_flock_kandang = {
value: projectFlockKandangDetail.project_flock.id,
label: projectFlockKandangDetail.project_flock.flock_name || '',
baseValues = {
...baseValues,
project_flock_kandang: {
value: projectFlockKandangDetail.project_flock?.id,
label: projectFlockKandangDetail.project_flock?.flock_name || '',
},
project_flock: {
value: projectFlockKandangDetail.project_flock?.id,
label: projectFlockKandangDetail.project_flock?.flock_name || '',
},
project_flock_id: projectFlockKandangDetail.project_flock?.id,
location: {
value: projectFlockKandangDetail.project_flock?.location?.id,
label: projectFlockKandangDetail.project_flock?.location?.name || '',
},
location_id: projectFlockKandangDetail.project_flock?.location?.id,
kandang: {
value: projectFlockKandangDetail.kandang?.id,
label: projectFlockKandangDetail.kandang?.name || '',
},
kandang_id: projectFlockKandangDetail.kandang?.id,
};
}
@@ -995,22 +1043,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
},
});
const handleValidateForm = async () => {
const errors = await formik.validateForm();
if (Object.keys(errors).length > 0) {
const errorMessages = getUniqueFormikErrors(errors);
setFormErrorList(errorMessages);
return;
}
};
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
// ===== HELPER FUNCTIONS =====
const getAvailableStock = useCallback(
(productWarehouseId: number) => {
@@ -1266,6 +1298,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[formik, duplicateErrorShown]
);
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
useEffect(() => {
if (projectFlockKandangLookup?.project_flock_kandang_id) {
const projectFlockKandangId =
@@ -1655,12 +1689,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
)}
<div className='text-xs text-error'>
{JSON.stringify(formik.errors)}
</div>
<div className='text-xs text-success'>
{JSON.stringify(formik.values)}
</div>
{/* Basic Info Card */}
{(type === 'add' || type === 'edit') && (
<Card
+18
View File
@@ -11,6 +11,8 @@ import {
ClosingSapronakCalculation,
ClosingProductionData,
ClosingHppExpedition,
ClosingIncomingSapronakSummary,
ClosingOutgoingSapronakSummary,
} from '@/types/api/closing';
import { BaseApiResponse } from '@/types/api/api-general';
import { httpClient, httpClientFetcher } from '@/services/http/client';
@@ -62,6 +64,14 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
);
}
async getAllIncomingSapronakSummaryFetcher(
endpoint: string
): Promise<BaseApiResponse<ClosingIncomingSapronakSummary[]>> {
return await httpClientFetcher<
BaseApiResponse<ClosingIncomingSapronakSummary[]>
>(endpoint);
}
async getAllOutgoingSapronakFetcher(
endpoint: string
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
@@ -70,6 +80,14 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
);
}
async getAllOutgoingSapronakSummaryFetcher(
endpoint: string
): Promise<BaseApiResponse<ClosingOutgoingSapronakSummary[]>> {
return await httpClientFetcher<
BaseApiResponse<ClosingOutgoingSapronakSummary[]>
>(endpoint);
}
async getGeneralInfo(
id: number
): Promise<BaseApiResponse<ClosingGeneralInformation> | undefined> {
+9
View File
@@ -11,6 +11,7 @@ import { Product } from '@type/api/master-data/product';
import { Customer } from '@type/api/master-data/customer';
import { BaseMetadata } from '@/types/api/api-general';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { BaseUom } from '@/types/api/master-data/uom';
export type BaseSales = {
id: number;
@@ -104,8 +105,16 @@ export type ClosingIncomingSapronak = {
notes: string;
};
export type ClosingIncomingSapronakSummary = {
category: string;
total_qty: number;
uom: BaseUom;
};
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
export type ClosingOutgoingSapronakSummary = ClosingIncomingSapronakSummary;
export type ClosingProductionData = {
purchase: {
initial_population: number;