From d9c154997dee2bcc85696fbbc3003e28646a07e7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 19 Jan 2026 14:41:20 +0700 Subject: [PATCH 1/5] feat: create Closing Sapronak Summary component --- .../ClosingIncomingSapronaksSummaryTable.tsx | 174 ++++++++++++++++++ .../ClosingOutgoingSapronaksSummaryTable.tsx | 174 ++++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx create mode 100644 src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx diff --git a/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx new file mode 100644 index 00000000..49e4f108 --- /dev/null +++ b/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx @@ -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([]); + const [rowSelection, setRowSelection] = useState>({}); + + const incomingSapronaksColumns: ColumnDef[] = + [ + { + 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 = (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 ( + + +
Ringkasan Sapronak Masuk
+ + + + } + className='w-full!' + titleClassName='w-full p-0!' + > +
+ + 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, + }), + }} + /> +
+
+
+ ); +}; + +export default ClosingIncomingSapronaksSummaryTable; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx new file mode 100644 index 00000000..42fcb588 --- /dev/null +++ b/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx @@ -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([]); + const [rowSelection, setRowSelection] = useState>({}); + + const outgoingSapronaksColumns: ColumnDef[] = + [ + { + 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 = (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 ( + + +
Ringkasan Sapronak Keluar
+ + + + } + className='w-full!' + titleClassName='w-full p-0!' + > +
+ + 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, + }), + }} + /> +
+
+
+ ); +}; + +export default ClosingOutgoingSapronaksSummaryTable; From 0f64baca230c2582e71d60fc041755bad67f5858 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 19 Jan 2026 14:42:04 +0700 Subject: [PATCH 2/5] feat: show Closing Sapronak Summary table --- .../pages/closing/ClosingSapronakTabContent.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/pages/closing/ClosingSapronakTabContent.tsx b/src/components/pages/closing/ClosingSapronakTabContent.tsx index 41c7aa05..03c3c984 100644 --- a/src/components/pages/closing/ClosingSapronakTabContent.tsx +++ b/src/components/pages/closing/ClosingSapronakTabContent.tsx @@ -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 = ({ <> + + + + )} From 949b5cbc1232cc2d0d3a9f3cd5ffc0fa7cd389c6 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 19 Jan 2026 14:44:35 +0700 Subject: [PATCH 3/5] feat: create Closing Sapronak Summary type --- src/types/api/closing.d.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index ff35fd28..ec256a45 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -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; From e143668f825a1997357790d28c8f43697e19381a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 19 Jan 2026 14:46:29 +0700 Subject: [PATCH 4/5] feat: create getAllIncomingSapronakSummaryFetcher and getAllOutgoingSapronakSummaryFetcher method --- src/services/api/closing.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 323e09e8..7462e41a 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -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 { ); } + async getAllIncomingSapronakSummaryFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher< + BaseApiResponse + >(endpoint); + } + async getAllOutgoingSapronakFetcher( endpoint: string ): Promise> { @@ -70,6 +80,14 @@ export class ClosingApiService extends BaseApiService { ); } + async getAllOutgoingSapronakSummaryFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher< + BaseApiResponse + >(endpoint); + } + async getGeneralInfo( id: number ): Promise | undefined> { From 18f3295562729440a81fb64da4f330795d170b2b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Mon, 19 Jan 2026 15:28:16 +0700 Subject: [PATCH 5/5] fix(FE): fixing initial state in form recording & refactor formik message errors list --- .../recording/form/RecordingForm.tsx | 85 +++++++++++++------ 1 file changed, 61 insertions(+), 24 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index d7f913e8..5044dec5 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -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([]); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [, setNewRecordingData] = useState(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) => { - 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 && ( - setFormErrorList([])} - /> + )} +
+ {JSON.stringify(formik.errors)} +
+
+ {JSON.stringify(formik.values)} +
{/* Basic Info Card */} {(type === 'add' || type === 'edit') && (