[];
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 (
+
+
+
+ );
+};
+
+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 || '') && (
);
};
@@ -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 (
-
-
-
-
- Injection Saldo Bank
-
-
-
-
- Saldo Awal
-
-
-
-
- Tambah
-
-
-
-
+ <>
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Injection (Saldo Bank)
+
+
+
+
+
+ Add Initial Balance
+
+
+
+
+
+ Add Finance
+
+
+
+
+ {/* 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',
+ }}
+ />
+
- Reset
-
- filterFormik.handleSubmit()}
- >
- Cari
+
+ Filter
+ {hasFilters && (
+
+ {activeFiltersCount}
+
+ )}
- }
- >
-
-
-
-
-
-
-
-
-
-
-
- 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
+
+
+
+
+
+
+
+
{
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 (
<>
-
-
-
-
-
-
-
- Tambah
-
-
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Adjustment
+
+
-
- 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
+
+
+
+
+
+
+
>
);
};
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 (
<>
-
+
-
- {/* Select Input Product Category */}
-
+
+
+ {/* Select Input Location */}
+
+
+ {/* Select Input Project Flock */}
+
+
+ {/* Select Input Kandang */}
+
+
{/* Select Input Product */}
- {/* Select Input Warehouse */}
+ {/* Select Input Transaction Type */}
-
- {/* Radio Button Flag Stock */}
-
{
- formik.handleChange(e);
- setQuantityLabel(
- e.target.value === 'increase' ? 'Tambah Stok' : 'Kurangi Stok'
- );
- }}
- onBlur={formik.handleBlur}
+ value={selectedTransactionType}
+ onChange={transactionTypeChangeHandler}
+ options={TRANSACTION_TYPE_OPTIONS}
isError={
formik.touched.transaction_type &&
Boolean(formik.errors.transaction_type)
}
errorMessage={formik.errors.transaction_type as string}
- color='primary'
- required
- bottomLabel={
- formik.values.transaction_type == undefined
- ? 'Pilih salah satu tipe transaksi'
- : undefined
- }
- disabled={type === 'detail'}
+ isDisabled={type === 'detail'}
+ placeholder='Pilih Tipe Transaksi'
+ isClearable
+ isSearchable
/>
- {/* Number Input Stock */}
- {
- const rawValue = e.target.value.replace(/,/g, '');
- const numericValue = parseFloat(rawValue);
- if (!isNaN(numericValue)) {
- formik.setFieldValue('quantity', numericValue);
- } else {
- formik.setFieldValue('quantity', 0);
- }
- }}
- onBlur={formik.handleBlur}
+ label='Jenis Transaksi'
+ value={selectedTransactionSubtype}
+ onChange={transactionSubtypeChangeHandler}
+ options={transactionSubtypeOptions}
isError={
- formik.touched.quantity && Boolean(formik.errors.quantity)
+ formik.touched.transaction_subtype &&
+ Boolean(formik.errors.transaction_subtype)
}
- errorMessage={formik.errors.quantity as string}
- readOnly={type === 'detail'}
+ errorMessage={formik.errors.transaction_subtype as string}
+ isDisabled={
+ type === 'detail' ||
+ isTransactionSubtypeReadonly ||
+ !selectedTransactionType
+ }
+ placeholder={
+ !selectedTransactionType
+ ? 'Pilih Tipe Transaksi terlebih dahulu'
+ : isTransactionSubtypeReadonly
+ ? 'Otomatis terisi'
+ : 'Pilih Sub Tipe Transaksi'
+ }
+ isClearable
+ isSearchable
/>
- {/* Text Area Input Reason */}
-
+ )}
+
+ {/* Number Input Quantity */}
+
+ {/* Number Input Price */}
+
+
+ {/* Text Area Input Notes */}
+
-
-
- {type !== 'detail' && (
-
-
- Reset
-
-
- Submit
-
-
- )}
-
+
0 ? 'block' : 'hidden'} mt-4`}
+ >
+
+
{InventoryAdjustmentFormErrorMessage && (
{InventoryAdjustmentFormErrorMessage}
)}
+
+ {type !== 'detail' && (
+ <>
+
+ Reset
+
+
+ Submit
+
+ >
+ )}
+
>
diff --git a/src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx b/src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx
new file mode 100644
index 00000000..3473f996
--- /dev/null
+++ b/src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx
@@ -0,0 +1,37 @@
+import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
+import Table from '@/components/Table';
+import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
+import { ColumnDef } from '@tanstack/react-table';
+
+const InventoryAdjustmentTableSkeleton = ({
+ columns,
+ icon,
+ title = 'No Data Available',
+ subtitle = 'There is no inventory adjustment data displayed. Enter inventory adjustment data to get started.',
+}: {
+ columns: ColumnDef
[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default InventoryAdjustmentTableSkeleton;
diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx
index c0d51a50..f953099d 100644
--- a/src/components/pages/inventory/movement/MovementTable.tsx
+++ b/src/components/pages/inventory/movement/MovementTable.tsx
@@ -1,49 +1,98 @@
'use client';
-import { ChangeEventHandler, useState } from 'react';
+import {
+ ChangeEventHandler,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { usePathname } from 'next/navigation';
import useSWR from 'swr';
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
+import { useFormik } from 'formik';
import Table from '@/components/Table';
import { Icon } from '@iconify/react';
import { Movement } from '@/types/api/inventory/movement';
import { MovementApi } from '@/services/api/inventory';
+import { WarehouseApi, ProductApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { ROWS_OPTIONS } from '@/config/constant';
-import { OptionType } from '@/components/input/SelectInput';
+import { useUiStore } from '@/stores/ui/ui.store';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
-import SelectInput from '@/components/input/SelectInput';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
+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 RequirePermission from '@/components/helper/RequirePermission';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import MovementTableSkeleton from '@/components/pages/inventory/movement/skeleton/MovementTableSkeleton';
+import { Warehouse } from '@/types/api/master-data/warehouse';
+import { Product } from '@/types/api/master-data/product';
+import {
+ MovementFilterSchema,
+ MovementFilterType,
+} from '@/components/pages/inventory/movement/filter/MovementFilter';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
-}) => (
-
-
- {
+ const popoverId = `movement#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
+ return (
+
+
-
- Detail
-
-
-
-);
+
+
+
+
+
+
+
+
+ Detail
+
+
+
+
+
+ );
+};
const MovementTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -53,13 +102,109 @@ const MovementTable = () => {
} = useTableFilter({
initial: {
search: '',
+ productFilter: '',
+ warehouseFilter: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
+ productFilter: 'product_id',
+ warehouseFilter: 'warehouse_id',
},
});
+ // ===== FILTER MODAL STATE =====
+ const filterModal = useModal();
+
+ // ===== FORMIK SETUP =====
+ const formik = useFormik({
+ initialValues: {
+ product_id: null,
+ warehouse_id: null,
+ },
+ validationSchema: MovementFilterSchema,
+ onSubmit: (values, { setSubmitting }) => {
+ updateFilter('productFilter', values.product_id || '');
+ updateFilter('warehouseFilter', values.warehouse_id || '');
+ filterModal.closeModal();
+ setSubmitting(false);
+ },
+ onReset: () => {
+ updateFilter('productFilter', '');
+ updateFilter('warehouseFilter', '');
+ },
+ });
+
+ // ===== 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'
+ );
+
+ // ===== 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]
+ );
+
+ // ===== 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]);
+
+ // ===== HANDLE FILTER MODAL OPEN =====
+ const handleFilterModalOpen = () => {
+ filterModal.openModal();
+ formik.validateForm();
+ };
+
const [sorting, setSorting] = useState([]);
const { data: movements, isLoading } = useSWR(
@@ -67,153 +212,249 @@ const MovementTable = () => {
MovementApi.getAllFetcher
);
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
+
+ useEffect(() => {
+ setTableState('movement-table', pathname);
+ }, [pathname, setTableState]);
+
const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
updateFilter('search', e.target.value);
};
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
- setPageSize(newVal.value as number);
- setPage(1);
- };
-
- const movementColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorFn: (row) => row.source_warehouse?.name,
- header: 'Gudang Asal',
- },
- {
- accessorFn: (row) => row.destination_warehouse?.name,
- header: 'Gudang Tujuan',
- },
- {
- accessorKey: 'transfer_reason',
- header: 'Catatan',
- },
- {
- accessorKey: 'transfer_date',
- header: 'Tanggal',
- cell: (props) =>
- new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
- },
- {
- accessorFn: (row) => {
- const totalCost = row.deliveries?.reduce(
- (sum, d) => sum + (d.shipping_cost_total || 0),
- 0
- );
- return totalCost?.toLocaleString('id-ID');
+ const movementColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
},
- header: 'Biaya Pengiriman',
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
-
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
-
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
+ {
+ accessorFn: (row) => row.source_warehouse?.name,
+ header: 'Gudang Asal',
},
- },
- ];
+ {
+ accessorFn: (row) => row.destination_warehouse?.name,
+ header: 'Gudang Tujuan',
+ },
+ {
+ accessorKey: 'transfer_reason',
+ header: 'Catatan',
+ },
+ {
+ accessorKey: 'transfer_date',
+ header: 'Tanggal',
+ cell: (props) =>
+ new Date(props.row.original.transfer_date).toLocaleDateString(
+ 'id-ID'
+ ),
+ },
+ {
+ accessorFn: (row) => {
+ const totalCost = row.deliveries?.reduce(
+ (sum, d) => sum + (d.shipping_cost_total || 0),
+ 0
+ );
+ return totalCost?.toLocaleString('id-ID');
+ },
+ header: 'Biaya Pengiriman',
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page]
+ );
return (
<>
-
-
-
-
-
-
-
- Add Movement
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Movement
+
+
-
-
+
+ }
className={{
- wrapper:
- 'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
+ 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(movements) ? movements?.data : []}
- columns={movementColumns}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
- totalItems={
- isResponseSuccess(movements) ? movements?.meta?.total_results : 0
- }
- onPageChange={setPage}
- isLoading={isLoading}
- sorting={sorting}
- setSorting={setSorting}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(movements) && movements?.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(movements) || movements.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ data={isResponseSuccess(movements) ? movements?.data : []}
+ columns={movementColumns}
+ pageSize={tableFilterState.pageSize}
+ page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
+ totalItems={
+ isResponseSuccess(movements)
+ ? movements?.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
+
+
+
+
+
+
+
>
);
};
diff --git a/src/components/pages/inventory/movement/filter/MovementFilter.ts b/src/components/pages/inventory/movement/filter/MovementFilter.ts
new file mode 100644
index 00000000..fc27b898
--- /dev/null
+++ b/src/components/pages/inventory/movement/filter/MovementFilter.ts
@@ -0,0 +1,11 @@
+import { string, object } from 'yup';
+
+export const MovementFilterSchema = object().shape({
+ product_id: string().nullable(),
+ warehouse_id: string().nullable(),
+});
+
+export type MovementFilterType = {
+ product_id: string | null;
+ warehouse_id: string | null;
+};
diff --git a/src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx b/src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx
new file mode 100644
index 00000000..a3ba3c5a
--- /dev/null
+++ b/src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx
@@ -0,0 +1,37 @@
+import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
+import Table from '@/components/Table';
+import { Movement } from '@/types/api/inventory/movement';
+import { ColumnDef } from '@tanstack/react-table';
+
+const MovementTableSkeleton = ({
+ columns,
+ icon,
+ title = 'No Data Available',
+ subtitle = 'There is no movement data displayed. Enter movement data to get started.',
+}: {
+ columns: ColumnDef[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default MovementTableSkeleton;
diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx
index 316bd103..21ded2bc 100644
--- a/src/components/pages/inventory/product/InventoryProductTable.tsx
+++ b/src/components/pages/inventory/product/InventoryProductTable.tsx
@@ -2,46 +2,78 @@
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
import Table from '@/components/Table';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
-import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
import { InventoryProductApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
+import { useUiStore } from '@/stores/ui/ui.store';
import { InventoryProduct } from '@/types/api/inventory/product';
import { Icon } from '@iconify/react';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
-import { ChangeEventHandler, useMemo, useState } from 'react';
+import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
+import { usePathname } from 'next/navigation';
import useSWR from 'swr';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
-}) => (
-
-
- {
+ const popoverId = `product#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-product#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
+ return (
+
+
-
- Detail
-
-
-
-);
+
+
+
+
+
+
+
+
+ Detail
+
+
+
+
+
+ );
+};
const InventoryProductTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -65,20 +97,23 @@ const InventoryProductTable = () => {
InventoryProductApi.getAllFetcher
);
- const searchChangeHandler: ChangeEventHandler = (e) => {
- updateFilter('search', e.target.value);
- };
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
- setPageSize(newVal.value as number);
- setPage(1);
+ useEffect(() => {
+ setTableState('inventory-product-table', pathname);
+ }, [pathname, setTableState]);
+
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
};
const columns: ColumnDef[] = useMemo(
() => [
{
- header: '#',
+ header: 'No',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
@@ -125,7 +160,7 @@ const InventoryProductTable = () => {
},
{
header: 'Aksi',
- cell: (props) => {
+ cell: (props: CellContext) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
@@ -135,96 +170,108 @@ const InventoryProductTable = () => {
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
+
);
},
},
],
- []
+ [tableFilterState.pageSize, tableFilterState.page]
);
return (
- <>
-
-
-
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Product
+
+
-
- data={
- isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : []
- }
- columns={columns}
- pageSize={tableFilterState.pageSize}
- page={
- isResponseSuccess(inventoryProducts)
- ? inventoryProducts?.meta?.page
- : 0
- }
- totalItems={
- isResponseSuccess(inventoryProducts)
- ? inventoryProducts?.meta?.total_results
- : 0
- }
- onPageChange={setPage}
- isLoading={isLoading}
- sorting={sorting}
- setSorting={setSorting}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(inventoryProducts) &&
- inventoryProducts?.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 */}
+
+
+ }
+ 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(inventoryProducts) ||
+ inventoryProducts.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ data={
+ isResponseSuccess(inventoryProducts)
+ ? inventoryProducts?.data
+ : []
+ }
+ columns={columns}
+ pageSize={tableFilterState.pageSize}
+ page={
+ isResponseSuccess(inventoryProducts)
+ ? inventoryProducts?.meta?.page
+ : 0
+ }
+ totalItems={
+ isResponseSuccess(inventoryProducts)
+ ? inventoryProducts?.meta?.total_results
+ : 0
+ }
+ onPageChange={setPage}
+ onPageSizeChange={setPageSize}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: cn('p-3 mb-0'),
+ headerColumnClassName: 'text-nowrap',
+ }}
+ />
+ )}
+
+
);
};
diff --git a/src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx b/src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx
new file mode 100644
index 00000000..9fe9cb51
--- /dev/null
+++ b/src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx
@@ -0,0 +1,37 @@
+import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
+import Table from '@/components/Table';
+import { InventoryProduct } from '@/types/api/inventory/product';
+import { ColumnDef } from '@tanstack/react-table';
+
+const InventoryProductTableSkeleton = ({
+ columns,
+ icon,
+ title = 'No Data Available',
+ subtitle = 'There is no inventory product data displayed. Enter inventory product data to get started.',
+}: {
+ columns: ColumnDef[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default InventoryProductTableSkeleton;
diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx
index ae559328..4635c826 100644
--- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx
+++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx
@@ -1,7 +1,6 @@
'use client';
import AlertErrorList from '@/components/helper/form/FormErrors';
-import { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
@@ -112,16 +111,34 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
useState(null);
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
DeliveryOrderProductFormValues[]
- >(
- isResponseSuccess(marketing)
- ? mergeSOwithDO(
- marketing?.data.sales_order?.map(SalesProductToFieldValues) ?? [],
- marketing?.data.delivery_order?.flatMap((delivery) =>
- DeliveryProductToFieldValues(marketing.data.sales_order, delivery)
- ) ?? [],
- true
- )
- : []
+ >([]);
+
+ const getDeliveryOrderValues = useCallback(
+ (marketingData: Marketing): DeliveryOrderProductFormValues[] => {
+ const hasDeliveryOrder =
+ marketingData.delivery_order &&
+ marketingData.delivery_order.length > 0 &&
+ marketingData.delivery_order.some(
+ (doItem) => doItem.deliveries && doItem.deliveries.length > 0
+ );
+
+ if (hasDeliveryOrder) {
+ return (
+ marketingData.delivery_order?.flatMap((delivery) =>
+ DeliveryProductToFieldValues(marketingData.sales_order, delivery)
+ ) ?? []
+ );
+ }
+
+ return mergeSOwithDO(
+ marketingData.sales_order?.map(SalesProductToFieldValues) ?? [],
+ marketingData.delivery_order?.flatMap((delivery) =>
+ DeliveryProductToFieldValues(marketingData.sales_order, delivery)
+ ) ?? [],
+ true
+ );
+ },
+ []
);
// ================== SETUP FORMIK ==================
@@ -130,14 +147,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
>(() => {
if (!isResponseSuccess(marketing))
return {} as SalesOrderFormValues & DeliveryOrderFormValues;
- const deliveryValues = mergeSOwithDO(
- marketing?.data.sales_order?.map(SalesProductToFieldValues) ?? [],
- marketing?.data.delivery_order?.flatMap((delivery) =>
- DeliveryProductToFieldValues(marketing.data.sales_order, delivery)
- ) ?? [],
- true
- );
+ const deliveryValues = getDeliveryOrderValues(marketing.data);
setDeliveryOrderValues(deliveryValues);
return {
@@ -163,7 +174,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
) ?? [],
delivery_order: deliveryValues,
};
- }, [marketing]);
+ }, [marketing, getDeliveryOrderValues]);
const formik = useFormik({
enableReinitialize: true,
@@ -648,9 +659,8 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
| No. Order |
- {marketing.data.do_number
- ? marketing.data.do_number
- : marketing.data.so_number}
+ {marketing.data.do_number ||
+ marketing.data.so_number}
|
@@ -765,6 +775,7 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
{
updateFilter('customer_id', '');
};
- // ===== ACTIVE FILTERS COUNT =====
- const activeFiltersCount = useMemo(() => {
- let count = 0;
-
- // Product filter
- if (tableFilterState.product_ids) {
- count += 1;
- }
-
- // Status filter
- if (tableFilterState.status) {
- count += 1;
- }
-
- // Customer filter
- if (tableFilterState.customer_id) {
- count += 1;
- }
-
- return count;
- }, [
- tableFilterState.product_ids,
- tableFilterState.status,
- tableFilterState.customer_id,
- ]);
-
const approveClickHandler = () => {
setApproveAction('APPROVED');
confirmationModal.openModal();
@@ -588,28 +564,14 @@ const MarketingTable = () => {
)}
- {
filterModal.openModal();
}}
- className={cn(
- 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
- {
- 'border-primary-gradient text-primary':
- activeFiltersCount > 0,
- }
- )}
- >
-
- Filter
- {activeFiltersCount > 0 && (
-
- {activeFiltersCount}
-
- )}
-
+ className='px-3 py-2.5'
+ />
{
-
+
+ {isLoadingMarketing ? (
+
+
+
+ ) : !isResponseSuccess(marketing) || marketing.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ )}
+
Promise;
@@ -115,6 +120,36 @@ const DeliveryOrderProductForm = ({
})
?.filter((item) => item != null) as OptionType[];
+ const hasDeliveryOrder = useMemo(() => {
+ return (
+ deliveryOrders &&
+ deliveryOrders.length > 0 &&
+ deliveryOrders.some(
+ (doItem) => doItem.deliveries && doItem.deliveries.length > 0
+ )
+ );
+ }, [deliveryOrders]);
+
+ const deliveryOrder = useMemo(() => {
+ if (!hasDeliveryOrder || !deliveryOrders) return null;
+
+ for (const doItem of deliveryOrders) {
+ const found = doItem.deliveries.find(
+ (d) =>
+ d.product_warehouse.id ===
+ initialValues?.marketing_product?.product_warehouse_id
+ );
+ if (found) {
+ return {
+ ...found,
+ delivery_date: doItem.delivery_date,
+ do_number: doItem.do_number,
+ };
+ }
+ }
+ return null;
+ }, [deliveryOrders, hasDeliveryOrder, initialValues]);
+
const salesOrder = salesOrders.find(
(item) => item.id === initialValues?.marketing_product_id
);
@@ -122,15 +157,25 @@ const DeliveryOrderProductForm = ({
const formik = useFormik({
enableReinitialize: true,
initialValues: {
- delivery_date: initialValues?.delivery_date || undefined,
- vehicle_number: initialValues?.vehicle_number || undefined,
+ delivery_date:
+ deliveryOrder?.delivery_date ||
+ initialValues?.delivery_date ||
+ undefined,
+ vehicle_number:
+ deliveryOrder?.vehicle_number ||
+ initialValues?.vehicle_number ||
+ undefined,
marketing_product_id:
salesOrder?.id || initialValues?.marketing_product_id || undefined,
- unit_price: initialValues?.unit_price || undefined,
- total_weight: initialValues?.total_weight || undefined,
- qty: initialValues?.qty || undefined,
- avg_weight: initialValues?.avg_weight || undefined,
- total_price: initialValues?.total_price || undefined,
+ unit_price:
+ deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined,
+ total_weight:
+ deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined,
+ qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined,
+ avg_weight:
+ deliveryOrder?.avg_weight ?? initialValues?.avg_weight ?? undefined,
+ total_price:
+ deliveryOrder?.total_price ?? initialValues?.total_price ?? undefined,
marketing_product: initialValues?.marketing_product || undefined,
uom: initialValues?.uom || '',
weight_per_convertion:
@@ -236,6 +281,25 @@ const DeliveryOrderProductForm = ({
});
};
+ // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
+ const handleFieldChange = (
+ field: string,
+ value: number | string,
+ callback?: () => void
+ ) => {
+ formik.setFieldValue(field, value);
+
+ setTimeout(() => {
+ handleMarketingCalculation(field, {
+ values: { ...formik.values, [field]: value },
+ setFieldValue: formik.setFieldValue,
+ hasSisaBerat,
+ });
+ }, 0);
+
+ if (callback) callback();
+ };
+
// Handler khusus untuk toggle sisa berat - langsung pakai nilai baru
const handleSisaBeratToggle = (newHasSisaBerat: boolean) => {
setHasSisaBerat(newHasSisaBerat);
@@ -520,13 +584,11 @@ const DeliveryOrderProductForm = ({
} per ${formik.values.convertion_unit?.value}`}
value={formik.values.weight_per_convertion ?? ''}
onChange={(e) => {
- formik.setFieldValue(
- 'weight_per_convertion',
- Number(e.target.value)
+ const value = Number(e.target.value);
+ handleFieldChange('weight_per_convertion', value, () =>
+ setCurrentInput(e.target.name)
);
- setCurrentInput(e.target.name);
}}
- onBlur={() => handleBlurField('weight_per_convertion')}
/>
@@ -564,10 +626,11 @@ const DeliveryOrderProductForm = ({
name='total_peti'
value={formik.values.total_peti ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('total_peti', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('total_peti')}
isError={
formik.touched.total_peti && Boolean(formik.errors.total_peti)
}
@@ -592,10 +655,11 @@ const DeliveryOrderProductForm = ({
name='avg_weight'
value={formik.values.avg_weight}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('avg_weight', value, () =>
+ setCurrentInput('avg_weight')
+ );
}}
- onBlur={() => handleBlurField('avg_weight')}
isError={
formik.touched.avg_weight &&
Boolean(formik.errors.avg_weight)
@@ -613,10 +677,11 @@ const DeliveryOrderProductForm = ({
name='total_weight'
value={formik.values.total_weight}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('total_weight', value, () =>
+ setCurrentInput('total_weight')
+ );
}}
- onBlur={() => handleBlurField('total_weight')}
isError={
formik.touched.total_weight &&
Boolean(formik.errors.total_weight)
@@ -638,10 +703,11 @@ const DeliveryOrderProductForm = ({
name='qty'
value={formik.values.qty}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('qty', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('qty')}
isError={Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas'
@@ -677,10 +743,11 @@ const DeliveryOrderProductForm = ({
name='price_per_convertion'
value={formik.values.price_per_convertion ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('price_per_convertion', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('price_per_convertion')}
isError={
formik.touched.price_per_convertion &&
Boolean(formik.errors.price_per_convertion)
@@ -699,10 +766,11 @@ const DeliveryOrderProductForm = ({
name='price_per_qty'
value={formik.values.price_per_qty ?? undefined}
onChange={(e) => {
- formik.setFieldValue('price_per_qty', Number(e.target.value));
- setCurrentInput('price_per_qty');
+ const value = Number(e.target.value);
+ handleFieldChange('price_per_qty', value, () =>
+ setCurrentInput('price_per_qty')
+ );
}}
- onBlur={() => handleBlurField('price_per_qty')}
isError={
formik.touched.price_per_qty &&
Boolean(formik.errors.price_per_qty)
@@ -721,10 +789,11 @@ const DeliveryOrderProductForm = ({
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('unit_price', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('unit_price')}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
@@ -760,10 +829,11 @@ const DeliveryOrderProductForm = ({
name='sisa_berat'
value={formik.values.sisa_berat ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('sisa_berat', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('sisa_berat')}
isError={
formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat)
}
@@ -776,10 +846,11 @@ const DeliveryOrderProductForm = ({
name='price_sisa_berat'
value={formik.values.price_sisa_berat ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('price_sisa_berat', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('price_sisa_berat')}
isError={
formik.touched.price_sisa_berat &&
Boolean(formik.errors.price_sisa_berat)
@@ -797,10 +868,11 @@ const DeliveryOrderProductForm = ({
name='total_price'
value={formik.values.total_price}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('total_price', value, () =>
+ setCurrentInput('total_price')
+ );
}}
- onBlur={() => handleBlurField('total_price')}
isError={
formik.touched.total_price && Boolean(formik.errors.total_price)
}
diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
index 70965071..8da873e5 100644
--- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
+++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
@@ -250,6 +250,25 @@ const SalesOrderProductForm = ({
});
};
+ // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
+ const handleFieldChange = (
+ field: string,
+ value: number | string,
+ callback?: () => void
+ ) => {
+ formik.setFieldValue(field, value);
+
+ setTimeout(() => {
+ handleMarketingCalculation(field, {
+ values: { ...formik.values, [field]: value },
+ setFieldValue: formik.setFieldValue,
+ hasSisaBerat,
+ });
+ }, 0);
+
+ if (callback) callback();
+ };
+
// Handler khusus untuk toggle sisa berat - langsung pakai nilai baru
const handleSisaBeratToggle = (newHasSisaBerat: boolean) => {
setHasSisaBerat(newHasSisaBerat);
@@ -475,13 +494,11 @@ const SalesOrderProductForm = ({
} per ${formik.values.convertion_unit?.value}`}
value={formik.values.weight_per_convertion ?? ''}
onChange={(e) => {
- formik.setFieldValue(
- 'weight_per_convertion',
- Number(e.target.value)
+ const value = Number(e.target.value);
+ handleFieldChange('weight_per_convertion', value, () =>
+ setCurrentInput(e.target.name)
);
- setCurrentInput(e.target.name);
}}
- onBlur={() => handleBlurField('weight_per_convertion')}
/>
@@ -519,10 +536,11 @@ const SalesOrderProductForm = ({
name='total_peti'
value={formik.values.total_peti ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('total_peti', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('total_peti')}
isError={
formik.touched.total_peti && Boolean(formik.errors.total_peti)
}
@@ -547,10 +565,11 @@ const SalesOrderProductForm = ({
name='avg_weight'
value={formik.values.avg_weight}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('avg_weight', value, () =>
+ setCurrentInput('avg_weight')
+ );
}}
- onBlur={() => handleBlurField('avg_weight')}
isError={
formik.touched.avg_weight &&
Boolean(formik.errors.avg_weight)
@@ -568,10 +587,11 @@ const SalesOrderProductForm = ({
name='total_weight'
value={formik.values.total_weight}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('total_weight', value, () =>
+ setCurrentInput('total_weight')
+ );
}}
- onBlur={() => handleBlurField('total_weight')}
isError={
formik.touched.total_weight &&
Boolean(formik.errors.total_weight)
@@ -593,10 +613,11 @@ const SalesOrderProductForm = ({
name='qty'
value={formik.values.qty}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('qty', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('qty')}
isError={formik.touched.qty && Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas'
@@ -630,10 +651,11 @@ const SalesOrderProductForm = ({
name='price_per_convertion'
value={formik.values.price_per_convertion ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('price_per_convertion', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('price_per_convertion')}
isError={
formik.touched.price_per_convertion &&
Boolean(formik.errors.price_per_convertion)
@@ -652,10 +674,11 @@ const SalesOrderProductForm = ({
name='price_per_qty'
value={formik.values.price_per_qty ?? undefined}
onChange={(e) => {
- formik.setFieldValue('price_per_qty', Number(e.target.value));
- setCurrentInput('price_per_qty');
+ const value = Number(e.target.value);
+ handleFieldChange('price_per_qty', value, () =>
+ setCurrentInput('price_per_qty')
+ );
}}
- onBlur={() => handleBlurField('price_per_qty')}
isError={
formik.touched.price_per_qty &&
Boolean(formik.errors.price_per_qty)
@@ -674,10 +697,11 @@ const SalesOrderProductForm = ({
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('unit_price', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('unit_price')}
isError={
formik.touched.unit_price && Boolean(formik.errors.unit_price)
}
@@ -715,10 +739,11 @@ const SalesOrderProductForm = ({
name='sisa_berat'
value={formik.values.sisa_berat ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('sisa_berat', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('sisa_berat')}
isError={
formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat)
}
@@ -731,10 +756,11 @@ const SalesOrderProductForm = ({
name='price_sisa_berat'
value={formik.values.price_sisa_berat ?? undefined}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('price_sisa_berat', value, () =>
+ setCurrentInput(e.target.name)
+ );
}}
- onBlur={() => handleBlurField('price_sisa_berat')}
isError={
formik.touched.price_sisa_berat &&
Boolean(formik.errors.price_sisa_berat)
@@ -752,10 +778,11 @@ const SalesOrderProductForm = ({
name='total_price'
value={formik.values.total_price}
onChange={(e) => {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
+ const value = Number(e.target.value);
+ handleFieldChange('total_price', value, () =>
+ setCurrentInput('total_price')
+ );
}}
- onBlur={() => handleBlurField('total_price')}
isError={
formik.touched.total_price && Boolean(formik.errors.total_price)
}
diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx
index 12d97b9a..71a6040c 100644
--- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx
+++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx
@@ -2,10 +2,11 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for
import Button from '@/components/Button';
import Card from '@/components/Card';
import { Icon } from '@iconify/react';
-import { useRef } from 'react';
+import { useRef, useMemo } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
-import { Marketing } from '@/types/api/marketing/marketing';
+import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing';
+import { Warehouse } from '@/types/api/master-data/warehouse';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
@@ -42,7 +43,31 @@ const DeliveryOrderProductTable = ({
const approvalStepNumber = marketing?.latest_approval?.step_number;
- const renderTableContent = (item: DeliveryOrderProductFormValues) => {
+ const hasDeliveryOrder = useMemo(() => {
+ return (
+ marketing?.delivery_order &&
+ marketing.delivery_order.length > 0 &&
+ marketing.delivery_order.some(
+ (doItem) => doItem.deliveries && doItem.deliveries.length > 0
+ )
+ );
+ }, [marketing?.delivery_order]);
+
+ const deliveryItems = useMemo(() => {
+ if (!hasDeliveryOrder) return [];
+ return (
+ marketing?.delivery_order?.flatMap((doItem) =>
+ doItem.deliveries.map((delivery) => ({
+ ...delivery,
+ do_number: doItem.do_number,
+ delivery_date: doItem.delivery_date,
+ warehouse: doItem.warehouse,
+ }))
+ ) ?? []
+ );
+ }, [marketing?.delivery_order, hasDeliveryOrder]);
+
+ const renderSalesOrderContent = (item: DeliveryOrderProductFormValues) => {
const doItem = marketing?.delivery_order?.find(
(doItem) => doItem.do_number === item.do_number
);
@@ -185,50 +210,217 @@ const DeliveryOrderProductTable = ({
);
};
+ const renderDeliveryOrderContent = (
+ item: BaseDelivery & {
+ do_number: string;
+ delivery_date: string;
+ warehouse: Warehouse;
+ }
+ ) => {
+ const parentDoItem = marketing?.delivery_order?.find(
+ (doItem) => doItem.do_number === item.do_number
+ );
+
+ return (
+ <>
+
+ |
+ Label
+ |
+
+
+ |
+
+ <>
+
+ | Gudang |
+ {item.warehouse?.name} |
+
+
+ | Produk |
+
+ {item.product_warehouse?.product?.name}
+ |
+
+
+ | Qty |
+
+ {item.qty
+ ? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}`
+ : '-'}
+ |
+
+ {Number(item.avg_weight ?? 0) > 0 && (
+
+ | Avg Bobot |
+
+ {formatNumber(Number(item.avg_weight))} Kg
+ |
+
+ )}
+ {Number(item.total_weight ?? 0) > 0 && (
+
+ | Total Bobot |
+
+ {formatNumber(Number(item.total_weight))}
+ |
+
+ )}
+
+ | Total Harga Satuan |
+
+ {formatCurrency(item.unit_price)}
+ |
+
+
+ | Total Penjualan |
+
+ {formatCurrency(item.total_price)}
+ |
+
+ >
+
+ |
+ Label
+ |
+
+
+ |
+
+ <>
+ {approvalStepNumber !== 1 && (
+
+ | Tanggal Pengiriman |
+
+ {item.delivery_date
+ ? formatDate(item.delivery_date, 'DD MMM YYYY')
+ : '-'}
+ |
+
+ )}
+ {item.do_number && (
+
+ | No. Pengiriman |
+ {item.do_number} |
+
+ )}
+
+ | No. Polisi |
+ {item.vehicle_number} |
+
+ {parentDoItem && (
+
+ | Dokumen Pengiriman |
+
+
+ |
+
+ )}
+ >
+ >
+ );
+ };
+
return (
<>
- {data.map((item) => (
-
- {formType === 'success' ? (
-
-
- {renderTableContent(item)}
-
+ {hasDeliveryOrder
+ ? deliveryItems.map((item, index) => (
+
+ {formType === 'success' ? (
+
+
+
+ {renderDeliveryOrderContent(item)}
+
+
+
+ ) : (
+
+
+
+ {renderDeliveryOrderContent(item)}
+
+
+
+ )}
- ) : (
-
-
- {renderTableContent(item)}
-
-
- )}
-
- ))}
+ ))
+ : data.map((item) => (
+
+ {formType === 'success' ? (
+
+
+
+ {renderSalesOrderContent(item)}
+
+
+
+ ) : (
+
+
+
+ {renderSalesOrderContent(item)}
+
+
+
+ )}
+
+ ))}
>
);
diff --git a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx
index 5db89450..cdf18652 100644
--- a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx
+++ b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx
@@ -1,7 +1,7 @@
import Button from '@/components/Button';
import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
-import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
+import { Document, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
diff --git a/src/components/pages/marketing/skeleton/MarketingTableSkeleton.tsx b/src/components/pages/marketing/skeleton/MarketingTableSkeleton.tsx
new file mode 100644
index 00000000..babbf21b
--- /dev/null
+++ b/src/components/pages/marketing/skeleton/MarketingTableSkeleton.tsx
@@ -0,0 +1,37 @@
+import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
+import Table from '@/components/Table';
+import { Marketing } from '@/types/api/marketing/marketing';
+import { ColumnDef } from '@tanstack/react-table';
+
+const MarketingTableSkeleton = ({
+ columns,
+ icon,
+ title = 'No Data Available',
+ subtitle = 'There is no marketing data displayed. Enter marketing data to get started.',
+}: {
+ columns: ColumnDef
[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default MarketingTableSkeleton;
diff --git a/src/components/pages/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx
index d92c7840..95f91ee9 100644
--- a/src/components/pages/master-data/area/AreasTable.tsx
+++ b/src/components/pages/master-data/area/AreasTable.tsx
@@ -1,6 +1,6 @@
'use client';
-import { ChangeEventHandler, useEffect, useState } from 'react';
+import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
@@ -11,75 +11,101 @@ 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 } 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 RequirePermission from '@/components/helper/RequirePermission';
+import AreaTableSkeleton from '@/components/pages/master-data/area/skeleton/AreaTableSkeleton';
import { Area } from '@/types/api/master-data/area';
import { AreaApi } from '@/services/api/master-data';
-import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { ROWS_OPTIONS } from '@/config/constant';
+import { usePathname } from 'next/navigation';
+import { useUiStore } from '@/stores/ui/ui.store';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
deleteClickHandler: () => void;
}) => {
+ const popoverId = `area#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-area#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
return (
-
-
-
-
- Detail
-
-
+
+
+
+
-
-
-
- Edit
-
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+
+ Detail
+
+
+
+
+
+ Edit
+
+
+
+ {
+ deleteClickHandler();
+ closePopover();
+ }}
+ variant='ghost'
+ color='none'
+ className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
+ >
+
+ Delete
+
+
+
+
+
);
};
const AreasTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -87,10 +113,17 @@ const AreasTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
- initial: { search: '', nameSort: '' },
- paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' },
+ initial: {
+ search: searchValue,
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ },
});
+ const [sorting, setSorting] = useState([]);
+
const {
data: areas,
isLoading,
@@ -101,65 +134,21 @@ const AreasTable = () => {
);
const deleteModal = useModal();
-
const [selectedArea, setSelectedArea] = useState(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
- const [sorting, setSorting] = useState([]);
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
- const areasColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama',
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+ useEffect(() => {
+ setTableState('areas-table', pathname);
+ }, [pathname, setTableState]);
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
-
- const deleteClickHandler = () => {
- setSelectedArea(props.row.original);
- deleteModal.openModal();
- };
-
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ];
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
+ };
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -179,95 +168,132 @@ const AreasTable = () => {
setIsDeleteLoading(false);
};
- const searchChangeHandler: ChangeEventHandler = (e) => {
- updateFilter('search', e.target.value);
- };
+ const areasColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
- setPageSize(newVal.value as number);
- };
+ const deleteClickHandler = () => {
+ setSelectedArea(props.row.original);
+ deleteModal.openModal();
+ };
- // track sorting
- useEffect(() => {
- const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
-
- if (!isNameSorted) {
- updateFilter('nameSort', '');
- } else {
- updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
- }
- }, [sorting, updateFilter]);
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page, deleteModal]
+ );
return (
<>
-
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Area
+
+
-
-
+
+ }
+ 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',
}}
- onChange={pageSizeChangeHandler}
- className={{ wrapper: 'max-w-28' }}
/>
-
- data={isResponseSuccess(areas) ? areas?.data : []}
- columns={areasColumns}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(areas) ? areas?.meta?.page : 0}
- totalItems={isResponseSuccess(areas) ? areas?.meta?.total_results : 0}
- onPageChange={setPage}
- isLoading={isLoading}
- sorting={sorting}
- setSorting={setSorting}
- className={{
- containerClassName: cn({
- 'mb-20': isResponseSuccess(areas) && areas?.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(areas) || areas.data?.length === 0 ? (
+
+ ) : (
+
+ data={isResponseSuccess(areas) ? areas?.data : []}
+ columns={areasColumns}
+ pageSize={tableFilterState.pageSize}
+ page={isResponseSuccess(areas) ? areas?.meta?.page : 0}
+ totalItems={
+ isResponseSuccess(areas) ? areas?.meta?.total_results : 0
+ }
+ onPageChange={setPage}
+ onPageSizeChange={setPageSize}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: 'p-3 mb-0',
+ headerColumnClassName: 'text-nowrap',
+ }}
+ />
+ )}
+
[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default AreaTableSkeleton;
diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx
index c5a564fe..cc62cf70 100644
--- a/src/components/pages/master-data/bank/BanksTable.tsx
+++ b/src/components/pages/master-data/bank/BanksTable.tsx
@@ -1,6 +1,6 @@
'use client';
-import { ChangeEventHandler, useEffect, useState } from 'react';
+import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
@@ -11,75 +11,101 @@ 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 } from '@/components/input/SelectInput';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import BankTableSkeleton from '@/components/pages/master-data/bank/skeleton/BankTableSkeleton';
import { Bank } from '@/types/api/master-data/bank';
import { BankApi } from '@/services/api/master-data';
-import { cn } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { ROWS_OPTIONS } from '@/config/constant';
+import { usePathname } from 'next/navigation';
+import { useUiStore } from '@/stores/ui/ui.store';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
deleteClickHandler: () => void;
}) => {
+ const popoverId = `bank#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-bank#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
return (
-
-
-
-
- Detail
-
-
+
+
+
+
-
-
-
- Edit
-
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+
+ Detail
+
+
+
+
+
+ Edit
+
+
+
+ {
+ deleteClickHandler();
+ closePopover();
+ }}
+ variant='ghost'
+ color='none'
+ className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
+ >
+
+ Delete
+
+
+
+
+
);
};
const BanksTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -87,10 +113,17 @@ const BanksTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
- initial: { search: '', nameSort: '' },
- paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' },
+ initial: {
+ search: searchValue,
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ },
});
+ const [sorting, setSorting] = useState([]);
+
const {
data: banks,
isLoading,
@@ -101,78 +134,21 @@ const BanksTable = () => {
);
const deleteModal = useModal();
-
const [selectedBank, setSelectedBank] = useState(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
- const [sorting, setSorting] = useState([]);
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
- const banksColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama',
- },
- {
- accessorKey: 'alias',
- header: 'Alias',
- },
- {
- accessorKey: 'account_number',
- header: 'No. Rekening',
- },
- {
- accessorKey: 'owner',
- header: 'Pemilik',
- cell: (props) => (props.getValue() ? props.getValue() : '-'),
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+ useEffect(() => {
+ setTableState('banks-table', pathname);
+ }, [pathname, setTableState]);
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
-
- const deleteClickHandler = () => {
- setSelectedBank(props.row.original);
- deleteModal.openModal();
- };
-
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ];
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
+ };
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -192,93 +168,145 @@ const BanksTable = () => {
setIsDeleteLoading(false);
};
- const searchChangeHandler: ChangeEventHandler = (e) => {
- updateFilter('search', e.target.value);
- };
+ const banksColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorKey: 'alias',
+ header: 'Alias',
+ },
+ {
+ accessorKey: 'account_number',
+ header: 'No. Rekening',
+ },
+ {
+ accessorKey: 'owner',
+ header: 'Pemilik',
+ cell: (props) => props.getValue() || '-',
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
- setPageSize(newVal.value as number);
- };
+ const deleteClickHandler = () => {
+ setSelectedBank(props.row.original);
+ deleteModal.openModal();
+ };
- // track sorting
- useEffect(() => {
- const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
-
- if (!isNameSorted) {
- updateFilter('nameSort', '');
- } else {
- updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
- }
- }, [sorting]);
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page, deleteModal]
+ );
return (
<>
-
-
-
-
-
-
-
- Tambah
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Bank
+
+
-
-
+
+ }
+ 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',
}}
- onChange={pageSizeChangeHandler}
- className={{ wrapper: 'max-w-28' }}
/>
-
- data={isResponseSuccess(banks) ? banks?.data : []}
- columns={banksColumns}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(banks) ? banks?.meta?.page : 0}
- totalItems={isResponseSuccess(banks) ? banks?.meta?.total_results : 0}
- onPageChange={setPage}
- isLoading={isLoading}
- sorting={sorting}
- setSorting={setSorting}
- className={{
- containerClassName: cn({
- 'mb-20': isResponseSuccess(banks) && banks?.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(banks) || banks.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ data={isResponseSuccess(banks) ? banks?.data : []}
+ columns={banksColumns}
+ pageSize={tableFilterState.pageSize}
+ page={isResponseSuccess(banks) ? banks?.meta?.page : 0}
+ totalItems={
+ isResponseSuccess(banks) ? banks?.meta?.total_results : 0
+ }
+ onPageChange={setPage}
+ onPageSizeChange={setPageSize}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: 'p-3 mb-0',
+ headerColumnClassName: 'text-nowrap',
+ }}
+ />
+ )}
+
[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default BankTableSkeleton;
diff --git a/src/components/pages/master-data/customer/CustomersTable.tsx b/src/components/pages/master-data/customer/CustomersTable.tsx
index e605d9f7..1f02428a 100644
--- a/src/components/pages/master-data/customer/CustomersTable.tsx
+++ b/src/components/pages/master-data/customer/CustomersTable.tsx
@@ -1,81 +1,111 @@
'use client';
-import Button from '@/components/Button';
+import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
+import useSWR from 'swr';
+import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
+import toast from 'react-hot-toast';
+
+import { Icon } from '@iconify/react';
+import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
+import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import Table from '@/components/Table';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
-import { ROWS_OPTIONS } from '@/config/constant';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { cn } from '@/lib/helper';
-import { CustomerApi } from '@/services/api/master-data';
-import { useTableFilter } from '@/services/hooks/useTableFilter';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import CustomerTableSkeleton from '@/components/pages/master-data/customer/skeleton/CustomerTableSkeleton';
+
import { Customer } from '@/types/api/master-data/customer';
-import { Icon } from '@iconify/react';
-import { CellContext, ColumnDef } from '@tanstack/react-table';
-import { useState } from 'react';
-import toast from 'react-hot-toast';
-import useSWR from 'swr';
+import { CustomerApi } from '@/services/api/master-data';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { useTableFilter } from '@/services/hooks/useTableFilter';
+import { usePathname } from 'next/navigation';
+import { useUiStore } from '@/stores/ui/ui.store';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
deleteClickHandler: () => void;
}) => {
+ const popoverId = `customer#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-customer#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
return (
-
-
-
-
- Detail
-
-
-
-
-
- Edit
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Detail
+
+
+
+
+
+ Edit
+
+
+
+ {
+ deleteClickHandler();
+ closePopover();
+ }}
+ variant='ghost'
+ color='none'
+ className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
+ >
+
+ Delete
+
+
+
+
+
);
};
const CustomersTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -83,16 +113,17 @@ const CustomersTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
- initial: { search: '', nameSort: '', picSort: '' },
+ initial: {
+ search: searchValue,
+ },
paramMap: {
page: 'page',
pageSize: 'limit',
- nameSort: 'sort_name',
- picSort: 'sort_pic',
},
});
- // Fetch Data
+ const [sorting, setSorting] = useState([]);
+
const {
data: customers,
isLoading,
@@ -102,87 +133,25 @@ const CustomersTable = () => {
CustomerApi.getAllFetcher
);
- // State
const deleteModal = useModal();
const [selectedCustomer, setSelectedCustomer] = useState<
Customer | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
- // Columns Definition
- const customersColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama',
- },
- {
- accessorKey: 'pic',
- header: 'PIC',
- cell: (props) => props.row.original.pic.name,
- },
- {
- accessorKey: 'type',
- header: 'Type',
- cell: (props) => props.row.original.type,
- },
- {
- accessorKey: 'phone',
- header: 'Phone',
- },
- {
- accessorKey: 'email',
- header: 'Email',
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+ useEffect(() => {
+ setTableState('customers-table', pathname);
+ }, [pathname, setTableState]);
- const deleteClickHandler = () => {
- setSelectedCustomer(props.row.original);
- deleteModal.openModal();
- };
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
+ };
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ];
-
- // Handler
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -202,81 +171,147 @@ const CustomersTable = () => {
toast.success('Successfully delete Customer!');
setIsDeleteLoading(false);
};
- const searchChangeHandler = (e: React.ChangeEvent) => {
- updateFilter('search', e.target.value);
- };
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
- setPageSize(newVal.value as number);
- };
+
+ const customersColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorFn: (row) => row.pic?.name ?? '-',
+ header: 'PIC',
+ },
+ {
+ accessorKey: 'type',
+ header: 'Type',
+ },
+ {
+ accessorKey: 'phone',
+ header: 'Phone',
+ },
+ {
+ accessorKey: 'email',
+ header: 'Email',
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ const deleteClickHandler = () => {
+ setSelectedCustomer(props.row.original);
+ deleteModal.openModal();
+ };
+
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page, deleteModal]
+ );
return (
<>
-
-
-
-
-
-
-
- Tambah
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Customer
+
+
-
-
+
+ }
+ 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',
}}
- onChange={pageSizeChangeHandler}
- className={{ wrapper: 'max-w-28' }}
/>
-
- data={isResponseSuccess(customers) ? customers?.data : []}
- columns={customersColumns}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(customers) ? customers?.meta?.page : 0}
- totalItems={
- isResponseSuccess(customers) ? customers?.meta?.total_results : 0
- }
- onPageChange={setPage}
- isLoading={isLoading}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(customers) && customers?.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(customers) || customers.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ data={customers?.data}
+ columns={customersColumns}
+ pageSize={tableFilterState.pageSize}
+ page={customers?.meta?.page ?? 0}
+ totalItems={customers?.meta?.total_results ?? 0}
+ onPageChange={setPage}
+ onPageSizeChange={setPageSize}
+ isLoading={false}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: 'p-3 mb-0',
+ headerColumnClassName: 'text-nowrap',
+ }}
+ />
+ )}
+
[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default CustomerTableSkeleton;
diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx
index dd6ebfe8..ed9f4007 100644
--- a/src/components/pages/master-data/flock/FlocksTable.tsx
+++ b/src/components/pages/master-data/flock/FlocksTable.tsx
@@ -1,91 +1,111 @@
'use client';
-import { CellContext, ColumnDef } from '@tanstack/react-table';
-import { Flock } from '@/types/api/master-data/flock';
-import { cn } from '@/lib/helper';
-import Button from '@/components/Button';
-import { Icon } from '@iconify/react';
-import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { useState } from 'react';
+import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
-import { FlockApi } from '@/services/api/master-data';
-import { useModal } from '@/components/Modal';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
-import RequirePermission from '@/components/helper/RequirePermission';
+import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
-import DebouncedTextInput from '@/components/input/DebouncedTextInput';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import { ROWS_OPTIONS } from '@/config/constant';
-import Table from '@/components/Table';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
-const RowsOptions = ({
- type = 'dropdown',
+import { Icon } from '@iconify/react';
+import Table from '@/components/Table';
+import DebouncedTextInput from '@/components/input/DebouncedTextInput';
+import Button from '@/components/Button';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import RequirePermission from '@/components/helper/RequirePermission';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import FlockTableSkeleton from '@/components/pages/master-data/flock/skeleton/FlockTableSkeleton';
+
+import { Flock } from '@/types/api/master-data/flock';
+import { FlockApi } from '@/services/api/master-data';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { useTableFilter } from '@/services/hooks/useTableFilter';
+import { usePathname } from 'next/navigation';
+import { useUiStore } from '@/stores/ui/ui.store';
+
+const RowOptionsMenu = ({
+ popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
deleteClickHandler: () => void;
}) => {
+ const popoverId = `flock#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-flock#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
return (
-
-
-
-
- Detail
-
-
-
-
-
- Edit
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Detail
+
+
+
+
+
+ Edit
+
+
+
+ {
+ deleteClickHandler();
+ closePopover();
+ }}
+ variant='ghost'
+ color='none'
+ className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
+ >
+
+ Delete
+
+
+
+
+
);
};
const FlockTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -93,15 +113,17 @@ const FlockTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
- initial: { search: '', nameSort: '' },
+ initial: {
+ search: searchValue,
+ },
paramMap: {
page: 'page',
pageSize: 'limit',
- nameSort: 'sort_name',
},
});
- // Fetch Data
+ const [sorting, setSorting] = useState([]);
+
const {
data: flocks,
isLoading,
@@ -111,74 +133,25 @@ const FlockTable = () => {
FlockApi.getAllFetcher
);
- // State
const deleteModal = useModal();
const [selectedFlock, setSelectedFlock] = useState(
undefined
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
- // Columns Definition
- const flocksColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama',
- },
- {
- accessorKey: 'created_at',
- header: 'Dibuat pada',
- cell: (props) =>
- new Date(props.row.original.created_at).toLocaleDateString(),
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+ useEffect(() => {
+ setTableState('flocks-table', pathname);
+ }, [pathname, setTableState]);
- const deleteClickHandler = () => {
- setSelectedFlock(props.row.original);
- deleteModal.openModal();
- };
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
+ };
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ];
-
- // Handler
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -196,85 +169,143 @@ const FlockTable = () => {
toast.success('Successfully delete Flock!');
setIsDeleteLoading(false);
};
- const searchChangeHandler = (e: React.ChangeEvent) => {
- updateFilter('search', e.target.value);
- };
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
- setPageSize(newVal.value as number);
- };
+
+ const flocksColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorKey: 'created_at',
+ header: 'Dibuat pada',
+ cell: (props) =>
+ new Date(props.row.original.created_at).toLocaleDateString('id-ID'),
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ const deleteClickHandler = () => {
+ setSelectedFlock(props.row.original);
+ deleteModal.openModal();
+ };
+
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page, deleteModal]
+ );
return (
<>
-
-
-
-
-
-
-
- Tambah
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Flock
+
+
-
-
+
+ }
+ 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',
}}
- onChange={pageSizeChangeHandler}
- className={{ wrapper: 'max-w-28' }}
/>
-
- data={isResponseSuccess(flocks) ? flocks?.data : []}
- columns={flocksColumns}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(flocks) ? flocks?.meta?.page : 0}
- totalItems={
- isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0
- }
- onPageChange={setPage}
- isLoading={isLoading}
- className={{
- containerClassName: cn({
- 'mb-20': isResponseSuccess(flocks) && flocks?.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(flocks) || flocks.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ data={flocks?.data}
+ columns={flocksColumns}
+ pageSize={tableFilterState.pageSize}
+ page={flocks?.meta?.page ?? 0}
+ totalItems={flocks?.meta?.total_results ?? 0}
+ onPageChange={setPage}
+ onPageSizeChange={setPageSize}
+ isLoading={false}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: 'p-3 mb-0',
+ headerColumnClassName: 'text-nowrap',
+ }}
+ />
+ )}
+
+
[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default FlockTableSkeleton;
diff --git a/src/components/pages/master-data/kandang/KandangsTable.tsx b/src/components/pages/master-data/kandang/KandangsTable.tsx
index 7d79d456..9d923cbe 100644
--- a/src/components/pages/master-data/kandang/KandangsTable.tsx
+++ b/src/components/pages/master-data/kandang/KandangsTable.tsx
@@ -1,90 +1,130 @@
'use client';
-import { ChangeEventHandler, useCallback, useEffect, useState } from 'react';
-import useSWR from 'swr';
import {
- CellContext,
- ColumnDef,
- ColumnSort,
- SortingState,
-} from '@tanstack/react-table';
+ ChangeEventHandler,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { usePathname } from 'next/navigation';
+import useSWR from 'swr';
+import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
+import { useFormik } from 'formik';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
-import { useModal } from '@/components/Modal';
+import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import KandangTableSkeleton from '@/components/pages/master-data/kandang/skeleton/KandangTableSkeleton';
+import SelectInput, { useSelect } from '@/components/input/SelectInput';
+import { OptionType } from '@/components/input/SelectInput';
+import ButtonFilter from '@/components/helper/ButtonFilter';
import { Kandang } from '@/types/api/master-data/kandang';
-import { KandangApi } from '@/services/api/master-data';
-import { cn, formatNumber } from '@/lib/helper';
+import { Location } from '@/types/api/master-data/location';
+import { KandangApi, LocationApi } from '@/services/api/master-data';
+import { UserApi } from '@/services/api/user';
+import { User } from '@/types/api/api-general';
+import { formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { ROWS_OPTIONS } from '@/config/constant';
+import { useUiStore } from '@/stores/ui/ui.store';
+import {
+ KandangFilterSchema,
+ KandangFilterType,
+} from '@/components/pages/master-data/kandang/filter/KandangFilter';
+import SelectInputRadio from '@/components/input/SelectInputRadio';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
deleteClickHandler: () => void;
}) => {
+ const popoverId = `kandang#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-kandang#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
return (
-
-
-
-
- Detail
-
-
+
+
+
+
-
-
-
- Edit
-
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+
+ Detail
+
+
+
+
+
+ Edit
+
+
+
+ {
+ deleteClickHandler();
+ closePopover();
+ }}
+ variant='ghost'
+ color='none'
+ className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
+ >
+
+ Delete
+
+
+
+
+
);
};
const KandangsTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -94,21 +134,112 @@ const KandangsTable = () => {
} = useTableFilter({
initial: {
search: '',
- nameSort: '',
- locationSort: '',
- capacitySort: '',
- picSort: '',
+ locationFilter: '',
+ picFilter: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
- nameSort: 'sort_name',
- locationSort: 'sort_location',
- capacitySort: 'sort_capacity',
- picSort: ' sort_pic',
+ locationFilter: 'location_id',
+ picFilter: 'pic_id',
},
});
+ // ===== FILTER MODAL STATE =====
+ const filterModal = useModal();
+
+ // ===== FORMIK SETUP =====
+ const formik = useFormik({
+ initialValues: {
+ location_id: null,
+ pic_id: null,
+ },
+ validationSchema: KandangFilterSchema,
+ onSubmit: (values, { setSubmitting }) => {
+ updateFilter('locationFilter', values.location_id || '');
+ updateFilter('picFilter', values.pic_id || '');
+ filterModal.closeModal();
+ setSubmitting(false);
+ },
+ onReset: () => {
+ updateFilter('locationFilter', '');
+ updateFilter('picFilter', '');
+ },
+ });
+
+ // ===== LOCATION OPTIONS =====
+ const {
+ setInputValue: setLocationInputValue,
+ options: locationOptions,
+ isLoadingOptions: isLoadingLocationOptions,
+ loadMore: loadMoreLocations,
+ } = useSelect(
+ filterModal.open ? LocationApi.basePath : null,
+ 'id',
+ 'name',
+ 'search'
+ );
+
+ // ===== PIC OPTIONS =====
+ const {
+ setInputValue: setPicInputValue,
+ options: picOptions,
+ isLoadingOptions: isLoadingPicOptions,
+ loadMore: loadMorePics,
+ } = useSelect(
+ filterModal.open ? UserApi.basePath : null,
+ 'id',
+ 'name',
+ 'search'
+ );
+
+ // ===== FILTER HANDLERS =====
+ const handleFilterLocationChange = useCallback(
+ (val: OptionType | OptionType[] | null) => {
+ const location = val as OptionType | null;
+ const locationId = location?.value ? String(location.value) : null;
+
+ formik.setFieldValue('location_id', locationId);
+ },
+ [formik]
+ );
+
+ const handleFilterPicChange = useCallback(
+ (val: OptionType | OptionType[] | null) => {
+ const pic = val as OptionType | null;
+ const picId = pic?.value ? String(pic.value) : null;
+
+ formik.setFieldValue('pic_id', picId);
+ },
+ [formik]
+ );
+
+ // ===== FILTER HELPERS =====
+ const locationIdValue = useMemo(() => {
+ if (!formik.values.location_id) return null;
+ return (
+ locationOptions.find(
+ (opt) => String(opt.value) === formik.values.location_id
+ ) || null
+ );
+ }, [formik.values.location_id, locationOptions]);
+
+ const picIdValue = useMemo(() => {
+ if (!formik.values.pic_id) return null;
+ return (
+ picOptions.find((opt) => String(opt.value) === formik.values.pic_id) ||
+ null
+ );
+ }, [formik.values.pic_id, picOptions]);
+
+ // ===== HANDLE FILTER MODAL OPEN =====
+ const handleFilterModalOpen = () => {
+ filterModal.openModal();
+ formik.validateForm();
+ };
+
+ const [sorting, setSorting] = useState([]);
+
const {
data: kandangs,
isLoading,
@@ -119,82 +250,23 @@ const KandangsTable = () => {
);
const deleteModal = useModal();
-
const [selectedKandang, setSelectedKandang] = useState(
undefined
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
- const [sorting, setSorting] = useState([]);
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
- const kandangsColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama',
- },
- {
- accessorKey: 'location',
- header: 'Lokasi',
- cell: (props) => props.row.original.location.name,
- },
- {
- accessorKey: 'capacity',
- header: 'Kapasitas',
- cell: (props) => formatNumber(props.row.original.capacity ?? 0),
- },
- {
- accessorKey: 'pic',
- header: 'PIC',
- cell: (props) => props.row.original.pic.name,
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+ useEffect(() => {
+ setTableState('kandangs-table', pathname);
+ }, [pathname, setTableState]);
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
-
- const deleteClickHandler = () => {
- setSelectedKandang(props.row.original);
- deleteModal.openModal();
- };
-
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ];
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
+ };
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -216,114 +288,150 @@ const KandangsTable = () => {
setIsDeleteLoading(false);
};
- const searchChangeHandler: ChangeEventHandler = (e) => {
- updateFilter('search', e.target.value);
- };
+ const kandangsColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorFn: (row) => row.location?.name ?? '-',
+ header: 'Lokasi',
+ },
+ {
+ accessorKey: 'capacity',
+ header: 'Kapasitas',
+ cell: (props) => formatNumber(props.row.original.capacity ?? 0),
+ },
+ {
+ accessorFn: (row) => row.pic?.name ?? '-',
+ header: 'PIC',
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
- setPageSize(newVal.value as number);
- };
+ const deleteClickHandler = () => {
+ setSelectedKandang(props.row.original);
+ deleteModal.openModal();
+ };
- const updateSortingFilter = useCallback(
- (
- sortName: Exclude,
- sortFilter: ColumnSort | undefined
- ) => {
- if (!sortFilter) {
- updateFilter(sortName, '');
- } else {
- updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
- }
- },
- [updateFilter]
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
- // track sorting
- useEffect(() => {
- const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name');
- const locationSortFilter = sorting.find(
- (sortItem) => sortItem.id === 'location'
- );
- const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic');
-
- updateSortingFilter('nameSort', nameSortFilter);
- updateSortingFilter('locationSort', locationSortFilter);
- updateSortingFilter('picSort', picSortFilter);
- }, [sorting, updateSortingFilter]);
-
return (
<>
-
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+
+ Add Kandang
+
+
-
-
+
+ }
+ 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',
}}
- onChange={pageSizeChangeHandler}
- className={{ wrapper: 'max-w-28' }}
+ />
+
+
-
- data={isResponseSuccess(kandangs) ? kandangs?.data : []}
- columns={kandangsColumns}
- pageSize={tableFilterState.pageSize}
- page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
- totalItems={
- isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
- }
- onPageChange={setPage}
- isLoading={isLoading}
- sorting={sorting}
- setSorting={setSorting}
- className={{
- containerClassName: cn({
- 'mb-20':
- isResponseSuccess(kandangs) && kandangs?.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(kandangs) || kandangs.data?.length === 0 ? (
+
+
+ }
+ />
+
+ ) : (
+
+ data={kandangs?.data}
+ columns={kandangsColumns}
+ pageSize={tableFilterState.pageSize}
+ page={kandangs?.meta?.page ?? 0}
+ totalItems={kandangs?.meta?.total_results ?? 0}
+ onPageChange={setPage}
+ onPageSizeChange={setPageSize}
+ isLoading={false}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: 'p-3 mb-0',
+ headerColumnClassName: 'text-nowrap',
+ }}
+ />
+ )}
+
{
onClick: confirmationModalDeleteClickHandler,
}}
/>
+
+ {/* Filter Modal */}
+
+ {/* Modal Header */}
+
+
+
+
Filter Data
+
+
+
+
+
+
+
>
);
};
diff --git a/src/components/pages/master-data/kandang/filter/KandangFilter.ts b/src/components/pages/master-data/kandang/filter/KandangFilter.ts
new file mode 100644
index 00000000..30132611
--- /dev/null
+++ b/src/components/pages/master-data/kandang/filter/KandangFilter.ts
@@ -0,0 +1,11 @@
+import { string, object } from 'yup';
+
+export const KandangFilterSchema = object().shape({
+ location_id: string().nullable(),
+ pic_id: string().nullable(),
+});
+
+export type KandangFilterType = {
+ location_id: string | null;
+ pic_id: string | null;
+};
diff --git a/src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx b/src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx
new file mode 100644
index 00000000..65a759ac
--- /dev/null
+++ b/src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx
@@ -0,0 +1,37 @@
+import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
+import Table from '@/components/Table';
+import { Kandang } from '@/types/api/master-data/kandang';
+import { ColumnDef } from '@tanstack/react-table';
+
+const KandangTableSkeleton = ({
+ columns,
+ icon,
+ title = 'No Data Available',
+ subtitle = 'There is no kandang data displayed. Enter kandang data to get started.',
+}: {
+ columns: ColumnDef[];
+ icon: React.ReactNode;
+ title?: string;
+ subtitle?: string;
+}) => {
+ return (
+
+ );
+};
+
+export default KandangTableSkeleton;
diff --git a/src/components/pages/master-data/location/LocationsTable.tsx b/src/components/pages/master-data/location/LocationsTable.tsx
index a35ffd09..89a00539 100644
--- a/src/components/pages/master-data/location/LocationsTable.tsx
+++ b/src/components/pages/master-data/location/LocationsTable.tsx
@@ -1,90 +1,126 @@
'use client';
-import { ChangeEventHandler, useCallback, useEffect, useState } from 'react';
-import useSWR from 'swr';
import {
- CellContext,
- ColumnDef,
- ColumnSort,
- SortingState,
-} from '@tanstack/react-table';
+ ChangeEventHandler,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { usePathname } from 'next/navigation';
+import useSWR from 'swr';
+import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast';
+import { useFormik } from 'formik';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Button from '@/components/Button';
-import { useModal } from '@/components/Modal';
+import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import RowDropdownOptions from '@/components/table/RowDropdownOptions';
-import RowCollapseOptions from '@/components/table/RowCollapseOptions';
-import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
+import PopoverButton from '@/components/popover/PopoverButton';
+import PopoverContent from '@/components/popover/PopoverContent';
+import LocationTableSkeleton from '@/components/pages/master-data/location/skeleton/LocationTableSkeleton';
+import SelectInput, { useSelect } from '@/components/input/SelectInput';
+import { OptionType } from '@/components/input/SelectInput';
+import ButtonFilter from '@/components/helper/ButtonFilter';
import { Location } from '@/types/api/master-data/location';
-import { LocationApi } from '@/services/api/master-data';
-import { cn } from '@/lib/helper';
+import { Area } from '@/types/api/master-data/area';
+import { LocationApi, AreaApi } from '@/services/api/master-data';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
-import { ROWS_OPTIONS } from '@/config/constant';
+import { useUiStore } from '@/stores/ui/ui.store';
+import {
+ LocationFilterSchema,
+ LocationFilterType,
+} from '@/components/pages/master-data/location/filter/LocationFilter';
const RowOptionsMenu = ({
- type = 'dropdown',
+ popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
- type: 'dropdown' | 'collapse';
+ popoverPosition: 'bottom' | 'top';
props: CellContext;
deleteClickHandler: () => void;
}) => {
+ const popoverId = `location#${props.row.original.id}`;
+ const popoverAnchorName = `--anchor-location#${props.row.original.id}`;
+
+ const closePopover = () => {
+ document.getElementById(popoverId)?.hidePopover();
+ };
+
return (
-
-
-
-
- Detail
-
-
+
+
+
+
-
-
-
- Edit
-
-
-
-
-
-
- Delete
-
-
-
+
+
+
+
+
+ Detail
+
+
+
+
+
+ Edit
+
+
+
+ {
+ deleteClickHandler();
+ closePopover();
+ }}
+ variant='ghost'
+ color='none'
+ className='p-3 justify-start text-sm font-semibold w-full text-error hover:text-error'
+ >
+
+ Delete
+
+
+
+
+
);
};
const LocationsTable = () => {
+ const { searchValue, setSearchValue, setTableState } = useUiStore();
+ const pathname = usePathname();
+
const {
state: tableFilterState,
updateFilter,
@@ -92,16 +128,75 @@ const LocationsTable = () => {
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
- initial: { search: '', nameSort: '', addressSort: '', areaSort: '' },
+ initial: {
+ search: '',
+ areaFilter: '',
+ },
paramMap: {
page: 'page',
pageSize: 'limit',
- nameSort: 'sort_name',
- addressSort: 'sort_address',
- areaSort: ' sort_area',
+ areaFilter: 'area_id',
},
});
+ // ===== FILTER MODAL STATE =====
+ const filterModal = useModal();
+
+ // ===== FORMIK SETUP =====
+ const formik = useFormik({
+ initialValues: {
+ area_id: null,
+ },
+ validationSchema: LocationFilterSchema,
+ onSubmit: (values, { setSubmitting }) => {
+ updateFilter('areaFilter', values.area_id || '');
+ filterModal.closeModal();
+ setSubmitting(false);
+ },
+ onReset: () => {
+ updateFilter('areaFilter', '');
+ },
+ });
+
+ // ===== AREA OPTIONS =====
+ const {
+ setInputValue: setAreaInputValue,
+ options: areaOptions,
+ isLoadingOptions: isLoadingAreaOptions,
+ loadMore: loadMoreAreas,
+ } = useSelect(
+ filterModal.open ? AreaApi.basePath : null,
+ 'id',
+ 'name',
+ 'search'
+ );
+
+ // ===== FILTER HANDLERS =====
+ const handleFilterAreaChange = useCallback(
+ (val: OptionType | OptionType[] | null) => {
+ const area = val as OptionType | null;
+ const areaId = area?.value ? String(area.value) : null;
+
+ formik.setFieldValue('area_id', areaId);
+ },
+ [formik]
+ );
+
+ // ===== FILTER HELPERS =====
+ const areaIdValue = useMemo(() => {
+ if (!formik.values.area_id) return null;
+ return (
+ areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
+ null
+ );
+ }, [formik.values.area_id, areaOptions]);
+
+ // ===== HANDLE FILTER MODAL OPEN =====
+ const handleFilterModalOpen = () => {
+ filterModal.openModal();
+ formik.validateForm();
+ };
+
const {
data: locations,
isLoading,
@@ -112,76 +207,25 @@ const LocationsTable = () => {
);
const deleteModal = useModal();
-
const [selectedLocation, setSelectedLocation] = useState<
Location | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ useEffect(() => {
+ updateFilter('search', searchValue);
+ }, [searchValue, updateFilter]);
+
+ useEffect(() => {
+ setTableState('locations-table', pathname);
+ }, [pathname, setTableState]);
+
const [sorting, setSorting] = useState([]);
- const locationsColumns: ColumnDef[] = [
- {
- header: '#',
- cell: (props) =>
- tableFilterState.pageSize * (tableFilterState.page - 1) +
- props.row.index +
- 1,
- },
- {
- accessorKey: 'name',
- header: 'Nama',
- },
- {
- accessorKey: 'address',
- header: 'Alamat',
- },
- {
- accessorKey: 'area',
- header: 'Area',
- cell: (props) => props.row.original.area.name,
- },
- {
- header: 'Aksi',
- cell: (props) => {
- const currentPageSize = props.table.getPaginationRowModel().rows.length;
- const currentPageRows = props.table.getPaginationRowModel().flatRows;
- const currentRowRelativeIndex =
- currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
-
- const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
-
- const deleteClickHandler = () => {
- setSelectedLocation(props.row.original);
- deleteModal.openModal();
- };
-
- return (
- <>
- {currentPageSize > 2 && (
-
-
-
- )}
-
- {currentPageSize <= 2 && (
-
-
-
- )}
- >
- );
- },
- },
- ];
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ setSearchValue(e.target.value);
+ updateFilter('search', e.target.value);
+ };
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
@@ -203,114 +247,145 @@ const LocationsTable = () => {
setIsDeleteLoading(false);
};
- const searchChangeHandler: ChangeEventHandler = (e) => {
- updateFilter('search', e.target.value);
- };
+ const locationsColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ header: 'No',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorKey: 'address',
+ header: 'Alamat',
+ },
+ {
+ accessorFn: (row) => row.area?.name ?? '-',
+ header: 'Area',
+ },
+ {
+ header: 'Aksi',
+ cell: (props: CellContext) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
- setPageSize(newVal.value as number);
- };
+ const deleteClickHandler = () => {
+ setSelectedLocation(props.row.original);
+ deleteModal.openModal();
+ };
- const updateSortingFilter = useCallback(
- (
- sortName: Exclude,
- sortFilter: ColumnSort | undefined
- ) => {
- if (!sortFilter) {
- updateFilter(sortName, '');
- } else {
- updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc');
- }
- },
- [updateFilter]
+ return (
+
+ );
+ },
+ },
+ ],
+ [tableFilterState.pageSize, tableFilterState.page, deleteModal]
);
- // track sorting
- useEffect(() => {
- const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name');
- const addressSortFilter = sorting.find(
- (sortItem) => sortItem.id === 'address'
- );
- const areaSortFilter = sorting.find((sortItem) => sortItem.id === 'area');
-
- updateSortingFilter('nameSort', nameSortFilter);
- updateSortingFilter('addressSort', addressSortFilter);
- updateSortingFilter('areaSort', areaSortFilter);
- }, [sorting, updateSortingFilter]);
-
return (
<>
-
-
-
-
-
-
+
+ {/* Header Section */}
+
+ {/* Action Buttons */}
+
+
+
+