diff --git a/src/app/finance/page.tsx b/src/app/finance/page.tsx index ec78820c..11a67181 100644 --- a/src/app/finance/page.tsx +++ b/src/app/finance/page.tsx @@ -3,12 +3,7 @@ import FinanceTable from '@/components/pages/finance/FinanceTable'; const Finance = () => { - return ( -
-
- -
- ); + return ; }; export default Finance; diff --git a/src/app/inventory/adjustment/page.tsx b/src/app/inventory/adjustment/page.tsx index 518fd0bf..44782aa7 100644 --- a/src/app/inventory/adjustment/page.tsx +++ b/src/app/inventory/adjustment/page.tsx @@ -2,7 +2,7 @@ import InventoryAdjustmentTable from '@/components/pages/inventory/adjustment/In const InventoryAdjustment = () => { return ( -
+
); diff --git a/src/app/master-data/area/page.tsx b/src/app/master-data/area/page.tsx index f8789af2..2c3cef14 100644 --- a/src/app/master-data/area/page.tsx +++ b/src/app/master-data/area/page.tsx @@ -1,11 +1,7 @@ import AreasTable from '@/components/pages/master-data/area/AreasTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/bank/page.tsx b/src/app/master-data/bank/page.tsx index 3f913c55..371cc3bf 100644 --- a/src/app/master-data/bank/page.tsx +++ b/src/app/master-data/bank/page.tsx @@ -1,11 +1,7 @@ import BanksTable from '@/components/pages/master-data/bank/BanksTable'; const Bank = () => { - return ( -
- -
- ); + return ; }; export default Bank; diff --git a/src/app/master-data/customer/page.tsx b/src/app/master-data/customer/page.tsx index 8aec1088..05c0e1e8 100644 --- a/src/app/master-data/customer/page.tsx +++ b/src/app/master-data/customer/page.tsx @@ -1,11 +1,7 @@ import CustomersTable from '@/components/pages/master-data/customer/CustomersTable'; const Customer = () => { - return ( -
- -
- ); + return ; }; export default Customer; diff --git a/src/app/master-data/flock/page.tsx b/src/app/master-data/flock/page.tsx index 76cc32c1..418018ab 100644 --- a/src/app/master-data/flock/page.tsx +++ b/src/app/master-data/flock/page.tsx @@ -1,11 +1,7 @@ import FlockTable from '@/components/pages/master-data/flock/FlocksTable'; const Flock = () => { - return ( -
- -
- ); + return ; }; export default Flock; diff --git a/src/app/master-data/kandang/page.tsx b/src/app/master-data/kandang/page.tsx index 293eb0da..e281e82c 100644 --- a/src/app/master-data/kandang/page.tsx +++ b/src/app/master-data/kandang/page.tsx @@ -1,11 +1,7 @@ import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/location/page.tsx b/src/app/master-data/location/page.tsx index 338fdbff..af65761f 100644 --- a/src/app/master-data/location/page.tsx +++ b/src/app/master-data/location/page.tsx @@ -1,11 +1,7 @@ import LocationsTable from '@/components/pages/master-data/location/LocationsTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/nonstock/page.tsx b/src/app/master-data/nonstock/page.tsx index 0812a5e2..02ed2e1e 100644 --- a/src/app/master-data/nonstock/page.tsx +++ b/src/app/master-data/nonstock/page.tsx @@ -1,11 +1,7 @@ import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/product-category/page.tsx b/src/app/master-data/product-category/page.tsx index 78a4fda3..7c0a6656 100644 --- a/src/app/master-data/product-category/page.tsx +++ b/src/app/master-data/product-category/page.tsx @@ -1,11 +1,7 @@ import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable'; const ProductCategory = () => { - return ( -
- -
- ); + return ; }; export default ProductCategory; diff --git a/src/app/master-data/product/page.tsx b/src/app/master-data/product/page.tsx index a385d411..5a4aafa9 100644 --- a/src/app/master-data/product/page.tsx +++ b/src/app/master-data/product/page.tsx @@ -1,11 +1,7 @@ import ProductsTable from '@/components/pages/master-data/product/ProductTable'; const Product = () => { - return ( -
- -
- ); + return ; }; export default Product; diff --git a/src/app/master-data/production-standard/page.tsx b/src/app/master-data/production-standard/page.tsx index ed1107cd..17944ebe 100644 --- a/src/app/master-data/production-standard/page.tsx +++ b/src/app/master-data/production-standard/page.tsx @@ -1,11 +1,7 @@ import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable'; const ProductionStandardPage = () => { - return ( -
- -
- ); + return ; }; export default ProductionStandardPage; diff --git a/src/app/master-data/supplier/page.tsx b/src/app/master-data/supplier/page.tsx index 8000be0a..169fd071 100644 --- a/src/app/master-data/supplier/page.tsx +++ b/src/app/master-data/supplier/page.tsx @@ -1,11 +1,7 @@ import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable'; const Supplier = () => { - return ( -
- -
- ); + return ; }; export default Supplier; diff --git a/src/app/master-data/uom/page.tsx b/src/app/master-data/uom/page.tsx index 689b9d0d..b5ba52b8 100644 --- a/src/app/master-data/uom/page.tsx +++ b/src/app/master-data/uom/page.tsx @@ -1,11 +1,7 @@ import UomsTable from '@/components/pages/master-data/uom/UomsTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/warehouse/page.tsx b/src/app/master-data/warehouse/page.tsx index eb5ae416..7119283e 100644 --- a/src/app/master-data/warehouse/page.tsx +++ b/src/app/master-data/warehouse/page.tsx @@ -1,11 +1,7 @@ import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable'; const Warehouse = () => { - return ( -
- -
- ); + return ; }; export default Warehouse; diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx index 471ef648..9b986b49 100644 --- a/src/app/production/recording/page.tsx +++ b/src/app/production/recording/page.tsx @@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab const Recording = () => { return ( -
+
); diff --git a/src/components/Drawer.tsx b/src/components/Drawer.tsx index fc0af9fb..280b4b34 100644 --- a/src/components/Drawer.tsx +++ b/src/components/Drawer.tsx @@ -108,7 +108,9 @@ const Drawer = ({ if (closeOnBackdropClick) { setOpen(false); } - onBackdropClick && onBackdropClick(); + if (onBackdropClick) { + onBackdropClick(); + } }; return ( diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index caa07870..7f65534d 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -31,7 +31,11 @@ export const useModal = (isNestingModal = false) => { }, []); const toggle = useCallback(() => { - open ? closeModal() : openModal(); + if (open) { + closeModal(); + } else { + openModal(); + } }, [open, closeModal, openModal]); useEffect(() => { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index ce17f6b8..7627e53a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -26,13 +26,17 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => { const logoutClickHandler = async () => { const logoutRes = await AuthApi.logout(); - if (isResponseError(logoutRes)) { toast.error('Gagal logout! Coba lagi!'); return; } setUser(undefined); + const redirect = (logoutRes as { redirect?: string })?.redirect; + if (redirect) { + window.location.href = redirect; + return; + } router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); }; diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index cff1d167..4ea9bb4f 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -3,15 +3,51 @@ import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; import { FormikValues } from 'formik'; +import { useMemo } from 'react'; export type ButtonFilterProps = ButtonProps & { values: FormikValues; onClick: () => void; + excludeFields?: string[]; + fieldGroups?: string[][]; }; // 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200 -const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { +const ButtonFilter = ({ + values, + onClick, + excludeFields = [], + fieldGroups = [], + ...props +}: ButtonFilterProps) => { + const activeCount = useMemo(() => { + const filteredValues: FormikValues = {}; + Object.keys(values).forEach((key) => { + if (!excludeFields.includes(key)) { + filteredValues[key] = values[key]; + } + }); + + let count = getFilledFormikValuesCount(filteredValues); + + fieldGroups.forEach((group) => { + const groupFields = group.filter( + (field) => !excludeFields.includes(field) + ); + const filledGroupFields = groupFields.filter( + (field) => filteredValues[field] + ); + if ( + filledGroupFields.length === groupFields.length && + groupFields.length > 1 + ) { + count -= groupFields.length - 1; + } + }); + + return count; + }, [values, excludeFields, fieldGroups]); return ( diff --git a/src/components/input/DropFileInput.tsx b/src/components/input/DropFileInput.tsx index e146a994..c8c88a1e 100644 --- a/src/components/input/DropFileInput.tsx +++ b/src/components/input/DropFileInput.tsx @@ -134,14 +134,20 @@ const DropFileInput: React.FC = ({ {!isError && bottomLabel && (

{bottomLabel}

)} {isError && (

{errorMessage}

diff --git a/src/components/input/RadioInput.tsx b/src/components/input/RadioInput.tsx index e508e7ba..a3ae9e5b 100644 --- a/src/components/input/RadioInput.tsx +++ b/src/components/input/RadioInput.tsx @@ -144,12 +144,12 @@ export const RadioGroup = ({ {/* Label bawah */} {!isError && bottomLabel && ( -

{bottomLabel}

+

{bottomLabel}

)} {/* Pesan error */} {isError && errorMessage && ( -

{errorMessage}

+

{errorMessage}

)} diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index ef959ea7..0edb3b6d 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -488,9 +488,11 @@ const SelectInput = (props: SelectInputProps) => { /> )} - {isError &&

{errorMessage}

} + {isError && ( +

{errorMessage}

+ )} {!isError && bottomLabel && ( -

{bottomLabel}

+

{bottomLabel}

)} ); diff --git a/src/components/input/TagInput.tsx b/src/components/input/TagInput.tsx index a14b2f63..6388cffe 100644 --- a/src/components/input/TagInput.tsx +++ b/src/components/input/TagInput.tsx @@ -159,9 +159,11 @@ const TagInput: React.FC = ({ {/* Bottom label or error message */} {!isError && bottomLabel && ( -

{bottomLabel}

+

{bottomLabel}

+ )} + {isError && ( +

{errorMessage}

)} - {isError &&

{errorMessage}

} ); }; diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index dc8bd6f8..12651313 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -18,7 +18,7 @@ import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditio import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { useClosingTabStore } from '@/stores/closing/closing-tab.store'; +import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; @@ -33,7 +33,7 @@ const ClosingDetail: React.FC = ({ kandangData, }) => { const [activeTabId, setActiveTabId] = useState('sapronak'); - const tabActions = useClosingTabStore((state) => state.tabActions); + const tabActions = useTabActionsStore((state) => state.tabActions); const closingDetailTabs = useMemo(() => { const validTabs = [ diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 12114110..d3586e98 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -1,6 +1,8 @@ 'use client'; import { ChangeEventHandler, useEffect, useState, useMemo } from 'react'; +import { usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; @@ -31,6 +33,7 @@ import { ClosingFilterType, } from '@/components/pages/closing/filter/ClosingFilter'; import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton'; +import ButtonFilter from '@/components/helper/ButtonFilter'; const RowOptionsMenu = ({ props, @@ -90,6 +93,9 @@ const RowOptionsMenu = ({ }; const ClosingsTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + // ===== ROUTER ===== const router = useRouter(); @@ -158,6 +164,7 @@ const ClosingsTable = () => { onReset: () => { updateFilter('location_id', ''); updateFilter('project_status', ''); + filterModal.closeModal(); }, }); @@ -287,25 +294,17 @@ const ClosingsTable = () => { ); }, [formik.values.project_status, projectStatusOptions]); - // ===== ACTIVE FILTERS COUNT ===== - const activeFiltersCount = useMemo(() => { - let count = 0; + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); - if (tableFilterState.location_id) { - count += 1; - } - - if (tableFilterState.project_status) { - count += 1; - } - - return count; - }, [tableFilterState.location_id, tableFilterState.project_status]); - - const hasFilters = activeFiltersCount > 0; + useEffect(() => { + setTableState('closing-table', pathname); + }, [pathname, setTableState]); // ===== SEARCH CHANGE HANDLER ===== const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; @@ -352,25 +351,12 @@ const ClosingsTable = () => { }} /> - + className='px-3 py-2.5' + /> @@ -379,19 +365,19 @@ const ClosingsTable = () => { ) : data.length === 0 ? ( - - } - title='Data Closing Belum Tersedia' - subtitle='Tidak ada data closing untuk saat ini.' - /> +
+ + } + /> +
) : ( data={isResponseSuccess(closings) ? closings?.data : []} @@ -410,10 +396,7 @@ const ClosingsTable = () => { rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn('mt-3', { - 'w-full mb-0': - isResponseSuccess(closings) && closings?.data?.length === 0, - }), + containerClassName: cn('mt-3 mb-0'), headerColumnClassName: 'text-nowrap', }} /> diff --git a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx index 4b59510a..c99a3194 100644 --- a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx +++ b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx @@ -6,13 +6,13 @@ import { ColumnDef } from '@tanstack/react-table'; const ClosingTableSkeleton = ({ columns, icon, - title, - subtitle, + title = 'No Data Available', + subtitle = 'There is no closing data displayed. Enter closing data to get started.', }: { columns: ColumnDef[]; icon: React.ReactNode; - title: string; - subtitle: string; + title?: string; + subtitle?: string; }) => { return (
diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index 5105d965..e362f1e0 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -308,7 +308,16 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { // }, // }, ], - [] + [ + summary, + totals.avgActualPrice, + totals.avgSalesPrice, + totals.avgWeight, + totals.totalActualPrice, + totals.totalQuantity, + totals.totalSalesPrice, + totals.totalWeight, + ] ); return ( diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index 674f3719..27db0039 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -150,33 +150,39 @@ const DashboardProduction = () => { }, }); + const { resetForm } = formik; + const handleResetFilter = useCallback(() => { - formik.resetForm(); + resetForm(); resetFilterValues(); // Clear stored filter values setAnalysisMode('OVERVIEW'); setEndpointUrl('/dashboards'); setSelectedLocationIds([]); - }, [resetFilterValues, filterValues, selectedLocationIds]); + }, [resetForm, resetFilterValues]); - const handleApplyFilter = (values: DashboardFilter) => { - // Build query params object, only include non-empty values - const params: Record = {}; + const handleApplyFilter = useCallback( + (values: DashboardFilter) => { + // Build query params object, only include non-empty values + const params: Record = {}; - if (values.start_date) params.start_date = values.start_date; - if (values.end_date) params.end_date = values.end_date; - if (values.analysis_mode) params.analysis_mode = values.analysis_mode; - if (values.location_ids.length > 0) - params.location_ids = values.location_ids.toString(); - if (values.flock_ids.length > 0) - params.flock_ids = values.flock_ids.toString(); - if (values.kandang_ids.length > 0) - params.kandang_ids = values.kandang_ids.toString(); - if (values.comparison_type) params.comparison_type = values.comparison_type; + if (values.start_date) params.start_date = values.start_date; + if (values.end_date) params.end_date = values.end_date; + if (values.analysis_mode) params.analysis_mode = values.analysis_mode; + if (values.location_ids.length > 0) + params.location_ids = values.location_ids.toString(); + if (values.flock_ids.length > 0) + params.flock_ids = values.flock_ids.toString(); + if (values.kandang_ids.length > 0) + params.kandang_ids = values.kandang_ids.toString(); + if (values.comparison_type) + params.comparison_type = values.comparison_type; - setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`); - filterModal.closeModal(); - refreshDashboardProductionData(); - }; + setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`); + filterModal.closeModal(); + refreshDashboardProductionData(); + }, + [filterModal, refreshDashboardProductionData] + ); // ===== Load filter from store on mount ===== useEffect(() => { @@ -190,20 +196,20 @@ const DashboardProduction = () => { kandang_ids: normalizeToArray(filterValues.kandang), comparison_type: filterValues.comparisonType, }); - }, [filterValues]); + }, [filterValues, handleApplyFilter]); // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); // ===== Export PDF ===== - const handleExportPDF = async () => { + const handleExportPDF = useCallback(async () => { await generateDashboardPDF({ filterValues: formik.values, allStatsRef, allChartsRef, setExporting, }); - }; + }, [formik.values]); // ===== Register Navbar Actions ===== const openFilterModalRef = useRef(filterModal.openModal); @@ -253,7 +259,7 @@ const DashboardProduction = () => {
); - }, [formik.values, exporting, setNavbarActions]); + }, [formik.values, exporting, setNavbarActions, handleExportPDF]); // Cleanup only on unmount useEffect(() => { diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index bfb13d9a..b7e0e1c2 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -409,14 +409,14 @@ const DashboardLineChart = ({ axisLine={{ stroke: '#C1C1C180', opacity: 0.5 }} domain={(() => { // Calculate dynamic domain based on visible data - let seriesData: DashboardChartsSeries[] = []; + // let seriesData: DashboardChartsSeries[] = []; let dataset: DashboardChartsDataset[] = []; if ( analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts) ) { - seriesData = data.charts[chartData]?.series || []; + // seriesData = data.charts[chartData]?.series || []; dataset = data.charts[chartData]?.dataset || []; } else if ( analysisMode === 'COMPARISON' && @@ -426,7 +426,7 @@ const DashboardLineChart = ({ data.charts.farm || data.charts.flock || data.charts.kandang; - seriesData = comparisonChart?.series || []; + // seriesData = comparisonChart?.series || []; dataset = comparisonChart?.dataset || []; } diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index e141ad67..d9118fdf 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -1,6 +1,8 @@ 'use client'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; +import { usePathname } from 'next/navigation'; +import { useUiStore } from '@/stores/ui/ui.store'; import useSWR from 'swr'; import { CellContext, @@ -16,41 +18,40 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import DateInput from '@/components/input/DateInput'; import RequirePermission from '@/components/helper/RequirePermission'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; +import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { LocationApi, SupplierApi } from '@/services/api/master-data'; -import { Location } from '@/types/api/master-data/location'; -import { Supplier } from '@/types/api/master-data/supplier'; import { BaseApiResponse } from '@/types/api/api-general'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; - approveClickHandler: () => void; - rejectClickHandler: () => void; deleteClickHandler: () => void; }) => { + const popoverId = `expense#${props.row.original.id}`; + const popoverAnchorName = `--anchor-expense#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + const showEditButton = props.row.original.latest_approval ? props.row.original.latest_approval.step_number !== 6 && (props.row.original.latest_approval.step_number === 1 || @@ -59,85 +60,102 @@ const RowOptionsMenu = ({ props.row.original.latest_approval.step_number === 4) : false; - // TODO: apply RBAC const showRealizationButton = props.row.original.latest_approval ? props.row.original.latest_approval.action !== 'REJECTED' && props.row.original.latest_approval.step_number === 4 : false; return ( - -
- - - +
+ + + - {showEditButton && ( - + +
+ - )} - {showRealizationButton && ( - + {showEditButton && ( + + + + )} + + {showRealizationButton && ( + + + + )} + + - )} - - - - -
- +
+ +
); }; const ExpensesTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -179,6 +197,9 @@ const ExpensesTable = () => { const approveModal = useModal(); const rejectModal = useModal(); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + const [selectedExpense, setSelectedExpense] = useState( undefined ); @@ -340,31 +361,7 @@ const ExpensesTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; - - const approveClickHandler = () => { - setSelectedExpense(props.row.original); - - // Set row selection - setRowSelection({ - [String(props.row.original.id)]: true, - }); - - setApprovalNotes(''); - approveModal.openModal(); - }; - - const rejectClickHandler = () => { - setSelectedExpense(props.row.original); - - // Set row selection - setRowSelection({ - [String(props.row.original.id)]: true, - }); - - setApprovalNotes(''); - rejectModal.openModal(); - }; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const deleteClickHandler = () => { setSelectedExpense(props.row.original); @@ -372,31 +369,11 @@ const ExpensesTable = () => { }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, @@ -535,51 +512,41 @@ const ExpensesTable = () => { setIsRejectLoading(false); }; - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - } = useSelect(LocationApi.basePath, 'id', 'name'); + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); - const [selectedLocation, setSelectedLocation] = useState( - null - ); - - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'locationId', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const { - setInputValue: setVendorInputValue, - options: vendorOptions, - isLoadingOptions: isLoadingVendorOptions, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const [selectedVendor, setSelectedVendor] = useState(null); - - const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedVendor(val as OptionType); - updateFilter('vendorId', val ? ((val as OptionType).value as string) : ''); - }; + useEffect(() => { + setTableState('expense-table', pathname); + }, [pathname, setTableState]); const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); updateFilter('search', e.target.value); }; - const transactionDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('transactionDate', e.target.value); + // ===== FILTER MODAL HANDLERS ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); }; - const realizationDateChangeHandler: ChangeEventHandler = ( - e - ) => { - updateFilter('realizationDate', e.target.value); + const handleFilterSubmit = (values: { + transaction_date?: string | null; + realization_date?: string | null; + location_id?: string | null; + vendor_id?: string | null; + }) => { + updateFilter('transactionDate', values.transaction_date || ''); + updateFilter('realizationDate', values.realization_date || ''); + updateFilter('locationId', values.location_id || ''); + updateFilter('vendorId', values.vendor_id || ''); + }; + + const handleFilterReset = () => { + updateFilter('transactionDate', ''); + updateFilter('realizationDate', ''); + updateFilter('locationId', ''); + updateFilter('vendorId', ''); }; // track sorting @@ -595,188 +562,193 @@ const ExpensesTable = () => { return ( <> -
-
-
-
-
- +
+
+ {/* Action Buttons */} +
+ + + + + {selectedRowIds.length > 0 && ( + <> +
+ + - {selectedRowIds.length > 0 && ( - <> - - - + + + - - - + + + - - - + + + + + )} +
- - - - - )} -
-
+ {/* Search and Filter */} +
+ + } + 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', + }} + /> -
- - - - - - - - - -
+
- - data={isResponseSuccess(expenses) ? expenses?.data : []} - columns={expensesColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} - totalItems={ - isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={tableEnableRowSelectionHandler} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(expenses) && expenses?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(expenses) || expenses.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(expenses) ? expenses?.data : []} + columns={expensesColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} + totalItems={ + isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
{ onClick: confirmationModalRejectClickHandler, }} /> + + ); }; diff --git a/src/components/pages/expense/filter/ExpensesFilter.ts b/src/components/pages/expense/filter/ExpensesFilter.ts new file mode 100644 index 00000000..8ee14a90 --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilter.ts @@ -0,0 +1,28 @@ +import * as yup from 'yup'; + +export type ExpensesFilterType = { + transaction_date: string | null; + realization_date: string | null; + location_id: string | null; + vendor_id: string | null; +}; + +export const ExpensesFilterSchema = yup.object({ + transaction_date: yup.string().nullable(), + realization_date: yup + .string() + .nullable() + .test( + 'is-greater-or-equal-transaction', + 'Tanggal realisasi tidak boleh sebelum tanggal transaksi', + function (value) { + const { transaction_date } = this.parent; + if (!transaction_date || !value) return true; + return new Date(value) >= new Date(transaction_date); + } + ), + location_id: yup.string().nullable(), + vendor_id: yup.string().nullable(), +}); + +export type ExpensesFilterValues = yup.InferType; diff --git a/src/components/pages/expense/filter/ExpensesFilterModal.tsx b/src/components/pages/expense/filter/ExpensesFilterModal.tsx new file mode 100644 index 00000000..1885785f --- /dev/null +++ b/src/components/pages/expense/filter/ExpensesFilterModal.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { RefObject } from 'react'; +import { useFormik } from 'formik'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInput from '@/components/input/SelectInput'; + +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { LocationApi, SupplierApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { + ExpensesFilterSchema, + ExpensesFilterValues, +} from '@/components/pages/expense/filter/ExpensesFilter'; + +interface ExpensesFilterModalProps { + ref: RefObject; + initialValues?: ExpensesFilterValues; + onSubmit?: (values: Partial) => void; + onReset?: () => void; +} + +const ExpensesFilterModal = ({ + ref, + initialValues, + onSubmit, + onReset, +}: ExpensesFilterModalProps) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setVendorInputValue, + options: vendorOptions, + isLoadingOptions: isLoadingVendorOptions, + } = useSelect(SupplierApi.basePath, 'id', 'name'); + + const formik = useFormik({ + initialValues: initialValues || { + transaction_date: null, + realization_date: null, + location_id: null, + vendor_id: null, + }, + validationSchema: ExpensesFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const locationValue = formik.values.location_id + ? locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + : null; + + const vendorValue = formik.values.vendor_id + ? vendorOptions.find( + (opt) => String(opt.value) === formik.values.vendor_id + ) || null + : null; + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const locationId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('location_id', locationId); + }; + + const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { + const vendorId = + val && !Array.isArray(val) ? (String(val.value) as string) : null; + formik.setFieldValue('vendor_id', vendorId); + }; + + return ( + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+
+ Tanggal +
+ +
+ +
+ {formik.touched.realization_date && + formik.errors.realization_date && ( + + {formik.errors.realization_date} + + )} +
+ + + + +
+ + {/* Modal Footer */} +
+ + + +
+
+
+ ); +}; + +export default ExpensesFilterModal; diff --git a/src/components/pages/expense/form/ExpenseKandangsTable.tsx b/src/components/pages/expense/form/ExpenseKandangsTable.tsx index 5c60ae1e..bc779417 100644 --- a/src/components/pages/expense/form/ExpenseKandangsTable.tsx +++ b/src/components/pages/expense/form/ExpenseKandangsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; @@ -75,6 +75,12 @@ const ExpenseKandangsTable = ({ .filter((id): id is number => id !== undefined) ) ); + const rowSelectionRef = useRef(rowSelection); + const prevRowSelectionRef = useRef>({}); + + useEffect(() => { + rowSelectionRef.current = rowSelection; + }, [rowSelection]); const kandangsColumns: ColumnDef[] = [ { @@ -133,33 +139,43 @@ const ExpenseKandangsTable = ({ useEffect(() => { setOpen(isResponseSuccess(kandangs) ? kandangs.data.length > 0 : false); - }, [kandangs, isResponseSuccess]); + }, [kandangs]); useEffect(() => { - if (Object.keys(rowSelection).length !== 0 && isResponseSuccess(kandangs)) { - const formattedSelectedKandangs = Object.keys(rowSelection).map( - (item) => { - const selectedKandang = kandangs.data.find( - (kandang) => kandang.id === parseInt(item) - ); + const currentKeys = Object.keys(rowSelection).sort().join(','); + const prevKeys = Object.keys(prevRowSelectionRef.current).sort().join(','); - return { - id: parseInt(item), - name: selectedKandang?.name ?? 'Kandang tidak ditemukan!', - }; - } - ); + if (currentKeys !== prevKeys) { + prevRowSelectionRef.current = { ...rowSelection }; - onChange(formattedSelectedKandangs); - } else { - onChange([]); + if ( + Object.keys(rowSelection).length !== 0 && + isResponseSuccess(kandangs) + ) { + const formattedSelectedKandangs = Object.keys(rowSelection).map( + (item) => { + const selectedKandang = kandangs.data.find( + (kandang) => kandang.id === parseInt(item) + ); + + return { + id: parseInt(item), + name: selectedKandang?.name ?? 'Kandang tidak ditemukan!', + }; + } + ); + + onChange(formattedSelectedKandangs); + } else if (Object.keys(rowSelection).length === 0) { + onChange([]); + } } - }, [rowSelection]); + }, [rowSelection, kandangs, onChange]); useEffect(() => { if ( selectedKandangs.length === 0 && - Object.keys(rowSelection).length !== 0 + Object.keys(rowSelectionRef.current).length !== 0 ) { setRowSelection({}); } diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx index acc0a393..e9720d0b 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; @@ -90,6 +90,7 @@ const ExpenseRealizationForm = ({ const formik = useFormik({ initialValues: getExpenseRealizationFormInitialValues(initialValues), + enableReinitialize: true, validationSchema: type === 'edit' ? UpdateExpenseRealizationFormSchema @@ -143,7 +144,6 @@ const ExpenseRealizationForm = ({ }, }); - const { setValues: formikSetValues } = formik; const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); const { @@ -254,10 +254,6 @@ const ExpenseRealizationForm = ({ formik.setFieldValue('documents', newRequestDocuments); }; - useEffect(() => { - formikSetValues(getExpenseRealizationFormInitialValues(initialValues)); - }, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]); - return (
diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index 733204d9..adc825c2 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; @@ -102,6 +102,7 @@ const ExpenseRequestForm = ({ const formik = useFormik({ initialValues: getExpenseFormInitialValues(initialValues), + enableReinitialize: true, validationSchema: type === 'edit' ? UpdateExpenseRequestFormSchema @@ -171,7 +172,7 @@ const ExpenseRequestForm = ({ }, }); - const { setValues: formikSetValues } = formik; + const { setFieldValue, setFieldTouched } = formik; const { setInputValue: setLocationInputValue, @@ -186,8 +187,8 @@ const ExpenseRequestForm = ({ } = useSelect(SupplierApi.basePath, 'id', 'name'); const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldTouched('category', true); - formik.setFieldValue('category', val); + setFieldTouched('category', true); + setFieldValue('category', val); }; const locationChangeHandler = useCallback( @@ -195,12 +196,12 @@ const ExpenseRequestForm = ({ const location = val as OptionType | null; const locationId = location ? Number(location.value) : 0; - formik.setFieldTouched('location', true); - formik.setFieldValue('location', location); - formik.setFieldTouched('location_id', true); - formik.setFieldValue('location_id', locationId); + setFieldTouched('location', true); + setFieldValue('location', location); + setFieldTouched('location_id', true); + setFieldValue('location_id', locationId); }, - [] + [setFieldTouched, setFieldValue] ); const kandangsChangeHandler = ( @@ -343,10 +344,6 @@ const ExpenseRequestForm = ({ formik.handleSubmit(e); }; - useEffect(() => { - formikSetValues(getExpenseFormInitialValues(initialValues)); - }, [formikSetValues, getExpenseFormInitialValues, initialValues]); - return ( <>
diff --git a/src/components/pages/expense/skeleton/ExpenseTableSkeleton.tsx b/src/components/pages/expense/skeleton/ExpenseTableSkeleton.tsx new file mode 100644 index 00000000..f159a81a --- /dev/null +++ b/src/components/pages/expense/skeleton/ExpenseTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Expense } from '@/types/api/expense'; +import { ColumnDef } from '@tanstack/react-table'; + +const ExpenseTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no expense data displayed. Enter expense data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+ +
+ +
+ + ); +}; + +export default ExpenseTableSkeleton; diff --git a/src/components/pages/finance/FinanceDetail.tsx b/src/components/pages/finance/FinanceDetail.tsx index 622fff6f..0887e029 100644 --- a/src/components/pages/finance/FinanceDetail.tsx +++ b/src/components/pages/finance/FinanceDetail.tsx @@ -2,7 +2,6 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import { FormHeader } from '@/components/helper/form/FormHeader'; import RequirePermission from '@/components/helper/RequirePermission'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import Table from '@/components/Table'; @@ -26,11 +25,13 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { const informasiUmum = [ { label: 'ID', - value: finance.payment_code, + value: finance.payment_code || '-', }, { label: 'Jenis Transaksi', - value: formatTitleCase(finance.transaction_type.split('_').join(' ')), + value: formatTitleCase( + (finance.transaction_type || '').split('_').join(' ') + ), }, { label: 'Pihak', @@ -38,11 +39,13 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Tanggal', - value: formatDate(finance.payment_date, 'DD MMM yyyy'), + value: finance.payment_date + ? formatDate(finance.payment_date, 'DD MMM yyyy') + : '-', }, { label: 'Metode Pembayaran', - value: finance.payment_method, + value: finance.payment_method || '-', }, { label: 'Catatan', @@ -61,22 +64,22 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { : '-', }, { - label: `Rekening ${formatTitleCase(finance.party?.type)}`, - value: finance.party?.account_number, + label: `Rekening ${formatTitleCase(finance.party?.type || '')}`, + value: finance.party?.account_number || '-', }, { label: 'Nominal', value: formatCurrency( finance.transaction_type === 'INJECTION' - ? finance.nominal - : Math.abs(finance.nominal) + ? finance.nominal || 0 + : Math.abs(finance.nominal || 0) ), }, ].filter((item) => { // Hide party account number row if transaction type is INJECTION if ( - FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && - item.label === `Rekening ${formatTitleCase(finance.party?.type)}` + FINANCE_INJECTION_STATUS.includes(finance.transaction_type || '') && + item.label === `Rekening ${formatTitleCase(finance.party?.type || '')}` ) { return false; } @@ -150,7 +153,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
- {FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && + {FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type || '') && finance.party?.type !== 'SUPPLIER' && ( )} - {FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && ( + {FINANCE_INITIAL_BALANCE_STATUS.includes( + finance.transaction_type || '' + ) && ( )} - {FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && ( + {FINANCE_INJECTION_STATUS.includes(finance.transaction_type || '') && ( - + + - {FINANCE_TRANSACTION_STATUS.includes( - props.row.original.transaction_type - ) && ( - - - - )} + + - {FINANCE_INITIAL_BALANCE_STATUS.includes( - props.row.original.transaction_type - ) && ( - - - - )} + {FINANCE_TRANSACTION_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} - {FINANCE_INJECTION_STATUS.includes( - props.row.original.transaction_type - ) && ( - - - - )} + {FINANCE_INITIAL_BALANCE_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} - - - - + {FINANCE_INJECTION_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} + + + + +
+ + ); }; @@ -171,6 +209,9 @@ const FinanceTable = () => { }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + // ===== State ===== const deleteModal = useModal(); const [selectedTransactionType, setSelectedTransactionType] = useState< @@ -189,6 +230,7 @@ const FinanceTable = () => { const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ @@ -214,6 +256,18 @@ const FinanceTable = () => { updateFilter('sortBy', values.sort_by); updateFilter('startDate', values.start_date); updateFilter('endDate', values.end_date); + filterModal.closeModal(); + }, + onReset: () => { + updateFilter('search', ''); + resetSearchValue(); + updateFilter('transactionTypes', ''); + updateFilter('bankIds', ''); + updateFilter('customerIds', ''); + updateFilter('supplierIds', ''); + updateFilter('sortBy', ''); + updateFilter('startDate', ''); + updateFilter('endDate', ''); }, }); @@ -266,10 +320,41 @@ const FinanceTable = () => { }); }, [bankOptions, bankRawData]); + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (tableFilterState.transactionTypes) count += 1; + if (tableFilterState.bankIds) count += 1; + if (tableFilterState.customerIds) count += 1; + if (tableFilterState.supplierIds) count += 1; + if (tableFilterState.sortBy) count += 1; + if (tableFilterState.startDate) count += 1; + if (tableFilterState.endDate) count += 1; + + return count; + }, [ + tableFilterState.transactionTypes, + tableFilterState.bankIds, + tableFilterState.customerIds, + tableFilterState.supplierIds, + tableFilterState.sortBy, + tableFilterState.startDate, + tableFilterState.endDate, + ]); + + const hasFilters = activeFiltersCount > 0; + // ===== Handler ===== - const searchChangeHandler = (e: React.ChangeEvent) => { - filterFormik.setFieldValue('search', e.target.value); - }; + const searchChangeHandler = useCallback( + (e: React.ChangeEvent) => { + updateFilter('search', e.target.value); + setSearchValue(e.target.value); + setPage(1); + }, + [updateFilter, setSearchValue, setPage] + ); + const transactionTypeChangeHandler = ( val: OptionType | OptionType[] | null ) => { @@ -335,10 +420,7 @@ const FinanceTable = () => { const endDateObj = new Date(endDate); if (endDateObj < startDate) { - filterFormik.setFieldError( - 'end_date', - 'Tanggal akhir tidak boleh masa lampau' - ); + setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, @@ -346,12 +428,14 @@ const FinanceTable = () => { setDateErrorShown(true); } } else { - filterFormik.setFieldError('end_date', undefined); + setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } } + } else { + setHasDateError(false); } }; @@ -366,10 +450,7 @@ const FinanceTable = () => { const endDate = new Date(value); if (endDate < startDateObj) { - filterFormik.setFieldError( - 'end_date', - 'Tanggal akhir tidak boleh masa lampau' - ); + setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, @@ -380,13 +461,18 @@ const FinanceTable = () => { } } - filterFormik.setFieldError('end_date', undefined); + setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } }; + const handleFilterModalOpen = () => { + filterModal.openModal(); + filterFormik.validateForm(); + }; + const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -406,6 +492,7 @@ const FinanceTable = () => { updateFilter('startDate', ''); updateFilter('endDate', ''); }; + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -417,8 +504,8 @@ const FinanceTable = () => { setIsDeleteLoading(false); }; - const columns = useMemo(() => { - return [ + const columns: ColumnDef[] = useMemo( + () => [ { header: 'ID', accessorKey: 'payment_code', @@ -498,32 +585,17 @@ const FinanceTable = () => { }; return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - + ); }, }, - ]; - }, []); + ], + [deleteModal] + ); useEffect(() => { return () => { @@ -555,151 +627,280 @@ const FinanceTable = () => { }, [resetSearchValue, dateErrorShown]); return ( -
-
- - - - - - - - - -
- + <> +
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + + + + + + + +
+ + {/* Search and Filter */} +
+ + } + 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', + }} + /> + -
- } - > -
- - - - - - - -
- - - data={isResponseSuccess(finances) ? finances.data : []} - columns={columns} - pageSize={tableFilterState.pageSize} - page={tableFilterState.page} - onPageChange={setPage} - onPageSizeChange={setPageSize} - totalItems={ - isResponseSuccess(finances) ? finances.meta?.total_results : 0 - } - isLoading={isLoading} - /> + + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(finances) || finances.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(finances) ? finances.data : []} + columns={columns} + pageSize={tableFilterState.pageSize} + page={tableFilterState.page} + totalItems={ + isResponseSuccess(finances) ? finances.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+
+ Tanggal +
+ +
+ +
+
+ + {}} + closeMenuOnSelect={false} + isClearable + isMulti + className={{ wrapper: 'w-full' }} + /> + + + + +
+ + {/* Modal Footer */} +
+ + +
+ +
+ { onClick: confirmationModalDeleteClickHandler, }} /> -
+ ); }; diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/filter/FinanceFilter.ts similarity index 100% rename from src/components/pages/finance/FinanceTableFilter.schema.ts rename to src/components/pages/finance/filter/FinanceFilter.ts diff --git a/src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx b/src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx new file mode 100644 index 00000000..ccfbf1f5 --- /dev/null +++ b/src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Finance } from '@/types/api/finance/finance'; +import { ColumnDef } from '@tanstack/react-table'; + +const FinanceTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no finance data displayed. Enter finance data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default FinanceTableSkeleton; diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index 1bd47caf..90b68b7d 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -1,22 +1,48 @@ 'use client'; -import Badge from '@/components/Badge'; -import Button from '@/components/Button'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; -import Table from '@/components/Table'; -import RequirePermission from '@/components/helper/RequirePermission'; -import { ROWS_OPTIONS } from '@/config/constant'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { cn } from '@/lib/helper'; -import { InventoryAdjustmentApi } from '@/services/api/inventory'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; +import { + ChangeEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { usePathname } from 'next/navigation'; +import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table'; -import { useCallback, useEffect, useState } from 'react'; -import useSWR from 'swr'; +import { useFormik } from 'formik'; +import Button from '@/components/Button'; +import Table from '@/components/Table'; +import RequirePermission from '@/components/helper/RequirePermission'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import { OptionType } from '@/components/input/SelectInput'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import Modal, { useModal } from '@/components/Modal'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper'; +import { InventoryAdjustmentApi } from '@/services/api/inventory'; +import { WarehouseApi, ProductApi } from '@/services/api/master-data'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { useUiStore } from '@/stores/ui/ui.store'; +import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant'; +import { Product } from '@/types/api/master-data/product'; +import StatusBadge from '@/components/helper/StatusBadge'; +import InventoryAdjustmentTableSkeleton from '@/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton'; + +import { + AdjustmentFilterSchema, + AdjustmentFilterType, +} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const InventoryAdjustmentTable = () => { + const { searchValue, setSearchValue, setTableState } = useUiStore(); + const pathname = usePathname(); + const { state: tableFilterState, updateFilter, @@ -30,6 +56,9 @@ const InventoryAdjustmentTable = () => { productSort: '', warehouseSort: '', stockSort: '', + productFilter: '', + warehouseFilter: '', + transactionTypeFilter: '', }, paramMap: { page: 'page', @@ -38,84 +67,257 @@ const InventoryAdjustmentTable = () => { productSort: 'sort_product', warehouseSort: 'sort_warehouse', stockSort: 'sort_stock', + productFilter: 'product_id', + warehouseFilter: 'warehouse_id', + transactionTypeFilter: 'transaction_type', }, }); - // Fetch Data + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + product_id: null, + warehouse_id: null, + transaction_type: null, + }, + validationSchema: AdjustmentFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('productFilter', values.product_id || ''); + updateFilter('warehouseFilter', values.warehouse_id || ''); + updateFilter('transactionTypeFilter', values.transaction_type || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('productFilter', ''); + updateFilter('warehouseFilter', ''); + updateFilter('transactionTypeFilter', ''); + }, + }); + + // ===== PRODUCT OPTIONS ===== + const { + setInputValue: setProductInputValue, + options: productOptions, + isLoadingOptions: isLoadingProductOptions, + loadMore: loadMoreProducts, + } = useSelect( + filterModal.open ? ProductApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== WAREHOUSE OPTIONS ===== + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + loadMore: loadMoreWarehouses, + } = useSelect( + filterModal.open ? WarehouseApi.basePath : null, + 'id', + 'name', + 'search' + ); + + // ===== TRANSACTION TYPE OPTIONS ===== + const transactionTypeOptions = useMemo(() => { + return [ + { value: 'increase', label: 'Increase' }, + { value: 'decrease', label: 'Decrease' }, + ]; + }, []); + + // ===== FILTER HANDLERS ===== + const handleFilterProductChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const product = val as OptionType | null; + const productId = product?.value ? String(product.value) : null; + formik.setFieldValue('product_id', productId); + }, + [formik] + ); + + const handleFilterWarehouseChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const warehouse = val as OptionType | null; + const warehouseId = warehouse?.value ? String(warehouse.value) : null; + formik.setFieldValue('warehouse_id', warehouseId); + }, + [formik] + ); + + const handleFilterTransactionTypeChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const type = val as OptionType | null; + const typeValue = type?.value ? String(type.value) : null; + formik.setFieldValue('transaction_type', typeValue); + }, + [formik] + ); + + // ===== FILTER HELPERS ===== + const productIdValue = useMemo(() => { + if (!formik.values.product_id) return null; + return ( + productOptions.find( + (opt) => String(opt.value) === formik.values.product_id + ) || null + ); + }, [formik.values.product_id, productOptions]); + + const warehouseIdValue = useMemo(() => { + if (!formik.values.warehouse_id) return null; + return ( + warehouseOptions.find( + (opt) => String(opt.value) === formik.values.warehouse_id + ) || null + ); + }, [formik.values.warehouse_id, warehouseOptions]); + + const transactionTypeValue = useMemo(() => { + if (!formik.values.transaction_type) return null; + return ( + transactionTypeOptions.find( + (opt) => String(opt.value) === formik.values.transaction_type + ) || null + ); + }, [formik.values.transaction_type, transactionTypeOptions]); + + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + const { data: inventoryAdjustments, isLoading } = useSWR( `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, InventoryAdjustmentApi.getAllFetcher ); - // State const [sorting, setSorting] = useState([]); - // Columns - const inventoryAdjustmentsColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - id: 'product_name', - header: 'Nama Produk', - accessorFn: (row) => row.product_warehouse?.product?.name ?? '-', - }, - { - id: 'warehouse_name', - header: 'Gudang', - accessorFn: (row) => row.product_warehouse?.warehouse?.name ?? '-', - }, - { - id: 'created_at', - header: 'Tanggal', - accessorFn: (row) => - new Date(row.created_at).toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - }), - }, - { - id: 'quantity', - header: 'Kuantitas', - accessorFn: (row) => formatNumber(String(row.increase + row.decrease)), - }, - { - id: 'transaction_type', - header: 'Tipe Transaksi', - accessorFn: (row) => { - if (row.increase > 0) return 'Peningkatan'; - if (row.decrease > 0) return 'Penurunan'; - return '-'; - }, - cell: (props) => { - const type = props.row.original.increase; - const label = type > 0 ? 'Peningkatan' : type <= 0 ? 'Penurunan' : '-'; + useEffect(() => { + updateFilter('search', searchValue); + }, [searchValue, updateFilter]); - return ( - 0 ? 'success' : 'error'}> - {label} - - ); - }, - }, - { - id: 'created_by', - header: 'Oleh', - accessorFn: (row) => row.created_user?.name ?? '-', - }, - ]; + useEffect(() => { + setTableState('inventory-adjustment-table', pathname); + }, [pathname, setTableState]); - // Handler - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); + updateFilter('search', e.target.value); }; + const inventoryAdjustmentsColumns: ColumnDef[] = useMemo( + () => [ + { + id: 'adj_number', + header: 'No. Referensi', + accessorFn: (row) => row.adj_number ?? '-', + }, + { + id: 'location', + header: 'Lokasi', + accessorFn: (row) => row.location?.name ?? '-', + }, + { + id: 'project_flock', + header: 'Flock', + accessorFn: (row) => row.project_flock?.flock_name ?? '-', + }, + { + id: 'warehouse_name', + header: 'Gudang', + accessorFn: (row) => row.product_warehouse?.warehouse?.name ?? '-', + }, + { + id: 'product_name', + header: 'Nama Produk', + accessorFn: (row) => row.product_warehouse?.product?.name ?? '-', + }, + { + id: 'quantity', + header: 'Kuantitas', + accessorFn: (row) => row.qty ?? '-', + cell: (row) => { + const value = row.row.original.increase + row.row.original.decrease; + return
{formatNumber(value)}
; + }, + }, + { + id: 'price', + header: 'Harga', + accessorFn: (row) => (row.price ? formatCurrency(row.price) : '-'), + }, + { + id: 'grand_total', + header: 'Grand Total', + accessorFn: (row) => + row.grand_total ? formatCurrency(row.grand_total) : '-', + }, + { + id: 'transaction_type', + header: 'Tipe Transaksi', + accessorFn: (row) => row.transaction_subtype ?? '-', + cell: (row) => { + const subtype = row.row.original.transaction_subtype; + const increase = row.row.original.increase; + + const getSubtypeLabel = (subtypeValue: string): string => { + if (subtypeValue === TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.value) { + return TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.label; + } + if (subtypeValue === TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.value) { + return TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.label; + } + const recordingOption = TRANSACTION_SUBTYPE_OPTIONS.RECORDING.find( + (opt) => opt.value === subtypeValue + ); + if (recordingOption) { + return recordingOption.label; + } + if (subtypeValue === 'RECORDING_DEPLETION_OUT') { + return 'Recording Depletion'; + } + return subtypeValue || '-'; + }; + + const label = getSubtypeLabel(subtype); + + return ( + 0 ? 'success' : increase <= 0 ? 'error' : 'neutral' + } + text={label} + className={{ + badge: 'whitespace-nowrap', + }} + /> + ); + }, + }, + { + id: 'created_at', + header: 'Tanggal', + accessorFn: (row) => + row.created_at ? formatDate(row.created_at, 'DD MMM YYYY') : '-', + }, + { + id: 'created_by', + header: 'Oleh', + accessorFn: (row) => row.created_user?.name ?? '-', + }, + ], + [tableFilterState.pageSize, tableFilterState.page] + ); + const updateSortingFilter = useCallback( ( sortName: Exclude, @@ -130,7 +332,6 @@ const InventoryAdjustmentTable = () => { [updateFilter] ); - // Effect useEffect(() => { const productCategorySortFilter = sorting.find( (sortItem) => sortItem.id === 'productCategory' @@ -149,88 +350,200 @@ const InventoryAdjustmentTable = () => { updateSortingFilter('stockSort', stockSortFilter); }, [sorting, updateSortingFilter]); - // Utils Function - const formatNumber = (value: string) => { - const numericValue = value.replace(/[^0-9.]/g, ''); - const [integer, decimal] = numericValue.split('.'); - const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return decimal ? `${formattedInteger}.${decimal}` : formattedInteger; - }; - - // Render return ( <> -
-
-
-
- - - -
- -
- -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + +
- - data={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.data - : [] - } - columns={inventoryAdjustmentsColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(inventoryAdjustments) && - inventoryAdjustments?.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', - }} - /> + {/* Search and Filter */} +
+ + } + 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', + }} + /> + + +
+
+ + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(inventoryAdjustments) || + inventoryAdjustments.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.data + : [] + } + columns={inventoryAdjustmentsColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ + + +
+ + {/* Modal Footer */} +
+ + +
+ +
); }; diff --git a/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts new file mode 100644 index 00000000..4568618f --- /dev/null +++ b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts @@ -0,0 +1,13 @@ +import { string, object } from 'yup'; + +export const AdjustmentFilterSchema = object().shape({ + product_id: string().nullable(), + warehouse_id: string().nullable(), + transaction_type: string().nullable(), +}); + +export type AdjustmentFilterType = { + product_id: string | null; + warehouse_id: string | null; + transaction_type: string | null; +}; diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts index 42ecf48d..cee01e00 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema.ts @@ -1,55 +1,118 @@ import * as Yup from 'yup'; -import { OptionType } from '@/components/input/SelectInput'; -export const InventoryAdjustmentFormSchema = Yup.object({ - product_category: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Kategori Produk wajib diisi!', - (value) => value !== null && value !== undefined +export type InventoryAdjustmentFormSchemaType = { + location: { + value: number; + label: string; + } | null; + location_id: number; + project_flock: { + value: number; + label: string; + } | null; + project_flock_id: number; + kandang: { + value: number; + label: string; + } | null; + kandang_id: number; + project_flock_kandang: { + value: number; + label: string; + } | null; + project_flock_kandang_id: number; + product: { + value: number; + label: string; + } | null; + product_id: number; + depletion_product: { + value: number; + label: string; + } | null; + depletion_product_id: number; + transaction_type: string; + transaction_subtype: string; + qty: number | string; + price: number | string; + notes: string; +}; + +export const InventoryAdjustmentFormSchema: Yup.ObjectSchema = + Yup.object({ + location: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + location_id: Yup.number() + .min(1, 'Lokasi wajib diisi!') + .required('Lokasi wajib diisi!') + .typeError('Lokasi wajib diisi!'), + project_flock: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_id: Yup.number() + .min(1, 'Project flock wajib diisi!') + .required('Project flock wajib diisi!') + .typeError('Project flock wajib diisi!'), + kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + kandang_id: Yup.number() + .min(1, 'Kandang wajib diisi!') + .required('Kandang wajib diisi!') + .typeError('Kandang wajib diisi!'), + project_flock_kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_kandang_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!'), + product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + product_id: Yup.number() + .min(1, 'Produk wajib diisi!') + .required('Produk wajib diisi!') + .typeError('Produk wajib diisi!'), + depletion_product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + depletion_product_id: Yup.number() + .default(0) + .typeError('Jenis deplesi harus berupa angka'), + transaction_type: Yup.string() + .min(1, 'Tipe transaksi wajib diisi!') + .oneOf( + ['PEMBELIAN', 'PENJUALAN', 'RECORDING'], + 'Tipe transaksi tidak valid' + ) + .required('Tipe transaksi wajib diisi') + .typeError('Tipe transaksi wajib diisi!'), + transaction_subtype: Yup.string().required( + 'Sub tipe transaksi wajib diisi' ), - - product_category_id: Yup.number().nullable(), - - product: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Produk wajib diisi!', - (value) => value !== null && value !== undefined - ), - - product_id: Yup.number() - .nullable() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!'), - - warehouse: Yup.mixed() - .nullable() - .test( - 'is-valid-option', - 'Warehouse wajib diisi!', - (value) => value !== null && value !== undefined - ), - - warehouse_id: Yup.number() - .nullable() - .required('Warehouse wajib diisi!') - .min(1, 'Warehouse wajib diisi!'), - - transaction_type: Yup.string() - .oneOf(['increase', 'decrease'], 'Tipe transaksi tidak valid') - .nullable() - .required('Tipe transaksi wajib diisi'), - - quantity: Yup.number() - .typeError('Kuantitas harus berupa angka') - .min(1, 'Minimal kuantitas adalah 1') - .required('Kuantitas wajib diisi'), - - note: Yup.string().required('Catatan wajib diisi!'), -}); + qty: Yup.number() + .typeError('Kuantitas harus berupa angka') + .min(1, 'Minimal kuantitas adalah 1') + .required('Kuantitas wajib diisi'), + price: Yup.number() + .typeError('Harga harus berupa angka') + .min(0, 'Minimal harga adalah 0') + .required('Harga wajib diisi'), + notes: Yup.string().required('Catatan wajib diisi!'), + }); export type InventoryAdjustmentFormValues = Yup.InferType< typeof InventoryAdjustmentFormSchema diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 612fbb20..ff710329 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -1,7 +1,8 @@ 'use client'; -import { isResponseError } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { InventoryAdjustmentApi } from '@/services/api/inventory'; +import { ProductApi } from '@/services/api/master-data'; import { CreateInventoryAdjustmentPayload, InventoryAdjustment, @@ -14,28 +15,37 @@ import { InventoryAdjustmentFormSchema, InventoryAdjustmentFormValues, } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; +import { KandangApi, LocationApi } from '@/services/api/master-data'; import { - ProductApi, - ProductCategoryApi, - WarehouseApi, -} from '@/services/api/master-data'; + ProjectFlockApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; -import TextInput from '@/components/input/TextInput'; -import { RadioGroup } from '@/components/input/RadioInput'; + import TextArea from '@/components/input/TextArea'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; -import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Location } from '@/types/api/master-data/location'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { Kandang } from '@/types/api/master-data/kandang'; import { Product } from '@/types/api/master-data/product'; -import { Warehouse } from '@/types/api/master-data/warehouse'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { BaseApiResponse } from '@/types/api/api-general'; +import useSWR from 'swr'; +import { + TRANSACTION_TYPE_OPTIONS, + TRANSACTION_SUBTYPE_OPTIONS, +} from '@/config/constant'; +import NumberInput from '@/components/input/NumberInput'; interface InventoryAdjustmentFormProps { - type?: 'add' | 'edit' | 'detail'; + type?: 'add' | 'detail'; initialValues?: InventoryAdjustment; } @@ -43,16 +53,32 @@ const InventoryAdjustmentForm = ({ type = 'add', initialValues, }: InventoryAdjustmentFormProps) => { - // State const router = useRouter(); const [ InventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage, ] = useState(''); - const [disabledProduct, setDisabledProduct] = useState(true); - const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); - // Submit Handler + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const [selectedProjectFlock, setSelectedProjectFlock] = + useState(null); + const [selectedKandang, setSelectedKandang] = useState( + null + ); + const [selectedProduct, setSelectedProduct] = useState( + null + ); + const [selectedTransactionType, setSelectedTransactionType] = + useState(null); + const [selectedTransactionSubtype, setSelectedTransactionSubtype] = + useState(null); + const [selectedDepletionProduct, setSelectedDepletionProduct] = + useState(null); + const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] = + useState(''); + const createInventoryAdjustmentHandler = useCallback( async (payload: CreateInventoryAdjustmentPayload) => { const createInventoryAdjustmentRes = @@ -71,34 +97,192 @@ const InventoryAdjustmentForm = ({ [router] ); - const formikInitialValues = useMemo< - Partial - >(() => { - return { - product_id: initialValues?.product_warehouse?.product_id ?? 0, - warehouse_id: initialValues?.product_warehouse?.warehouse_id ?? 0, - product_category: undefined, - product: undefined, - warehouse: undefined, - quantity: initialValues?.increase ?? initialValues?.decrease ?? 0, - transaction_type: undefined, - note: initialValues?.note ?? '', - }; - }, [initialValues]); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedProjectFlockLocationId, + } + ); + + const { rawData: approvedProjectFlockKandangsRawData } = + useSelect( + ProjectFlockKandangApi.basePath, + 'id', + 'id', + 'search', + { + step_name: 'Disetujui', + limit: '100', + } + ); + + const approvedProjectFlockKandangs = useMemo(() => { + if ( + approvedProjectFlockKandangsRawData && + 'data' in approvedProjectFlockKandangsRawData + ) { + return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[]; + } + return []; + }, [approvedProjectFlockKandangsRawData]); + + const { + setInputValue: setKandangInputValue, + options: kandangOptionsFromApi, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandangs, + } = useSelect( + selectedProjectFlock ? KandangApi.basePath : '', + 'id', + 'name', + 'search', + { + location_id: selectedProjectFlockLocationId, + } + ); + + const projectFlockKandangLookupUrl = useMemo(() => { + if (!selectedProjectFlock || !selectedKandang) return null; + const params = new URLSearchParams({ + project_flock_id: selectedProjectFlock.value.toString(), + kandang_id: selectedKandang.value.toString(), + }); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [selectedProjectFlock, selectedKandang]); + + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise> + : null + ); + + const projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; + + const { + setInputValue: setProductInputValue, + isLoadingOptions: isLoadingProductOptions, + loadMore: loadMoreProducts, + rawData: products, + } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { + include_all: 'true', + }); + + const { + setInputValue: setDepletionProductInputValue, + options: depletionProductOptions, + isLoadingOptions: isLoadingDepletionProductOptions, + loadMore: loadMoreDepletionProducts, + } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { + is_depletion: 'true', + }); + + const productOptions = useMemo(() => { + if (!isResponseSuccess(products)) return []; + + const excludedFlags = ['AYAM-AFKIR', 'AYAM-CULLING', 'AYAM-MATI']; + const filteredProducts = products.data.filter((product) => { + const productFlags = (product.flags as string[]) || []; + return !productFlags.some((flag) => excludedFlags.includes(flag)); + }); + + return filteredProducts.map((product) => ({ + value: product.id, + label: product.name, + flags: product.flags, + })); + }, [products]); + + const selectedProductFlags = useMemo(() => { + if (!selectedProduct) return []; + const product = productOptions.find( + (opt) => opt.value === selectedProduct.value + ); + return (product?.flags as string[]) || []; + }, [selectedProduct, productOptions]); + + const kandangOptions = useMemo(() => { + let options: OptionType[] = []; + + if (selectedProjectFlock) { + const approvedKandangIds = approvedProjectFlockKandangs + .filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value) + .map((pfk) => pfk.kandang_id); + + options = kandangOptionsFromApi.filter((kandang) => + approvedKandangIds.includes(kandang.value as number) + ); + } + + return options; + }, [ + selectedProjectFlock, + kandangOptionsFromApi, + approvedProjectFlockKandangs, + ]); + + const formikInitialValues = useMemo>( + () => ({ + location: null, + location_id: 0, + project_flock: null, + project_flock_id: 0, + kandang: null, + kandang_id: 0, + project_flock_kandang: null, + project_flock_kandang_id: 0, + product: null, + product_id: 0, + depletion_product: null, + depletion_product_id: 0, + transaction_type: '', + transaction_subtype: '', + qty: '', + price: '', + notes: '', + }), + [] + ); - // Formik const formik = useFormik({ - enableReinitialize: true, + enableReinitialize: false, initialValues: formikInitialValues as InventoryAdjustmentFormValues, validationSchema: InventoryAdjustmentFormSchema, + validateOnChange: true, + validateOnBlur: true, onSubmit: async (values) => { setInventoryAdjustmentFormErrorMessage(''); const payload: CreateInventoryAdjustmentPayload = { - product_id: values.product_id as number, - warehouse_id: values.warehouse_id as number, - quantity: values.quantity as number, - transaction_type: values.transaction_type as string, - note: values.note, + project_flock_kandang_id: values.project_flock_kandang_id, + product_id: + values.depletion_product_id > 0 + ? values.depletion_product_id + : values.product_id, + transaction_subtype: values.transaction_subtype, + qty: Number(values.qty), + price: Number(values.price), + notes: values.notes, }; switch (type) { @@ -109,119 +293,331 @@ const InventoryAdjustmentForm = ({ }, }); - // Fetch Data - const { - setInputValue: setProductCategoryInputValue, - options: productCategoryOptions, - isLoadingOptions: isLoadingProductCategoryOptions, - loadMore: loadMoreProductCategories, - } = useSelect(ProductCategoryApi.basePath, 'id', 'name'); + const { setFieldValue, setFieldTouched, resetForm, setValues } = formik; - const { - setInputValue: setProductInputValue, - options: productOptions, - isLoadingOptions: isLoadingProductOptions, - loadMore: loadMoreProducts, - } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { - product_category_id: formik.values.product_category_id - ? String(formik.values.product_category_id) - : '', - }); + const transactionSubtypeOptions = useMemo(() => { + const transactionType = selectedTransactionType?.value; - const { - setInputValue: setWarehouseInputValue, - options: warehouseOptions, - isLoadingOptions: isLoadingWarehouseOptions, - loadMore: loadMoreWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name'); + if (transactionType === 'RECORDING') { + const allRecordingOptions = [...TRANSACTION_SUBTYPE_OPTIONS.RECORDING]; - // Options Handler - const productCategoryChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - formik.setFieldTouched('product_category_id', true); - formik.setFieldValue('product_category_id', (val as OptionType)?.value); + if (selectedProductFlags.length > 0) { + const isEggProduct = selectedProductFlags.some((flag) => + flag.startsWith('TELUR') + ); + const isChickenProduct = selectedProductFlags.some( + (flag) => flag === 'AYAM' || flag === 'DOC' + ); - formik.setFieldValue('product_category', val); + if (isEggProduct) { + // Produk telur: hanya RECORDING_EGG_IN + return allRecordingOptions.filter( + (opt) => opt.value === 'RECORDING_EGG_IN' + ); + } else if (isChickenProduct) { + // Produk ayam: hanya RECORDING_DEPLETION_IN + return allRecordingOptions.filter( + (opt) => opt.value === 'RECORDING_DEPLETION_IN' + ); + } else { + // Produk non-telur dan non-ayam (PAKAN, OVK, dll): hanya RECORDING_STOCK_OUT + return allRecordingOptions.filter( + (opt) => opt.value === 'RECORDING_STOCK_OUT' + ); + } + } - const disabled = (val as OptionType)?.value == null; - setDisabledProduct(disabled); - formik.setFieldValue('product_id', 0); - formik.setFieldValue('product', null); - formik.setFieldTouched('product', false); - formik.setFieldTouched('product_id', false); + return allRecordingOptions; + } + + return []; + }, [selectedTransactionType, selectedProductFlags]); + + const isTransactionSubtypeReadonly = useMemo(() => { + const transactionType = selectedTransactionType?.value; + return transactionType === 'PEMBELIAN' || transactionType === 'PENJUALAN'; + }, [selectedTransactionType]); + + useEffect(() => { + if (selectedTransactionType?.value === 'RECORDING' && selectedProduct) { + setSelectedTransactionSubtype(null); + setFieldValue('transaction_subtype', ''); + } + }, [setFieldValue, selectedProduct, selectedTransactionType]); + + const isDepletionProductVisible = useMemo(() => { + return selectedTransactionSubtype?.value === 'RECORDING_DEPLETION_IN'; + }, [selectedTransactionSubtype]); + + useEffect(() => { + if (!isDepletionProductVisible) { + setSelectedDepletionProduct(null); + setFieldValue('depletion_product', null); + setFieldValue('depletion_product_id', 0); + } + }, [isDepletionProductVisible, setFieldValue]); + + // ===== EVENT HANDLERS ===== + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const location = val as OptionType | null; + const locationId = location ? Number(location.value) : 0; + + setFieldTouched('location', true); + setFieldValue('location', location); + setFieldTouched('location_id', true); + setFieldValue('location_id', locationId); + + setSelectedLocation(location); + setSelectedProjectFlock(null); + setSelectedKandang(null); + setSelectedProduct(null); + setSelectedProjectFlockLocationId( + location ? location.value.toString() : '' + ); + }; + + const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { + const projectFlock = val as OptionType | null; + const projectFlockId = Number(projectFlock?.value); + + setFieldTouched('project_flock', true); + setFieldValue('project_flock', projectFlock); + setFieldTouched('project_flock_id', true); + setFieldValue('project_flock_id', projectFlockId); + + setSelectedProjectFlock(projectFlock); + setSelectedKandang(null); + setSelectedProduct(null); + }; + + const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { + const kandang = val as OptionType | null; + const kandangId = Number(kandang?.value); + + setFieldTouched('kandang', true); + setFieldValue('kandang', kandang); + setFieldTouched('kandang_id', true); + setFieldValue('kandang_id', kandangId); + + setSelectedKandang(kandang); + setSelectedProduct(null); + setFieldTouched('project_flock_kandang', true); + setFieldTouched('project_flock_kandang_id', true); }; const productChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('product', val); + const product = val as OptionType | null; + const productId = (product?.value as number) ?? 0; - formik.setFieldTouched('product_id', true); - formik.setFieldValue('product_id', (val as OptionType)?.value); + setFieldTouched('product', true); + setFieldValue('product', product); + setFieldTouched('product_id', true); + setFieldValue('product_id', productId); + + setSelectedProduct(product); }; - const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('warehouse', val); + const depletionProductChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const depletionProduct = val as OptionType | null; + const depletionProductId = (depletionProduct?.value as number) ?? 0; - formik.setFieldTouched('warehouse_id', true); - formik.setFieldValue('warehouse_id', (val as OptionType)?.value); + setFieldTouched('depletion_product', true); + setFieldValue('depletion_product', depletionProduct); + setFieldTouched('depletion_product_id', true); + setFieldValue('depletion_product_id', depletionProductId); + + setSelectedDepletionProduct(depletionProduct); + }; + + useEffect(() => { + const transactionType = formik.values.transaction_type; + + if (!transactionType) { + setSelectedTransactionSubtype(null); + setFieldValue('transaction_subtype', ''); + return; + } + + setSelectedTransactionSubtype(null); + setFieldValue('transaction_subtype', ''); + setFieldTouched('transaction_subtype', true); + + if (transactionType === 'PEMBELIAN') { + setFieldValue( + 'transaction_subtype', + TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.value + ); + setSelectedTransactionSubtype({ + value: TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.value, + label: TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.label, + }); + } else if (transactionType === 'PENJUALAN') { + setFieldValue( + 'transaction_subtype', + TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.value + ); + setSelectedTransactionSubtype({ + value: TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.value, + label: TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.label, + }); + } + }, [setFieldTouched, setFieldValue, formik.values.transaction_type]); + + const transactionTypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const typeOption = val as OptionType | null; + const selectedType = typeOption?.value as string; + + setFieldValue('transaction_type', selectedType); + setFieldTouched('transaction_type', true); + + setSelectedTransactionType(typeOption); + }; + + const transactionSubtypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + const subtypeOption = val as OptionType | null; + const selectedSubtype = subtypeOption?.value as string; + + setFieldTouched('transaction_subtype', true); + setFieldValue('transaction_subtype', selectedSubtype); + + setSelectedTransactionSubtype(subtypeOption); }; const resetHandler = () => { - formik.resetForm(); - setQuantityLabel('Tambah Stok'); - productCategoryChangeHandler(null); - productChangeHandler(null); - warehouseChangeHandler(null); + resetForm(); + setSelectedLocation(null); + setSelectedProjectFlock(null); + setSelectedKandang(null); + setSelectedProduct(null); + setSelectedTransactionType(null); + setSelectedTransactionSubtype(null); + setSelectedDepletionProduct(null); + setSelectedProjectFlockLocationId(''); }; - const { setValues: formikSetValues } = formik; - - // Effect useEffect(() => { - if (initialValues?.product_warehouse?.product?.id) { - setDisabledProduct(false); - formik.setFieldValue( - 'product_id', - initialValues.product_warehouse.product.id - ); - formik.setFieldValue('product', { - value: initialValues.product_warehouse.product.id, - label: initialValues.product_warehouse.product.name, - }); - formik.setFieldValue( - 'warehouse_id', - initialValues.product_warehouse.warehouse.id - ); - formik.setFieldValue('warehouse', { - value: initialValues.product_warehouse.warehouse.id, - label: initialValues.product_warehouse.warehouse.name, - }); - formik.setFieldValue( - 'quantity', - initialValues.product_warehouse.quantity - ); - formik.setFieldValue('note', initialValues.note); + if (projectFlockKandangLookup?.project_flock_kandang_id) { + const projectFlockKandangId = + projectFlockKandangLookup.project_flock_kandang_id; + + if (formik.values.project_flock_kandang_id !== projectFlockKandangId) { + setFieldValue('project_flock_kandang_id', projectFlockKandangId); + setFieldValue('project_flock_kandang', { + value: projectFlockKandangId, + label: `${projectFlockKandangLookup.project_flock.flock_name} - ${projectFlockKandangLookup.kandang.name}`, + }); + } } - }, [formik, initialValues, setQuantityLabel, setDisabledProduct]); - useEffect(() => { - formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); - }, [formikSetValues, formikInitialValues]); + }, [ + projectFlockKandangLookup, + formik.values.project_flock_kandang_id, + setFieldValue, + ]); - // Utils Function - const formatNumber = (value: string) => { - const numericValue = value.replace(/[^0-9.]/g, ''); - const [integer, decimal] = numericValue.split('.'); - const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return decimal ? `${formattedInteger}.${decimal}` : formattedInteger; - }; + useEffect(() => { + if (initialValues && type === 'detail') { + const transactionSubtype = initialValues.transaction_subtype; + + let transactionType = ''; + if (transactionSubtype === 'PURCHASE_IN') { + transactionType = 'PEMBELIAN'; + } else if (transactionSubtype === 'MARKETING_OUT') { + transactionType = 'PENJUALAN'; + } else if (transactionSubtype?.startsWith('RECORDING')) { + transactionType = 'RECORDING'; + } + + if (initialValues.product_warehouse?.product) { + const productOption = { + value: initialValues.product_warehouse.product.id, + label: initialValues.product_warehouse.product.name, + }; + setSelectedProduct(productOption); + } + + if (transactionType) { + const typeOption = { + value: transactionType, + label: + TRANSACTION_TYPE_OPTIONS.find( + (opt) => opt.value === transactionType + )?.label || '', + }; + setSelectedTransactionType(typeOption); + } + + if (transactionSubtype) { + let subtypeLabel = ''; + if (transactionSubtype === 'PURCHASE_IN') { + subtypeLabel = TRANSACTION_SUBTYPE_OPTIONS.PEMBELIAN.label; + } else if (transactionSubtype === 'MARKETING_OUT') { + subtypeLabel = TRANSACTION_SUBTYPE_OPTIONS.PENJUALAN.label; + } else { + subtypeLabel = + TRANSACTION_SUBTYPE_OPTIONS.RECORDING.find( + (opt) => opt.value === transactionSubtype + )?.label || ''; + } + setSelectedTransactionSubtype({ + value: transactionSubtype, + label: subtypeLabel, + }); + } + + setValues({ + location: initialValues.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : null, + location_id: initialValues.location?.id ?? 0, + project_flock: initialValues.project_flock + ? { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + } + : null, + project_flock_id: initialValues.project_flock?.id ?? 0, + kandang: null, + kandang_id: 0, + project_flock_kandang: initialValues.project_flock_kandang_id + ? { + value: initialValues.project_flock_kandang_id, + label: `${initialValues.project_flock?.flock_name || ''} - Kandang`, + } + : null, + project_flock_kandang_id: initialValues.project_flock_kandang_id ?? 0, + product: initialValues.product_warehouse?.product + ? { + value: initialValues.product_warehouse.product.id, + label: initialValues.product_warehouse.product.name, + } + : null, + product_id: initialValues.product_warehouse?.product?.id ?? 0, + depletion_product: null, + depletion_product_id: 0, + transaction_type: transactionType, + transaction_subtype: transactionSubtype, + qty: initialValues.qty ?? '', + price: initialValues.price ?? '', + notes: initialValues.notes ?? '', + }); + } + }, [setValues, initialValues, type]); // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); - // Render return ( <> -
+