From 4e00ded84366488c9a0b33aeabaf671529452565 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 29 Jan 2026 15:56:31 +0700 Subject: [PATCH 01/39] chore: add new step in PROJECT_FLOCKS approval workflows --- src/config/constant.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index fff9fa92..5eb5df38 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -418,6 +418,10 @@ export const APPROVAL_WORKFLOWS = { step_number: 2, step_name: 'Aktif', }, + { + step_number: 3, + step_name: 'Selesai', + }, ], RECORDINGS: [ { From c5269c4fc540ebb443a0bb09a786cbcafa0fe596 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 29 Jan 2026 16:17:27 +0700 Subject: [PATCH 02/39] fix: adjust ProjectFlockTable UI --- .../project-flock/ProjectFlockTable.tsx | 448 +++++++++++++++--- 1 file changed, 386 insertions(+), 62 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index ab14ef84..cad76310 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -13,6 +13,7 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import Table from '@/components/Table'; +import Dropdown from '@/components/Dropdown'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { cn, formatDate, formatTitleCase } from '@/lib/helper'; @@ -29,6 +30,111 @@ import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; +import StatusBadge from '@/components/helper/StatusBadge'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; + +const RowOptionsMenu = ({ + props, + popoverPosition = 'bottom', + editClickHandler, + detailClickHandler, + deleteClickHandler, +}: { + props: CellContext; + popoverPosition: 'bottom' | 'top'; + editClickHandler: (id: number) => void; + detailClickHandler: (id: number) => void; + deleteClickHandler: () => void; +}) => { + // TODO: change this to real condition + const showEditButton = true; + + const showDeleteButton = showEditButton; + + const popoverId = `projectFlock#${props.row.original.id}`; + const popoverAnchorName = `--anchor-projectFlock#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + + const detailClickHandlerWrapper = () => { + detailClickHandler(props.row.original.id); + closePopover(); + }; + + const editClickHandlerWrapper = () => { + editClickHandler(props.row.original.id); + closePopover(); + }; + + return ( +
+ + + + + +
+ + + + + {showEditButton && ( + + + + )} + + {showDeleteButton && ( + +
+ +
+ )} +
+
+
+ ); +}; const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const { @@ -62,6 +168,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const selectedRowIds = Object.keys(rowSelection) .filter((id) => rowSelection[id]) .map((id) => parseInt(id)); + const [selectedArea, setSelectedArea] = useState(null); const [selectedLocation, setSelectedLocation] = useState( null @@ -78,6 +185,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); // ===== Fetch Data ===== const { @@ -175,14 +284,27 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { : null; }, [rowSelection]); + // const canApprove = useMemo(() => { + // if (!selectedSingleRow || isApproveLoading) return false; + + // const isPengajuan = selectedSingleRow.approval?.step_number == 1; + // const isNotRejected = selectedSingleRow.approval?.action != 'REJECTED'; + + // return isPengajuan && isNotRejected; + // }, [selectedSingleRow, isApproveLoading]); + const canApprove = useMemo(() => { - if (!selectedSingleRow || isApproveLoading) return false; + return selectedRowIds.every((id) => { + const projectFlock = isResponseSuccess(projectFlocks) + ? projectFlocks?.data.find((row) => row.id === id) + : null; - const isPengajuan = selectedSingleRow.approval?.step_number == 1; - const isNotRejected = selectedSingleRow.approval?.action != 'REJECTED'; - - return isPengajuan && isNotRejected; - }, [selectedSingleRow, isApproveLoading]); + const isProjectFlockRequesting = projectFlock?.approval?.step_number == 1; + const isProjectFlockNotRejected = + projectFlock?.approval?.action != 'REJECTED'; + return isProjectFlockRequesting && isProjectFlockNotRejected; + }); + }, [selectedRowIds, projectFlocks]); // ====== COLUMNS ====== const columns = useMemo[]>( @@ -256,44 +378,39 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const approval = props.row.original.approval; const isRejected = approval?.action == 'REJECTED'; const isApproved = approval?.action == 'APPROVED'; - return ( - - - {isRejected - ? 'Ditolak' - : formatTitleCase(approval?.step_name || '')} - + : 'neutral'; + + switch (approval.action.toLowerCase()) { + case 'pengajuan': + latestApprovalStepName = 'Pengajuan'; + break; + case 'aktif': + latestApprovalStepName = 'Aktif'; + break; + case 'Selesai': + latestApprovalStepName = 'Closing'; + break; + } + + if (isRejected) { + latestApprovalStepName = 'Ditolak'; + } + + return ( + ); }, }, @@ -325,27 +442,88 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { cell: (props) => formatDate(props.row.original.created_at, 'MMM DD, YYYY'), }, + { + id: 'actions', + cell: (props) => { + const currentPageSize = + props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const detailClickHandler = (id: number) => { + router.push( + `/production/project-flock/detail/?projectFlockId=${id}` + ); + }; + + const editClickHandler = (id: number) => { + router.push( + `/production/project-flock/detail/edit/?projectFlockId=${id}` + ); + }; + + const deleteClickHandler = () => { + // Set row selection + setRowSelection({ + [String(props.row.original.id)]: true, + }); + + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, ], [] ); + const exportToExcelHandler = async () => { + setIsLoadingExportingToExcel(true); + + toast.error('Not implemented yet!'); + + setIsLoadingExportingToExcel(false); + }; + + const bulkApproveClickHandler = () => { + setApprovalAction('APPROVED'); + confirmModal.openModal(); + }; + + const bulkRejectClickHandler = () => { + setApprovalAction('REJECTED'); + confirmModal.openModal(); + }; + return ( <> -
-
-
+
+
+ {/*
@@ -423,6 +601,158 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { }} />
+
*/} + +
+
+ + + + + {selectedRowIds.length > 0 && canApprove && ( + <> +
+ + + + + + + + + + )} +
+ +
+ + } + 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', + }} + /> + + + + +
+ + + Export + +
+ + +
+ + } + > + + +
@@ -448,26 +778,20 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setSorting={setSorting} rowSelection={rowSelection} setRowSelection={setRowSelection} + withCheckbox className={{ - containerClassName: cn({ - 'mb-40': + containerClassName: cn('p-3', { + 'w-full mb-20': isResponseSuccess(projectFlocks) && - projectFlocks?.data?.length > 0, + projectFlocks?.data?.length === 0, }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', + headerColumnClassName: 'text-nowrap', }} />
- void }) => { onClose={() => { setRowSelection({}); }} - /> + /> */} Date: Mon, 2 Feb 2026 11:15:38 +0700 Subject: [PATCH 03/39] chore: lint --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bdfffa4..1922fa2b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,8 +23,8 @@ default: - node_modules/ variables: - NODE_ENV: "" - NPM_CONFIG_PRODUCTION: "false" + NODE_ENV: '' + NPM_CONFIG_PRODUCTION: 'false' script: # Install dependencies @@ -69,7 +69,7 @@ default: EOF artifacts: - name: "out-$CI_COMMIT_SHORT_SHA" + name: 'out-$CI_COMMIT_SHORT_SHA' paths: - out/ expire_in: 1 week From 39b18f7efcf9fb23ecff86602dd3f85fb49774bf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 3 Feb 2026 09:17:17 +0700 Subject: [PATCH 04/39] refactor: use Modal instead of Drawer --- src/app/production/project-flock/layout.tsx | 31 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx index 3e9a65b7..3e46bbfc 100644 --- a/src/app/production/project-flock/layout.tsx +++ b/src/app/production/project-flock/layout.tsx @@ -2,9 +2,10 @@ import { usePathname, useRouter } from 'next/navigation'; import Drawer from '@/components/Drawer'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect } from 'react'; import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable'; import { useUiStore } from '@/stores/ui/ui.store'; +import Modal, { useModal } from '@/components/Modal'; export default function ProjectFlockLayout({ children, @@ -23,9 +24,12 @@ export default function ProjectFlockLayout({ const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing; + const formModal = useModal(); + const handleBackdropClick = () => { const unsub = useUiStore.getState().subscribeIsValid((isValid) => { if (isValid) { + formModal.closeModal(); unsub(); // berhenti listen router.push('/production/project-flock'); } @@ -34,6 +38,14 @@ export default function ProjectFlockLayout({ toggleValidate(); }; + useEffect(() => { + if (isOpen && !formModal.open) { + formModal.openModal(); + } else { + formModal.closeModal(); + } + }, [isOpen]); + return ( <> {/* List page always rendered */} @@ -44,7 +56,7 @@ export default function ProjectFlockLayout({
{/* Render Drawer only on /add */} - { if (!v) router.push('/production/project-flock'); @@ -54,7 +66,20 @@ export default function ProjectFlockLayout({ variant='right' zIndex='99999' sidebarContent={isOpen &&
{children}
} - /> + /> */} + + +
+ {isOpen && children} +
+
); } From d826746f2973547479f1f0cb39903b8cf3d2ede2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 3 Feb 2026 09:39:58 +0700 Subject: [PATCH 05/39] chore: adjust DrawerHeader styling --- src/components/helper/drawer/DrawerHeader.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/helper/drawer/DrawerHeader.tsx b/src/components/helper/drawer/DrawerHeader.tsx index 8bb635ae..ef3ba1db 100644 --- a/src/components/helper/drawer/DrawerHeader.tsx +++ b/src/components/helper/drawer/DrawerHeader.tsx @@ -27,7 +27,7 @@ export interface DrawerHeaderProps { const DrawerHeader = ({ leftIcon = 'mdi:close', - leftIconSize = 24, + leftIconSize = 20, leftIconHref, leftIconOnClick, leftIconClassName, @@ -43,7 +43,7 @@ const DrawerHeader = ({ icon={leftIcon} width={leftIconSize} height={leftIconSize} - className={cn('cursor-pointer', leftIconClassName)} + className={cn('cursor-pointer text-base-content ', leftIconClassName)} /> ); @@ -73,7 +73,7 @@ const DrawerHeader = ({ return (
@@ -82,7 +82,7 @@ const DrawerHeader = ({ {renderLeftIcon()} {showDivider && subtitle && ( -
+
)} {subtitle && ( From 68f3c95b818523ea20681f8fd4e7f5cf2a5c361d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 3 Feb 2026 09:40:16 +0700 Subject: [PATCH 06/39] chore: adjust ProjectFlockForm styling --- .../project-flock/form/ProjectFlockForm.tsx | 683 ++++++++++-------- 1 file changed, 365 insertions(+), 318 deletions(-) diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 89769787..f0ee4752 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -658,37 +658,48 @@ const ProjectFlockForm = ({ return ( <> -
+
{/* Header */} {formType == 'edit' && ( + )} + + {formType == 'add' && ( + )} @@ -719,145 +730,147 @@ const ProjectFlockForm = ({ onReset={formik.handleReset} > {/* Form Informasi Umum */} -
-
-

Informasi Umum

-
- - - { - return flock.label === formik.values.flock_name; - })?.value, - } as OptionType) - : undefined - } - onChange={(val) => { - optionChangeHandler(val, 'flock'); - setSelectedFlock((val as OptionType)?.label); - formik.setFieldValue( - 'flock_name', - (val as OptionType)?.label - ); - }} - options={optionsFlock} - onInputChange={setInputValueFlock} - onMenuScrollToBottom={loadMoreFlock} - isLoading={isLoadingFlocks} - isError={ - formik.touched.flock_name && Boolean(formik.errors.flock_name) - } - errorMessage={formik.errors.flock_name as string} - isClearable - isDisabled={formType != 'add'} - /> - { - optionChangeHandler(val, 'fcr'); - }} - onInputChange={setInputValueFcr} - onMenuScrollToBottom={loadMoreFcr} - options={optionsFcr} - isLoading={isLoadingFcrs} - isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)} - errorMessage={formik.errors.fcr_id as string} - isClearable - isDisabled={formType != 'add'} - /> - - { - optionChangeHandler(val, 'production_standard'); - }} - onInputChange={setInputValueProductionStandard} - onMenuScrollToBottom={loadMoreProductionStandard} - options={optionsProductionStandards} - isLoading={isLoadingProductionStandards} - isError={ - formik.touched.production_standard_id && - Boolean(formik.errors.production_standard_id) - } - errorMessage={formik.errors.production_standard_id as string} - isClearable - isDisabled={formType != 'add'} - /> - -
+
+

+ Informasi Umum +

+ + + + { + return flock.label === formik.values.flock_name; + })?.value, + } as OptionType) + : undefined + } + onChange={(val) => { + optionChangeHandler(val, 'flock'); + setSelectedFlock((val as OptionType)?.label); + formik.setFieldValue('flock_name', (val as OptionType)?.label); + }} + options={optionsFlock} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} + isLoading={isLoadingFlocks} + isError={ + formik.touched.flock_name && Boolean(formik.errors.flock_name) + } + errorMessage={formik.errors.flock_name as string} + isClearable + isDisabled={formType != 'add'} + /> + { + optionChangeHandler(val, 'fcr'); + }} + onInputChange={setInputValueFcr} + onMenuScrollToBottom={loadMoreFcr} + options={optionsFcr} + isLoading={isLoadingFcrs} + isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)} + errorMessage={formik.errors.fcr_id as string} + isClearable + isDisabled={formType != 'add'} + /> + + { + optionChangeHandler(val, 'production_standard'); + }} + onInputChange={setInputValueProductionStandard} + onMenuScrollToBottom={loadMoreProductionStandard} + options={optionsProductionStandards} + isLoading={isLoadingProductionStandards} + isError={ + formik.touched.production_standard_id && + Boolean(formik.errors.production_standard_id) + } + errorMessage={formik.errors.production_standard_id as string} + isClearable + isDisabled={formType != 'add'} + /> +
{/* Form Pilih Kandang */} -
-
-

Pilih Kandang

+ +
+

+ Informasi Kandang +

{isLoadingKandang && ( - + )} {/* Card Estimasi Budget */} -
-
-

- Estimasi Anggaran Per Flock -

+
{formik.values.project_budgets && formik.values.project_budgets.length > 0 ? ( formik.values.project_budgets.map((budget, index) => ( - -
-
-
Anggaran ke-{index + 1}
- +
*/} + +
+

+ Estimasi Anggaran Per Flock +

+ + +
+ +
+ { + const updatedBudgets = [ + ...formik.values.project_budgets, + ]; + updatedBudgets[index].nonstock = val as OptionType; + updatedBudgets[index].nonstock_id = + (val as OptionType) ? (val as OptionType).value : 0; + formik.setFieldValue( + 'project_budgets', + updatedBudgets + ); + formik.setFieldTouched( + `project_budgets[${index}].nonstock_id`, + true + ); + }} + errorMessage={ + ( + formik.errors.project_budgets?.[ index - ) - } - > - - -
-
- { - const updatedBudgets = [ - ...formik.values.project_budgets, - ]; - updatedBudgets[index].nonstock = val as OptionType; - updatedBudgets[index].nonstock_id = - (val as OptionType) - ? (val as OptionType).value - : 0; - formik.setFieldValue( - 'project_budgets', - updatedBudgets - ); - formik.setFieldTouched( - `project_budgets[${index}].nonstock_id`, - true - ); - }} - errorMessage={ + ] as FormikErrors + )?.nonstock_id as string + } + isError={ + formik.touched.project_budgets?.[index] + ?.nonstock_id && + Boolean( ( formik.errors.project_budgets?.[ index ] as FormikErrors )?.nonstock_id as string - } - isError={ - formik.touched.project_budgets?.[index] - ?.nonstock_id && - Boolean( - ( - formik.errors.project_budgets?.[ - index - ] as FormikErrors - )?.nonstock_id as string - ) - } - /> -
-
- - handleBudgetChange(index, 'qty', e.target.value) - } - onBlur={formik.handleBlur} - allowNegative={false} - endAdornment={ -
+ ) + } + /> +
+
+ + handleBudgetChange(index, 'qty', e.target.value) + } + onBlur={formik.handleBlur} + allowNegative={false} + inputPrefix={ +
+ {isResponseSuccess(nonstocks) ? (nonstocks.data.find( (ns) => ns.id === budget.nonstock_id )?.uom?.name ?? '') : ''} -
- } - errorMessage={ + + +
+
+ } + errorMessage={ + ( + formik.errors.project_budgets?.[ + index + ] as FormikErrors + )?.qty as string + } + isError={ + formik.touched.project_budgets?.[index]?.qty && + Boolean( ( formik.errors.project_budgets?.[ index ] as FormikErrors )?.qty as string - } - isError={ - formik.touched.project_budgets?.[index]?.qty && - Boolean( - ( - formik.errors.project_budgets?.[ - index - ] as FormikErrors - )?.qty as string - ) - } - /> -
-
- - handleBudgetChange(index, 'price', e.target.value) - } - onBlur={formik.handleBlur} - placeholder='Masukkan harga satuan' - allowNegative={false} - startAdornment='Rp' - endAdornment={ -
- {`Per ${ - isResponseSuccess(nonstocks) - ? (nonstocks.data.find( - (ns) => ns.id === budget.nonstock_id - )?.uom?.name ?? 'Item') - : 'Item' - }`} -
- } - errorMessage={ + ) + } + className={{ + inputPrefix: + 'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0', + inputWrapper: 'border-l-0 pl-5', + }} + /> +
+
+ + handleBudgetChange(index, 'price', e.target.value) + } + onBlur={formik.handleBlur} + placeholder='Masukkan Harga Satuan (Rp)' + allowNegative={false} + inputPrefix={ +
+ + Rp + + +
+
+ } + errorMessage={ + ( + formik.errors.project_budgets?.[ + index + ] as FormikErrors + )?.price as string + } + isError={ + formik.touched.project_budgets?.[index]?.price && + Boolean( ( formik.errors.project_budgets?.[ index ] as FormikErrors )?.price as string - } - isError={ - formik.touched.project_budgets?.[index]?.price && - Boolean( - ( - formik.errors.project_budgets?.[ - index - ] as FormikErrors - )?.price as string - ) - } - /> -
-
- - handleBudgetChange( - index, - 'total_price', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan harga total' - allowNegative={false} - startAdornment='Rp' - endAdornment={ -
Total
- } - errorMessage={ + ) + } + className={{ + inputPrefix: + 'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0', + inputWrapper: 'border-l-0 pl-5', + }} + /> +
+
+ + handleBudgetChange( + index, + 'total_price', + e.target.value + ) + } + onBlur={formik.handleBlur} + placeholder='Masukkan Total Harga' + allowNegative={false} + inputPrefix={ +
+ + Rp + + +
+
+ } + errorMessage={ + ( + formik.errors.project_budgets?.[ + index + ] as FormikErrors + )?.total_price as string + } + isError={ + formik.touched.project_budgets?.[index] + ?.total_price && + Boolean( ( formik.errors.project_budgets?.[ index ] as FormikErrors )?.total_price as string - } - isError={ - formik.touched.project_budgets?.[index] - ?.total_price && - Boolean( - ( - formik.errors.project_budgets?.[ - index - ] as FormikErrors - )?.total_price as string - ) - } - /> -
+ ) + } + className={{ + inputPrefix: + 'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0', + inputWrapper: 'border-l-0 pl-5', + }} + />
- +
)) ) : (
@@ -1093,7 +1135,9 @@ const ProjectFlockForm = ({
- +
+ +
{formType !== 'detail' && ( @@ -1133,6 +1177,9 @@ const ProjectFlockForm = ({ isLoading: isDeleteLoading, onClick: confirmationModalDeleteClickHandler, }} + className={{ + modal: 'w-full sm:w-[446px]', + }} /> ); From dc5bd6b3292e2a6e8a78af9f801b94bef91b37b9 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 3 Feb 2026 09:40:34 +0700 Subject: [PATCH 07/39] chore: adjust ProjectFlockKandangTable styling --- .../form/ProjectFlockKandangTable.tsx | 93 +++++++++---------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx b/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx index 14a9b11a..bba4a229 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockKandangTable.tsx @@ -2,6 +2,7 @@ import Badge from '@/components/Badge'; import Card from '@/components/Card'; +import StatusBadge from '@/components/helper/StatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import PillBadge from '@/components/PillBadge'; import Table from '@/components/Table'; @@ -32,6 +33,14 @@ const ProjectFlockKandangTable = ({ initialValues?: ProjectFlock; formType: 'add' | 'edit' | 'detail'; }) => { + const availableKandang = listKandang.filter( + (kandang) => kandang.status == 'NON_ACTIVE' + ).length; + + const unavailableKandang = listKandang.filter( + (kandang) => kandang.status != 'NON_ACTIVE' + ).length; + // Fungsi untuk menangani perubahan checkbox const handleCheckboxChange = (kandang: Kandang, isChecked: boolean) => { // Hanya izinkan perubahan jika tidak dalam mode 'detail' @@ -57,48 +66,30 @@ const ProjectFlockKandangTable = ({ {listKandang.length > 0 ? ( <> {/* ... Bagian Badge Status ... */} -
- - - Tersedia ( - { - listKandang.filter((kandang) => kandang.status == 'NON_ACTIVE') - .length - } - ) - -
- + + +
+ + - - Tidak Tersedia ( - { - listKandang.filter((kandang) => kandang.status != 'NON_ACTIVE') - .length - } - ) - + text={`Tidak Tersedia (${unavailableKandang})`} + className={{ badge: 'text-nowrap' }} + />
{/* --- */} -
+
{listKandang.map((kandang, index) => { const kandangIdString = kandang.id?.toString() ?? `temp-${index}`; @@ -112,28 +103,36 @@ const ProjectFlockKandangTable = ({ formType == 'detail' || kandang.status != 'NON_ACTIVE'; return ( -
+
handleCheckboxChange(kandang, e.currentTarget.checked) } - /> - - - {kandang.status != 'NON_ACTIVE' && 'Tidak'} Tersedia - + /> + +
); })} From ac3fbedccd91580035f120265c8d9adfe64ea38e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 10:46:37 +0700 Subject: [PATCH 08/39] refactor(FE): Rename filter keys to plural forms --- src/components/pages/finance/FinanceTable.tsx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index c1c7f079..ba2101b8 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -152,10 +152,10 @@ const FinanceTable = () => { } = useTableFilter({ initial: { search: searchValue, - transactionType: '', - bankId: '', - customerId: '', - supplierId: '', + transactionTypes: '', + bankIds: '', + customerIds: '', + supplierIds: '', sortBy: '', startDate: '', endDate: '', @@ -163,10 +163,10 @@ const FinanceTable = () => { paramMap: { page: 'page', pageSize: 'limit', - transactionType: 'transaction_type', - bankId: 'bank_id', - customerId: 'customer_id', - supplierId: 'supplier_id', + transactionTypes: 'transaction_types', + bankIds: 'bank_ids', + customerIds: 'customer_ids', + supplierIds: 'supplier_ids', sortBy: 'sort_date', startDate: 'start_date', endDate: 'end_date', @@ -178,10 +178,10 @@ const FinanceTable = () => { const deleteModal = useModal(); const [pendingFilters, setPendingFilters] = useState({ search: searchValue, - transactionType: '', - bankId: '', - customerId: '', - supplierId: '', + transactionTypes: '', + bankIds: '', + customerIds: '', + supplierIds: '', sortBy: '', startDate: '', endDate: '', @@ -247,7 +247,7 @@ const FinanceTable = () => { setSelectedTransactionType(val); setPendingFilters((prev) => ({ ...prev, - transactionType: val + transactionTypes: val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) @@ -258,7 +258,7 @@ const FinanceTable = () => { setSelectedBank(val); setPendingFilters((prev) => ({ ...prev, - bankId: val + bankIds: val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) @@ -269,7 +269,7 @@ const FinanceTable = () => { setSelectedCustomerId(val); setPendingFilters((prev) => ({ ...prev, - customerId: val + customerIds: val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) @@ -280,7 +280,7 @@ const FinanceTable = () => { setSelectedSupplierId(val); setPendingFilters((prev) => ({ ...prev, - supplierId: val + supplierIds: val ? Array.isArray(val) ? val.map((item) => item.value).join(',') : (val.value as string) @@ -307,10 +307,10 @@ const FinanceTable = () => { const submitFilterHandler = () => { updateFilter('search', pendingFilters.search); setSearchValue(pendingFilters.search); - updateFilter('transactionType', pendingFilters.transactionType); - updateFilter('bankId', pendingFilters.bankId); - updateFilter('customerId', pendingFilters.customerId); - updateFilter('supplierId', pendingFilters.supplierId); + updateFilter('transactionTypes', pendingFilters.transactionTypes); + updateFilter('bankIds', pendingFilters.bankIds); + updateFilter('customerIds', pendingFilters.customerIds); + updateFilter('supplierIds', pendingFilters.supplierIds); updateFilter('sortBy', pendingFilters.sortBy); updateFilter('startDate', pendingFilters.startDate); updateFilter('endDate', pendingFilters.endDate); @@ -324,10 +324,10 @@ const FinanceTable = () => { const emptyFilters = { search: '', - transactionType: '', - bankId: '', - customerId: '', - supplierId: '', + transactionTypes: '', + bankIds: '', + customerIds: '', + supplierIds: '', sortBy: '', startDate: '', endDate: '', @@ -336,10 +336,10 @@ const FinanceTable = () => { updateFilter('search', ''); resetSearchValue(); - updateFilter('transactionType', ''); - updateFilter('bankId', ''); - updateFilter('customerId', ''); - updateFilter('supplierId', ''); + updateFilter('transactionTypes', ''); + updateFilter('bankIds', ''); + updateFilter('customerIds', ''); + updateFilter('supplierIds', ''); updateFilter('sortBy', ''); updateFilter('startDate', ''); updateFilter('endDate', ''); From 3153423f14ec6d638b00d955f57593a16268f309 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 10:48:26 +0700 Subject: [PATCH 09/39] refactor(FE): Replace size-full with w-full in FinanceTable --- src/components/pages/finance/FinanceTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index ba2101b8..670ad5a4 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -483,7 +483,7 @@ const FinanceTable = () => { }, [resetSearchValue]); return ( -
+
@@ -601,22 +591,34 @@ const FinanceTable = () => { isClearable />
diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/FinanceTableFilter.schema.ts new file mode 100644 index 00000000..bc1053d3 --- /dev/null +++ b/src/components/pages/finance/FinanceTableFilter.schema.ts @@ -0,0 +1,39 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type FinanceTableFilterType = { + search: string; + transaction_types: string; + bank_ids: string; + customer_ids: string; + supplier_ids: string; + sort_by: string; + start_date: string; + end_date: string; +}; + +export const FinanceTableFilterSchema = yup.object({ + search: yup.string().optional(), + transaction_types: yup.string().optional(), + bank_ids: yup.string().optional(), + customer_ids: yup.string().optional(), + supplier_ids: yup.string().optional(), + sort_by: yup.string().optional(), + start_date: yup.string().optional(), + end_date: yup + .string() + .optional() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), +}) as yup.ObjectSchema; + +export type FinanceTableFilterValues = yup.InferType< + typeof FinanceTableFilterSchema +>; From 4aa9d54b1e6182a5668cc939556a68a9095afc96 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 11:02:20 +0700 Subject: [PATCH 12/39] refactor(FE): Remove unused search params and yup import --- src/components/pages/finance/FinanceTable.tsx | 40 +++++++------------ .../finance/FinanceTableFilter.schema.ts | 1 - 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index aba80e4e..40091237 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -3,7 +3,6 @@ import { CellContext } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { useFormik } from 'formik'; -import * as yup from 'yup'; import Button from '@/components/Button'; import Card from '@/components/Card'; @@ -174,7 +173,6 @@ const FinanceTable = () => { }); // ===== State ===== - const [searchParams, setSearchParams] = useSearchParams(); const deleteModal = useModal(); const [selectedTransactionType, setSelectedTransactionType] = useState< OptionType | OptionType[] | null @@ -254,6 +252,20 @@ const FinanceTable = () => { loadMore: bankLoadMore, } = useSelect(BankApi.basePath, 'id', 'alias'); + const bankSelectOptions = useMemo(() => { + if (!isResponseSuccess(bankRawData)) return []; + + return bankOptions.map((bank) => { + const bankData = bankRawData.data.find((data) => data.id === bank?.value); + return { + label: bankData + ? `${bankData.alias} - ${bankData.account_number} - ${bankData.owner}` + : '', + value: bank?.value, + }; + }); + }, [bankOptions, bankRawData]); + // ===== Handler ===== const searchChangeHandler = (e: React.ChangeEvent) => { filterFormik.setFieldValue('search', e.target.value); @@ -311,10 +323,6 @@ const FinanceTable = () => { val ? ((val as OptionType).value as string) : '' ); }; - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -454,18 +462,15 @@ const FinanceTable = () => { }, []); useEffect(() => { - // Store current path on mount previousPathRef.current = window.location.pathname; return () => { const currentPath = window.location.pathname; - // if both paths are within /finance module const isCurrentPathFinance = currentPath.includes('/finance'); const isPreviousPathFinance = previousPathRef.current?.includes('/finance'); - // reset if we outside finance module entirely if (isPreviousPathFinance && !isCurrentPathFinance) { resetSearchValue(); } @@ -558,22 +563,7 @@ const FinanceTable = () => { isMulti /> ({ - label: - bankRawData.data.find((data) => data.id === bank?.value) - ?.alias + - ' - ' + - bankRawData.data.find((data) => data.id === bank?.value) - ?.account_number + - ' - ' + - bankRawData.data.find((data) => data.id === bank?.value) - ?.owner, - value: bank?.value, - })) - : [] - } + options={bankSelectOptions} label='Bank' value={selectedBank} onChange={bankChangeHandler} diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/FinanceTableFilter.schema.ts index bc1053d3..fecfc35d 100644 --- a/src/components/pages/finance/FinanceTableFilter.schema.ts +++ b/src/components/pages/finance/FinanceTableFilter.schema.ts @@ -1,4 +1,3 @@ -import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; export type FinanceTableFilterType = { From 372b439ff01cdc542f861da5c0ba52bc9a413d2d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 11:13:53 +0700 Subject: [PATCH 13/39] refactor(FE): Validate date range and show persistent toast --- src/components/pages/finance/FinanceTable.tsx | 92 +++++++++++++++++-- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 40091237..6f422753 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -189,6 +189,7 @@ const FinanceTable = () => { const [selectedSortBy, setSelectedSortBy] = useState(null); const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [dateErrorShown, setDateErrorShown] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ @@ -323,6 +324,70 @@ const FinanceTable = () => { val ? ((val as OptionType).value as string) : '' ); }; + + const startDateChangeHandler = (e: React.ChangeEvent) => { + const value = e.target.value; + const endDate = filterFormik.values.end_date; + + filterFormik.setFieldValue('start_date', value); + + if (value && endDate) { + const startDate = new Date(value); + const endDateObj = new Date(endDate); + + if (endDateObj < startDate) { + filterFormik.setFieldError( + 'end_date', + 'Tanggal akhir tidak boleh masa lampau' + ); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + filterFormik.setFieldError('end_date', undefined); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } + }; + + const endDateChangeHandler = (e: React.ChangeEvent) => { + const value = e.target.value; + const startDate = filterFormik.values.start_date; + + filterFormik.setFieldValue('end_date', value); + + if (value && startDate) { + const startDateObj = new Date(startDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + filterFormik.setFieldError( + 'end_date', + 'Tanggal akhir tidak boleh masa lampau' + ); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + filterFormik.setFieldError('end_date', undefined); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -461,6 +526,14 @@ const FinanceTable = () => { ]; }, []); + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + useEffect(() => { previousPathRef.current = window.location.pathname; @@ -474,8 +547,13 @@ const FinanceTable = () => { if (isPreviousPathFinance && !isCurrentPathFinance) { resetSearchValue(); } + + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }; - }, [resetSearchValue]); + }, [resetSearchValue, dateErrorShown]); return (
@@ -584,25 +662,23 @@ const FinanceTable = () => { name='start_date' label='Periode Tanggal (Mulai)' value={filterFormik.values.start_date} - onChange={filterFormik.handleChange} + onChange={startDateChangeHandler} errorMessage={ - filterFormik.touched.start_date && filterFormik.errors.start_date - ? filterFormik.errors.start_date + filterFormik.errors.end_date + ? filterFormik.errors.end_date : undefined } - onBlur={filterFormik.handleBlur} /> Date: Thu, 5 Feb 2026 12:04:08 +0700 Subject: [PATCH 14/39] refactor(FE): Use API metadata for table pagination --- src/components/pages/marketing/MarketingTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index e09617aa..0a35a8bc 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -623,7 +623,10 @@ const MarketingTable = () => { data={allData} columns={columns} pageSize={tableFilterState.pageSize} - page={tableFilterState.page} + page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1} + totalItems={ + isResponseSuccess(marketing) ? marketing?.meta?.total_results : 0 + } isLoading={isLoadingMarketing} className={{ containerClassName: cn('p-3', { From 1af2b72beade136c4eccf69b2957528056657f03 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 12:06:04 +0700 Subject: [PATCH 15/39] refactor(FE): Prevent badge text wrapping --- src/components/pages/expense/ExpenseStatusBadge.tsx | 2 +- src/components/pages/expense/RealizationStatusBadge.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index eee84224..854b4d34 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -49,7 +49,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { color={expenseStatusBadgeColor} text={isLatestApprovalRejected ? 'Ditolak' : (approval?.step_name ?? '')} className={{ - badge: 'w-fit', + badge: 'whitespace-nowrap max-w-max w-fit', }} /> ); diff --git a/src/components/pages/expense/RealizationStatusBadge.tsx b/src/components/pages/expense/RealizationStatusBadge.tsx index d04d35c3..eb429473 100644 --- a/src/components/pages/expense/RealizationStatusBadge.tsx +++ b/src/components/pages/expense/RealizationStatusBadge.tsx @@ -29,7 +29,7 @@ const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => { color={realizationStatusBadgeColor} text={isLatestApprovalRejected ? 'Ditolak' : realizationStatus} className={{ - badge: 'w-fit', + badge: 'whitespace-nowrap max-w-max w-fit', }} /> ); From d41600d8e229bf476d662a969f164596b1afc54a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 13:48:07 +0700 Subject: [PATCH 16/39] refactor(FE): Replace week SelectInputRadio with NumberInput --- .../delivery-order/DeliverOrderProduct.tsx | 21 ++++++++++++------- .../sales-order/SalesOrderProductForm.tsx | 21 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index 850d88d2..f5089c84 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -511,19 +511,24 @@ const DeliveryOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', val); + onChange={(e) => { + formik.setFieldValue('week', Number(e.target.value)); + setCurrentInput(e.target.name); }} - placeholder='Pilih Week' + onBlur={() => handleBlurField('week')} + isError={formik.touched.week && Boolean(formik.errors.week)} + errorMessage={formik.errors.week as string} + placeholder='Masukan Minggu' + decimalScale={0} /> )} diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index c718c40c..ae1e4a44 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -467,19 +467,24 @@ const SalesOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', val); + onChange={(e) => { + formik.setFieldValue('week', Number(e.target.value)); + setCurrentInput(e.target.name); }} - placeholder='Pilih Week' + onBlur={() => handleBlurField('week')} + isError={formik.touched.week && Boolean(formik.errors.week)} + errorMessage={formik.errors.week as string} + placeholder='Masukan Minggu' + decimalScale={0} /> )} From 3b221795baf19b5eb945ddc3008147b3046430a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:08:05 +0700 Subject: [PATCH 17/39] refactor(FE): Add filter_by option to customer payment reports --- .../export/CustomerPaymentExportPDF.tsx | 3 +- .../report/finance/tab/CustomerPaymentTab.tsx | 103 ++++++++++-------- src/services/api/report/finance-report.ts | 6 +- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index e6c5d66e..d132be9a 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -96,8 +96,7 @@ interface CustomerPaymentExportPDFParams { // sales?: string; start_date?: string; end_date?: string; - // TODO: Uncomment when BE is ready - // filter_by?: string; + filter_by?: string; }; } diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 2987455a..4f12a4b8 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -14,7 +14,6 @@ import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { CustomerPaymentReport, - CustomerPaymentRow, CustomerPaymentSummary, } from '@/types/api/report/customer-payment'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -53,33 +52,39 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ); // TODO: Uncomment when BE is ready // const [filterSales, setFilterSales] = useState([]); - const [filterSales, setFilterSales] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); const filterModal = useModal(); + const dataTypeOptions = useMemo( + () => [ + { value: 'do_date', label: 'Tanggal Jual' }, + { value: 'payment_date', label: 'Tanggal Bayar' }, + { value: 'realization_date', label: 'Tanggal Realisasi' }, + ], + [] + ); + + const [filterByType, setFilterByType] = useState<(typeof dataTypeOptions)[0]>( + dataTypeOptions[0] + ); + const { options: customerOptions, setInputValue: setCustomerInputValue, isLoadingOptions: isLoadingCustomers, loadMore: loadMoreCustomers, - hasMore: hasMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); // TODO: Uncomment when BE is ready - const { - options: salesOptions, - setInputValue: setSalesInputValue, - isLoadingOptions: isLoadingSales, - loadMore: loadMoreSales, - hasMore: hasMoreSales, - } = useSelect(UserApi.basePath, 'id', 'name', 'search'); - - const dataTypeOptions = useMemo( - () => [{ value: 'do_date', label: 'Tanggal Jual' }], - [] - ); + // const { + // options: salesOptions, + // setInputValue: setSalesInputValue, + // isLoadingOptions: isLoadingSales, + // loadMore: loadMoreSales, + // hasMore: hasMoreSales, + // } = useSelect(UserApi.basePath, 'id', 'name', 'search'); const getPaymentStatusColor = (notes: string) => { const normalizedValue = notes.toLowerCase(); @@ -125,10 +130,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); - setFilterSales([]); + // setFilterSales([]); + setFilterByType(dataTypeOptions[0]); setFilterStartDate(''); setFilterEndDate(''); - }, []); + }, [dataTypeOptions]); const handleApplyFilters = useCallback(() => { setIsSubmitted(true); @@ -157,12 +163,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // } return count; - }, [ - filterStartDate, - filterEndDate, - filterCustomer, - // filterSales, - ]); + }, [filterStartDate, filterEndDate, filterCustomer]); const hasFilters = activeFiltersCount > 0; @@ -180,7 +181,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, - // filter_by: 'do_date' as const, + filter_by: filterByType.value as + | 'do_date' + | 'payment_date' + | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, page: currentPage, @@ -193,8 +197,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ([, params]) => FinanceApi.getCustomerPaymentReport( params.customer_ids, - undefined, // TODO: Change to params.sales_id when BE is ready - undefined, // TODO: Change to params.filter_by when BE is ready + params.filter_by, params.start_date, params.end_date, params.page, @@ -224,6 +227,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, + filter_by: filterByType.value as + | 'do_date' + | 'payment_date' + | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, limit: 100, @@ -232,8 +239,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const response = await FinanceApi.getCustomerPaymentReport( params.customer_ids, - undefined, // TODO: Change to params.sales_id when BE is ready - undefined, // TODO: Change to params.filter_by when BE is ready + params.filter_by, params.start_date, params.end_date, params.page, @@ -243,7 +249,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [filterCustomer, filterSales, filterStartDate, filterEndDate]); + }, [ + filterCustomer, + // filterSales, + filterStartDate, + filterEndDate, + filterByType, + ]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -297,8 +309,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // : undefined, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, - // TODO: Uncomment when BE is ready - // filter_by: 'do_date' as const, + filter_by: filterByType.value as + | 'do_date' + | 'payment_date' + | 'realization_date', }, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -406,7 +420,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { footer: () =>
Total
, }, { - id: 'do_date_or_payment_date', + id: 'trans_date', header: 'Tanggal Jual/Bayar', accessorKey: 'trans_date', enableSorting: false, @@ -864,17 +878,20 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { />
*/} - {/* TODO: Uncomment when BE is ready */} - {/*
- -
*/} +
+ { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> +
{/* Action Buttons */}
diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 1102f99c..f9c296d8 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -15,9 +15,7 @@ export class FinanceApiService extends BaseApiService< customer_ids?: string, // TODO: Uncomment when BE is ready // sales_id?: string, - // filter_by?: 'do_date', - sales_id?: string, - filter_by?: 'do_date' | undefined, + filter_by?: 'do_date' | 'payment_date' | 'realization_date', start_date?: string, end_date?: string, page?: number, @@ -31,7 +29,7 @@ export class FinanceApiService extends BaseApiService< customer_ids: customer_ids, // TODO: Uncomment when BE is ready // sales_id: sales_id, - // filter_by: filter_by, + filter_by: filter_by, start_date: start_date, end_date: end_date, page: page, From fb1b310d1d49da40d4ee779073fd02170c43ac90 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:13:13 +0700 Subject: [PATCH 18/39] refactor(FE): Replace SelectInput with SelectInputRadio --- .../report/finance/tab/CustomerPaymentTab.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4f12a4b8..91f8df42 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -3,12 +3,13 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; -import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import { useSelect } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; -import { UserApi } from '@/services/api/user'; +// import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; @@ -878,20 +879,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { />
*/} -
- { - if (val && !Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - /> -
+ { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> {/* Action Buttons */}
From 92886fe5e229811c42af49cd4b36a1036e18c083 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:28:14 +0700 Subject: [PATCH 19/39] refactor(FE): Consolidate date filters into trans_date --- .../report/finance/tab/CustomerPaymentTab.tsx | 18 ++++-------------- src/services/api/report/finance-report.ts | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 91f8df42..05aa36c0 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -60,8 +60,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const dataTypeOptions = useMemo( () => [ - { value: 'do_date', label: 'Tanggal Jual' }, - { value: 'payment_date', label: 'Tanggal Bayar' }, + { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, { value: 'realization_date', label: 'Tanggal Realisasi' }, ], [] @@ -182,10 +181,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as - | 'do_date' - | 'payment_date' - | 'realization_date', + filter_by: filterByType.value as 'trans_date' | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, page: currentPage, @@ -228,10 +224,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as - | 'do_date' - | 'payment_date' - | 'realization_date', + filter_by: filterByType.value as 'trans_date' | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, limit: 100, @@ -310,10 +303,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // : undefined, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, - filter_by: filterByType.value as - | 'do_date' - | 'payment_date' - | 'realization_date', + filter_by: filterByType.value as 'trans_date' | 'realization_date', }, }); toast.success('PDF berhasil dibuat dan diunduh.'); diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index f9c296d8..95a85b85 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -15,7 +15,7 @@ export class FinanceApiService extends BaseApiService< customer_ids?: string, // TODO: Uncomment when BE is ready // sales_id?: string, - filter_by?: 'do_date' | 'payment_date' | 'realization_date', + filter_by?: 'trans_date' | 'realization_date', start_date?: string, end_date?: string, page?: number, From eb95afe9a04c89a9c099920a898539a771f36667 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:39:33 +0700 Subject: [PATCH 20/39] refactor(FE): Separate applied and modal filter state --- .../report/finance/tab/CustomerPaymentTab.tsx | 101 ++++++++++++------ 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 05aa36c0..7160a273 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -48,6 +48,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const [isSubmitted, setIsSubmitted] = useState(false); // ===== FILTER STATE ===== + const [appliedFilterCustomer, setAppliedFilterCustomer] = useState< + typeof customerOptions + >([]); + // TODO: Uncomment when BE is ready + // const [appliedFilterSales, setAppliedFilterSales] = useState< + // typeof salesOptions + // >([]); + const [appliedFilterByType, setAppliedFilterByType] = useState< + (typeof dataTypeOptions)[0] + >({ value: 'trans_date', label: 'Tanggal Jual/Bayar' }); + const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); + const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [filterCustomer, setFilterCustomer] = useState( [] ); @@ -124,23 +137,47 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== FILTER HANDLERS ===== const handleFilterModalOpen = useCallback(() => { + setFilterCustomer(appliedFilterCustomer); + // setFilterSales(appliedFilterSales); + setFilterByType(appliedFilterByType); + setFilterStartDate(appliedFilterStartDate); + setFilterEndDate(appliedFilterEndDate); filterModal.openModal(); - }, [filterModal]); + }, [ + filterModal, + appliedFilterCustomer, + appliedFilterByType, + appliedFilterStartDate, + appliedFilterEndDate, + ]); const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); - // setFilterSales([]); setFilterByType(dataTypeOptions[0]); setFilterStartDate(''); setFilterEndDate(''); + setAppliedFilterCustomer([]); + setAppliedFilterByType(dataTypeOptions[0]); + setAppliedFilterStartDate(''); + setAppliedFilterEndDate(''); }, [dataTypeOptions]); const handleApplyFilters = useCallback(() => { + setAppliedFilterCustomer(filterCustomer); + setAppliedFilterByType(filterByType); + setAppliedFilterStartDate(filterStartDate); + setAppliedFilterEndDate(filterEndDate); setIsSubmitted(true); setCurrentPage(1); filterModal.closeModal(); - }, [filterModal]); + }, [ + filterModal, + filterCustomer, + filterByType, + filterStartDate, + filterEndDate, + ]); // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { @@ -163,7 +200,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // } return count; - }, [filterStartDate, filterEndDate, filterCustomer]); + }, [appliedFilterStartDate, appliedFilterEndDate, appliedFilterCustomer]); const hasFilters = activeFiltersCount > 0; @@ -173,17 +210,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ? () => { const params = { customer_ids: - filterCustomer.length > 0 - ? filterCustomer.map((v) => String(v.value)).join(',') + appliedFilterCustomer.length > 0 + ? appliedFilterCustomer.map((v) => String(v.value)).join(',') : undefined, // TODO: Uncomment when BE is ready // sales_id: - // filterSales.length > 0 - // ? filterSales.map((v) => String(v.value)).join(',') + // appliedFilterSales.length > 0 + // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as 'trans_date' | 'realization_date', - start_date: filterStartDate || undefined, - end_date: filterEndDate || undefined, + filter_by: appliedFilterByType.value as + | 'trans_date' + | 'realization_date', + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, page: currentPage, limit: pageSize, }; @@ -216,17 +255,17 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { > => { const params = { customer_ids: - filterCustomer.length > 0 - ? filterCustomer.map((v) => String(v.value)).join(',') + appliedFilterCustomer.length > 0 + ? appliedFilterCustomer.map((v) => String(v.value)).join(',') : undefined, // TODO: Uncomment when BE is ready // sales_id: - // filterSales.length > 0 - // ? filterSales.map((v) => String(v.value)).join(',') + // appliedFilterSales.length > 0 + // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as 'trans_date' | 'realization_date', - start_date: filterStartDate || undefined, - end_date: filterEndDate || undefined, + filter_by: appliedFilterByType.value as 'trans_date' | 'realization_date', + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, limit: 100, page: 1, }; @@ -244,11 +283,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ? (response.data as unknown as CustomerPaymentReport[]) : null; }, [ - filterCustomer, - // filterSales, - filterStartDate, - filterEndDate, - filterByType, + appliedFilterCustomer, + // appliedFilterSales, + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterByType, ]); // ===== EXPORT HANDLERS ===== @@ -293,17 +332,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { data: allDataForExport, params: { customer_name: - filterCustomer.length > 0 - ? filterCustomer.map((c) => c.label).join(', ') + appliedFilterCustomer.length > 0 + ? appliedFilterCustomer.map((c) => c.label).join(', ') : undefined, // TODO: Uncomment when BE is ready // sales: - // filterSales.length > 0 - // ? filterSales.map((s) => s.label).join(', ') + // appliedFilterSales.length > 0 + // ? appliedFilterSales.map((s) => s.label).join(', ') // : undefined, - start_date: filterStartDate || undefined, - end_date: filterEndDate || undefined, - filter_by: filterByType.value as 'trans_date' | 'realization_date', + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, + filter_by: appliedFilterByType.value as + | 'trans_date' + | 'realization_date', }, }); toast.success('PDF berhasil dibuat dan diunduh.'); From b4353cf834249f35d307cc75a0784a4c1e404b4c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:55:27 +0700 Subject: [PATCH 21/39] refactor(FE): Make filter type nullable and use applied filters --- .../report/finance/tab/CustomerPaymentTab.tsx | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 7160a273..e26abceb 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -56,8 +56,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // typeof salesOptions // >([]); const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] - >({ value: 'trans_date', label: 'Tanggal Jual/Bayar' }); + (typeof dataTypeOptions)[0] | null + >(null); const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); @@ -79,9 +79,9 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { [] ); - const [filterByType, setFilterByType] = useState<(typeof dataTypeOptions)[0]>( - dataTypeOptions[0] - ); + const [filterByType, setFilterByType] = useState< + (typeof dataTypeOptions)[0] | null + >(null); const { options: customerOptions, @@ -154,14 +154,14 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); - setFilterByType(dataTypeOptions[0]); + setFilterByType(null); setFilterStartDate(''); setFilterEndDate(''); setAppliedFilterCustomer([]); - setAppliedFilterByType(dataTypeOptions[0]); + setAppliedFilterByType(null); setAppliedFilterStartDate(''); setAppliedFilterEndDate(''); - }, [dataTypeOptions]); + }, []); const handleApplyFilters = useCallback(() => { setAppliedFilterCustomer(filterCustomer); @@ -184,23 +184,33 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { let count = 0; // Date filter (start_date + end_date = 1 filter) - if (filterStartDate || filterEndDate) { + if (appliedFilterStartDate || appliedFilterEndDate) { count += 1; } // Customer filter - if (filterCustomer.length > 0) { + if (appliedFilterCustomer.length > 0) { + count += 1; + } + + // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) + if (appliedFilterByType) { count += 1; } // TODO: Uncomment when BE is ready // // Sales filter - // if (filterSales.length > 0) { + // if (appliedFilterSales.length > 0) { // count += 1; // } return count; - }, [appliedFilterStartDate, appliedFilterEndDate, appliedFilterCustomer]); + }, [ + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterCustomer, + appliedFilterByType, + ]); const hasFilters = activeFiltersCount > 0; @@ -218,9 +228,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // appliedFilterSales.length > 0 // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: appliedFilterByType.value as + filter_by: appliedFilterByType?.value as | 'trans_date' - | 'realization_date', + | 'realization_date' + | undefined, start_date: appliedFilterStartDate || undefined, end_date: appliedFilterEndDate || undefined, page: currentPage, @@ -263,7 +274,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // appliedFilterSales.length > 0 // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: appliedFilterByType.value as 'trans_date' | 'realization_date', + filter_by: appliedFilterByType?.value as + | 'trans_date' + | 'realization_date' + | undefined, start_date: appliedFilterStartDate || undefined, end_date: appliedFilterEndDate || undefined, limit: 100, @@ -342,9 +356,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // : undefined, start_date: appliedFilterStartDate || undefined, end_date: appliedFilterEndDate || undefined, - filter_by: appliedFilterByType.value as + filter_by: appliedFilterByType?.value as | 'trans_date' - | 'realization_date', + | 'realization_date' + | undefined, }, }); toast.success('PDF berhasil dibuat dan diunduh.'); From 4fd4374e64a8722d20a15a4fb7119eaf968af7d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 15:04:12 +0700 Subject: [PATCH 22/39] refactor(FE): Validate date range and show toast on error --- .../report/finance/tab/CustomerPaymentTab.tsx | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index e26abceb..4e0e3f25 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -60,6 +60,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { >(null); const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); const [filterCustomer, setFilterCustomer] = useState( [] @@ -161,7 +163,12 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setAppliedFilterByType(null); setAppliedFilterStartDate(''); setAppliedFilterEndDate(''); - }, []); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, [dateErrorShown]); const handleApplyFilters = useCallback(() => { setAppliedFilterCustomer(filterCustomer); @@ -179,6 +186,67 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { filterEndDate, ]); + const handleStartDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterStartDate(value); + + if (value && filterEndDate) { + const startDate = new Date(value); + const endDateObj = new Date(filterEndDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }, + [filterEndDate, dateErrorShown] + ); + + const handleEndDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterEndDate(value); + + if (value && filterStartDate) { + const startDateObj = new Date(filterStartDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + [filterStartDate, dateErrorShown] + ); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; @@ -872,9 +940,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { { - setFilterStartDate(e.target.value); - }} + onChange={handleStartDateChange} className={{ wrapper: 'w-full' }} isNestedModal /> @@ -883,9 +949,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { { - setFilterEndDate(e.target.value); - }} + onChange={handleEndDateChange} className={{ wrapper: 'w-full' }} isNestedModal /> @@ -951,6 +1015,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { From 70a9fa15eca83003abd1440784f651cb5707dbc4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 15:43:03 +0700 Subject: [PATCH 23/39] refactor(FE): Switch week input to SelectInputRadio --- .../delivery-order/DeliverOrderProduct.tsx | 22 ++++++++----------- .../sales-order/SalesOrderProductForm.tsx | 22 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index f5089c84..cdeded75 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -511,24 +511,20 @@ const DeliveryOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', Number(e.target.value)); - setCurrentInput(e.target.name); + onChange={(val) => { + formik.setFieldValue('week', val); + handleBlurField('week'); }} - onBlur={() => handleBlurField('week')} - isError={formik.touched.week && Boolean(formik.errors.week)} - errorMessage={formik.errors.week as string} - placeholder='Masukan Minggu' - decimalScale={0} + placeholder='Pilih Week' /> )} diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index ae1e4a44..d5327644 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -467,24 +467,20 @@ const SalesOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', Number(e.target.value)); - setCurrentInput(e.target.name); + onChange={(val) => { + formik.setFieldValue('week', val); + handleBlurField('week'); }} - onBlur={() => handleBlurField('week')} - isError={formik.touched.week && Boolean(formik.errors.week)} - errorMessage={formik.errors.week as string} - placeholder='Masukan Minggu' - decimalScale={0} + placeholder='Pilih Week' /> )} From d5eeadc9a7a3f4c5df8376714e23ca7cb1b6a57f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 15:47:05 +0700 Subject: [PATCH 24/39] refactor(FE): Remove handleBlurField call on week change --- .../form/repeater/delivery-order/DeliverOrderProduct.tsx | 1 - .../form/repeater/sales-order/SalesOrderProductForm.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index cdeded75..850d88d2 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -522,7 +522,6 @@ const DeliveryOrderProductForm = ({ } onChange={(val) => { formik.setFieldValue('week', val); - handleBlurField('week'); }} placeholder='Pilih Week' /> diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index d5327644..c718c40c 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -478,7 +478,6 @@ const SalesOrderProductForm = ({ } onChange={(val) => { formik.setFieldValue('week', val); - handleBlurField('week'); }} placeholder='Pilih Week' /> From db7219e2614cbf51703dd3b13e4355f963579d14 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 6 Feb 2026 09:42:45 +0700 Subject: [PATCH 25/39] chore: add shadow-bg class name --- src/app/globals.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/globals.css b/src/app/globals.css index 96339bf2..fde8d2b4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -68,6 +68,8 @@ --shadow-button-soft: 0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200); + + --shadow-bg: 0px -2px 4px 0px #00000014; } html { From 66f017549cfdec9aecd1d0a80ec0a9b8252ef3a8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 6 Feb 2026 09:43:15 +0700 Subject: [PATCH 26/39] chore: use modal for project flock layout --- src/app/production/project-flock/layout.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx index 3e46bbfc..1b1b4edf 100644 --- a/src/app/production/project-flock/layout.tsx +++ b/src/app/production/project-flock/layout.tsx @@ -1,7 +1,6 @@ 'use client'; import { usePathname, useRouter } from 'next/navigation'; -import Drawer from '@/components/Drawer'; import React, { ReactNode, useEffect } from 'react'; import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable'; import { useUiStore } from '@/stores/ui/ui.store'; @@ -55,19 +54,7 @@ export default function ProjectFlockLayout({ />
- {/* Render Drawer only on /add */} - {/* { - if (!v) router.push('/production/project-flock'); - }} - closeOnBackdropClick={isDetail ? true : false} - onBackdropClick={handleBackdropClick} - variant='right' - zIndex='99999' - sidebarContent={isOpen &&
{children}
} - /> */} - + {/* Render Modal only on /add */} -
+
{isOpen && children}
From f6cf4a29ad1bda490ff90990356ba5426b39ac93 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 6 Feb 2026 09:43:27 +0700 Subject: [PATCH 27/39] feat: add ref prop to Alert --- src/components/Alert.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx index 61792d0c..6b243cf9 100644 --- a/src/components/Alert.tsx +++ b/src/components/Alert.tsx @@ -1,15 +1,16 @@ -import { ReactNode } from 'react'; +import { ReactNode, Ref } from 'react'; import { cn } from '@/lib/helper'; interface AlertProps { + ref?: Ref | undefined; variant?: 'outline' | 'dash' | 'soft'; color?: 'info' | 'success' | 'warning' | 'error'; children?: ReactNode; className?: string; } -const Alert = ({ children, variant, color, className }: AlertProps) => { +const Alert = ({ children, ref, variant, color, className }: AlertProps) => { const alertBaseClassName = cn('alert', { 'alert-soft': variant === 'soft', 'alert-outline': variant === 'outline', @@ -21,7 +22,11 @@ const Alert = ({ children, variant, color, className }: AlertProps) => { 'alert-error': color === 'error', }); - return
{children}
; + return ( +
+ {children} +
+ ); }; export default Alert; From f0637e2ce9d75c4444bbd99c2efff208e7b9f60f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 6 Feb 2026 09:43:34 +0700 Subject: [PATCH 28/39] chore: add title prop --- src/components/helper/ApprovalStepsV2.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/helper/ApprovalStepsV2.tsx b/src/components/helper/ApprovalStepsV2.tsx index dc21453e..0288b0f9 100644 --- a/src/components/helper/ApprovalStepsV2.tsx +++ b/src/components/helper/ApprovalStepsV2.tsx @@ -9,6 +9,7 @@ import Button from '@/components/Button'; import { cn, formatDate } from '@/lib/helper'; interface ApprovalStepsV2Props { + title?: string; approvals?: BaseApproval[]; steps: { step_number: number; @@ -23,6 +24,7 @@ interface ApprovalStepsV2Props { } const ApprovalStepsV2 = ({ + title = 'Progress Details', approvals, steps, maxVisibleSteps = 2, @@ -99,7 +101,7 @@ const ApprovalStepsV2 = ({ )} >

- Progress Details + {title}

Date: Fri, 6 Feb 2026 09:43:56 +0700 Subject: [PATCH 29/39] feat: add onClick prop and set text type to react node --- src/components/helper/StatusBadge.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/helper/StatusBadge.tsx b/src/components/helper/StatusBadge.tsx index c90ddaee..d1b32a24 100644 --- a/src/components/helper/StatusBadge.tsx +++ b/src/components/helper/StatusBadge.tsx @@ -1,24 +1,30 @@ +import { ReactNode } from 'react'; + import Badge from '@/components/Badge'; + import { cn } from '@/lib/helper'; import { Color } from '@/types/theme'; interface StatusBadgeProps { color: Color; - text: string; + text: ReactNode; className?: { badge?: string; status?: string; }; + onClick?: () => void; } const StatusBadge = ({ color = 'neutral', text, className, + onClick, }: StatusBadgeProps) => { return ( Date: Fri, 6 Feb 2026 09:44:06 +0700 Subject: [PATCH 30/39] chore: adjust divider styling --- src/components/helper/drawer/DrawerHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/helper/drawer/DrawerHeader.tsx b/src/components/helper/drawer/DrawerHeader.tsx index ef3ba1db..b3fe24f1 100644 --- a/src/components/helper/drawer/DrawerHeader.tsx +++ b/src/components/helper/drawer/DrawerHeader.tsx @@ -82,7 +82,7 @@ const DrawerHeader = ({ {renderLeftIcon()} {showDivider && subtitle && ( -
+
)} {subtitle && ( From ade8fefe0db2642592a2e18e6f47923ac436cbed Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 6 Feb 2026 09:44:33 +0700 Subject: [PATCH 31/39] feat: auto scroll to AlertErrorList if AlertErrorList is showing --- src/components/helper/form/FormErrors.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/helper/form/FormErrors.tsx b/src/components/helper/form/FormErrors.tsx index 60ec1687..e1de97ed 100644 --- a/src/components/helper/form/FormErrors.tsx +++ b/src/components/helper/form/FormErrors.tsx @@ -1,8 +1,10 @@ +'use client'; + import Alert from '@/components/Alert'; import Button from '@/components/Button'; import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; -import { useState } from 'react'; +import { useEffect, useRef } from 'react'; /** * Alert Unique Error List @@ -29,10 +31,22 @@ const AlertErrorList = ({ onClose: () => void; title?: string; }) => { + const alertRef = useRef(null); + + useEffect(() => { + if (formErrorList.length > 0) { + alertRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + }, [formErrorList.length]); + if (formErrorList.length === 0) return null; return (