Merge branch 'development' into 'production'

Development

See merge request mbugroup/lti-web-client!432
This commit is contained in:
Adnan Zahir
2026-04-25 14:46:53 +07:00
29 changed files with 1566 additions and 632 deletions
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
@@ -10,16 +10,14 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent'; import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
interface ExpenseDetailProps { interface ExpenseDetailProps {
initialValues?: Expense; initialValues?: Expense;
} }
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => { const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
const router = useRouter();
const [activeTab, setActiveTab] = useState<string>('request'); const [activeTab, setActiveTab] = useState<string>('request');
const searchParams = useSearchParams();
const returnTo = getExpenseListReturnTo(searchParams);
const expenseDetailTabs = useMemo(() => { const expenseDetailTabs = useMemo(() => {
const validTabs = [ const validTabs = [
@@ -50,8 +48,8 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
<section className='w-full max-w-full pb-16'> <section className='w-full max-w-full pb-16'>
<header className='flex flex-col gap-4'> <header className='flex flex-col gap-4'>
<Button <Button
href={returnTo}
variant='link' variant='link'
onClick={router.back}
className='w-fit p-0 text-primary' className='w-fit p-0 text-primary'
> >
<Icon icon='uil:arrow-left' width={24} height={24} /> <Icon icon='uil:arrow-left' width={24} height={24} />
@@ -411,7 +411,7 @@ const ExpensesTable = () => {
}, },
{ {
accessorFn: (row) => row.supplier.name ?? '-', accessorFn: (row) => row.supplier.name ?? '-',
header: 'Vendor', header: 'Uraian',
}, },
{ {
accessorKey: 'grand_total', accessorKey: 'grand_total',
+101 -75
View File
@@ -39,6 +39,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import ButtonFilter from '@/components/helper/ButtonFilter';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import { import {
FinanceTableFilterSchema, FinanceTableFilterSchema,
@@ -195,6 +196,9 @@ const FinanceTable = () => {
sortBy: '', sortBy: '',
startDate: '', startDate: '',
endDate: '', endDate: '',
bankNames: '',
customerNames: '',
supplierNames: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -207,6 +211,9 @@ const FinanceTable = () => {
startDate: 'start_date', startDate: 'start_date',
endDate: 'end_date', endDate: 'end_date',
}, },
excludeKeysFromUrl: ['bankNames', 'customerNames', 'supplierNames'],
persist: true,
storeName: 'finance-table',
}); });
// ===== FILTER MODAL STATE ===== // ===== FILTER MODAL STATE =====
@@ -256,9 +263,20 @@ const FinanceTable = () => {
updateFilter('sortBy', values.sort_by); updateFilter('sortBy', values.sort_by);
updateFilter('startDate', values.start_date); updateFilter('startDate', values.start_date);
updateFilter('endDate', values.end_date); updateFilter('endDate', values.end_date);
// Save display names for restoration on modal reopen
const toNames = (val: OptionType | OptionType[] | null) =>
val ? (Array.isArray(val) ? val : [val]).map((o) => String(o.label)).join(',') : '';
updateFilter('bankNames', toNames(selectedBank));
updateFilter('customerNames', toNames(selectedCustomerId));
updateFilter('supplierNames', toNames(selectedSupplierId));
filterModal.closeModal(); filterModal.closeModal();
}, },
onReset: () => { onReset: () => {
setSelectedTransactionType(null);
setSelectedBank(null);
setSelectedCustomerId(null);
setSelectedSupplierId(null);
setSelectedSortBy(null);
updateFilter('search', ''); updateFilter('search', '');
resetSearchValue(); resetSearchValue();
updateFilter('transactionTypes', ''); updateFilter('transactionTypes', '');
@@ -268,6 +286,10 @@ const FinanceTable = () => {
updateFilter('sortBy', ''); updateFilter('sortBy', '');
updateFilter('startDate', ''); updateFilter('startDate', '');
updateFilter('endDate', ''); updateFilter('endDate', '');
updateFilter('bankNames', '');
updateFilter('customerNames', '');
updateFilter('supplierNames', '');
filterModal.closeModal();
}, },
}); });
@@ -320,31 +342,6 @@ const FinanceTable = () => {
}); });
}, [bankOptions, bankRawData]); }, [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 ===== // ===== Handler =====
const searchChangeHandler = useCallback( const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -469,28 +466,73 @@ const FinanceTable = () => {
}; };
const handleFilterModalOpen = () => { const handleFilterModalOpen = () => {
// Restore transaction types from stored comma-separated IDs
const txIds = tableFilterState.transactionTypes
? tableFilterState.transactionTypes.split(',')
: [];
const restoredTxTypes = FINANCE_TRANSACTION_TYPE_OPTIONS.filter((opt) =>
txIds.includes(String(opt.value))
);
setSelectedTransactionType(restoredTxTypes.length ? restoredTxTypes : null);
// Restore banks from stored IDs and names
const bankIdList = tableFilterState.bankIds
? tableFilterState.bankIds.split(',')
: [];
const bankNameList = tableFilterState.bankNames
? tableFilterState.bankNames.split(',')
: [];
const restoredBanks = bankIdList.map((id, i) => ({
value: id,
label: bankNameList[i] || id,
}));
setSelectedBank(restoredBanks.length ? restoredBanks : null);
// Restore customers from stored IDs and names
const customerIdList = tableFilterState.customerIds
? tableFilterState.customerIds.split(',')
: [];
const customerNameList = tableFilterState.customerNames
? tableFilterState.customerNames.split(',')
: [];
const restoredCustomers = customerIdList.map((id, i) => ({
value: id,
label: customerNameList[i] || id,
}));
setSelectedCustomerId(restoredCustomers.length ? restoredCustomers : null);
// Restore suppliers from stored IDs and names
const supplierIdList = tableFilterState.supplierIds
? tableFilterState.supplierIds.split(',')
: [];
const supplierNameList = tableFilterState.supplierNames
? tableFilterState.supplierNames.split(',')
: [];
const restoredSuppliers = supplierIdList.map((id, i) => ({
value: id,
label: supplierNameList[i] || id,
}));
setSelectedSupplierId(restoredSuppliers.length ? restoredSuppliers : null);
// Restore sort by
const restoredSortBy =
sortByOptions.find((opt) => String(opt.value) === tableFilterState.sortBy) ||
null;
setSelectedSortBy(restoredSortBy);
// Restore formik values
filterFormik.setValues({
search: tableFilterState.search || '',
transaction_types: tableFilterState.transactionTypes || '',
bank_ids: tableFilterState.bankIds || '',
customer_ids: tableFilterState.customerIds || '',
supplier_ids: tableFilterState.supplierIds || '',
sort_by: tableFilterState.sortBy || '',
start_date: tableFilterState.startDate || '',
end_date: tableFilterState.endDate || '',
});
filterModal.openModal(); filterModal.openModal();
filterFormik.validateForm();
};
const resetFilterHandler = () => {
setSelectedTransactionType(null);
setSelectedBank(null);
setSelectedCustomerId(null);
setSelectedSupplierId(null);
setSelectedSortBy(null);
filterFormik.resetForm();
updateFilter('search', '');
resetSearchValue();
updateFilter('transactionTypes', '');
updateFilter('bankIds', '');
updateFilter('customerIds', '');
updateFilter('supplierIds', '');
updateFilter('sortBy', '');
updateFilter('startDate', '');
updateFilter('endDate', '');
}; };
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
@@ -687,25 +729,19 @@ const FinanceTable = () => {
}} }}
/> />
<Button <ButtonFilter
variant='outline' values={tableFilterState}
color='none' excludeFields={[
'page',
'pageSize',
'search',
'bankNames',
'customerNames',
'supplierNames',
]}
onClick={handleFilterModalOpen} onClick={handleFilterModalOpen}
className={cn( className='px-3 py-2.5'
'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': hasFilters,
}
)}
>
<Icon icon='heroicons:funnel' width={20} height={20} />
Filter
{hasFilters && (
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
{activeFiltersCount}
</span>
)}
</Button>
</div> </div>
</div> </div>
@@ -874,19 +910,9 @@ const FinanceTable = () => {
{/* Modal Footer */} {/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'> <div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button <Button
type='button' type='reset'
variant='soft' variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2' className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
onClick={() => {
filterFormik.resetForm();
setSelectedTransactionType(null);
setSelectedBank(null);
setSelectedCustomerId(null);
setSelectedSupplierId(null);
setSelectedSortBy(null);
resetFilterHandler();
filterModal.closeModal();
}}
> >
Reset Filter Reset Filter
</Button> </Button>
@@ -119,6 +119,8 @@ const InventoryAdjustmentTable = () => {
productFilter: '', productFilter: '',
warehouseFilter: '', warehouseFilter: '',
transactionTypeFilter: '', transactionTypeFilter: '',
productName: '',
warehouseName: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -131,6 +133,9 @@ const InventoryAdjustmentTable = () => {
warehouseFilter: 'warehouse_id', warehouseFilter: 'warehouse_id',
transactionTypeFilter: 'transaction_type', transactionTypeFilter: 'transaction_type',
}, },
excludeKeysFromUrl: ['productName', 'warehouseName'],
persist: true,
storeName: 'inventory-adjustment-table',
}); });
// ===== FILTER MODAL STATE ===== // ===== FILTER MODAL STATE =====
@@ -140,14 +145,16 @@ const InventoryAdjustmentTable = () => {
const formik = useFormik<AdjustmentFilterType>({ const formik = useFormik<AdjustmentFilterType>({
initialValues: { initialValues: {
product_id: null, product_id: null,
warehouse: null, warehouse_id: null,
transaction_type: null, transaction_type: null,
}, },
validationSchema: AdjustmentFilterSchema, validationSchema: AdjustmentFilterSchema,
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
updateFilter('productFilter', values.product_id || ''); updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', String(values.warehouse?.value) || ''); updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('transactionTypeFilter', values.transaction_type || ''); updateFilter('transactionTypeFilter', values.transaction_type || '');
updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : '');
updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : '');
filterModal.closeModal(); filterModal.closeModal();
setSubmitting(false); setSubmitting(false);
}, },
@@ -155,6 +162,9 @@ const InventoryAdjustmentTable = () => {
updateFilter('productFilter', ''); updateFilter('productFilter', '');
updateFilter('warehouseFilter', ''); updateFilter('warehouseFilter', '');
updateFilter('transactionTypeFilter', ''); updateFilter('transactionTypeFilter', '');
updateFilter('productName', '');
updateFilter('warehouseName', '');
filterModal.closeModal();
}, },
}); });
@@ -205,7 +215,8 @@ const InventoryAdjustmentTable = () => {
const handleFilterWarehouseChange = ( const handleFilterWarehouseChange = (
val: OptionType | OptionType[] | null val: OptionType | OptionType[] | null
) => { ) => {
formik.setFieldValue('warehouse', val); const warehouse = val as OptionType | null;
formik.setFieldValue('warehouse_id', warehouse?.value ? String(warehouse.value) : null);
}; };
const handleFilterTransactionTypeChange = useCallback( const handleFilterTransactionTypeChange = useCallback(
@@ -220,12 +231,27 @@ const InventoryAdjustmentTable = () => {
// ===== FILTER HELPERS ===== // ===== FILTER HELPERS =====
const productIdValue = useMemo(() => { const productIdValue = useMemo(() => {
if (!formik.values.product_id) return null; if (!formik.values.product_id) return null;
return ( const found = productOptions.find(
productOptions.find( (opt) => String(opt.value) === formik.values.product_id
(opt) => String(opt.value) === formik.values.product_id
) || null
); );
}, [formik.values.product_id, productOptions]); if (found) return found;
if (tableFilterState.productName) {
return { value: formik.values.product_id, label: tableFilterState.productName };
}
return null;
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
const warehouseIdValue = useMemo(() => {
if (!formik.values.warehouse_id) return null;
const found = warehouseOptions.find(
(opt) => String(opt.value) === formik.values.warehouse_id
);
if (found) return found;
if (tableFilterState.warehouseName) {
return { value: formik.values.warehouse_id, label: tableFilterState.warehouseName };
}
return null;
}, [formik.values.warehouse_id, warehouseOptions, tableFilterState.warehouseName]);
const transactionTypeValue = useMemo(() => { const transactionTypeValue = useMemo(() => {
if (!formik.values.transaction_type) return null; if (!formik.values.transaction_type) return null;
@@ -238,8 +264,12 @@ const InventoryAdjustmentTable = () => {
// ===== HANDLE FILTER MODAL OPEN ===== // ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => { const handleFilterModalOpen = () => {
formik.setValues({
product_id: tableFilterState.productFilter || null,
warehouse_id: tableFilterState.warehouseFilter || null,
transaction_type: tableFilterState.transactionTypeFilter || null,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
const { const {
@@ -507,6 +537,8 @@ const InventoryAdjustmentTable = () => {
'productSort', 'productSort',
'warehouseSort', 'warehouseSort',
'stockSort', 'stockSort',
'productName',
'warehouseName',
]} ]}
onClick={handleFilterModalOpen} onClick={handleFilterModalOpen}
className='px-3 py-2.5' className='px-3 py-2.5'
@@ -608,7 +640,7 @@ const InventoryAdjustmentTable = () => {
label='Gudang' label='Gudang'
placeholder='Pilih Gudang' placeholder='Pilih Gudang'
options={warehouseOptions} options={warehouseOptions}
value={formik.values.warehouse} value={warehouseIdValue}
onChange={handleFilterWarehouseChange} onChange={handleFilterWarehouseChange}
onInputChange={setWarehouseInputValue} onInputChange={setWarehouseInputValue}
isLoading={isLoadingWarehouseOptions} isLoading={isLoadingWarehouseOptions}
@@ -630,13 +662,9 @@ const InventoryAdjustmentTable = () => {
{/* Modal Footer */} {/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'> <div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button <Button
type='button' type='reset'
variant='soft' variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2' className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
onClick={() => {
formik.resetForm();
filterModal.closeModal();
}}
> >
Reset Filter Reset Filter
</Button> </Button>
@@ -1,5 +1,4 @@
import { string, object } from 'yup'; import { string, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
export const AdjustmentFilterSchema = object().shape({ export const AdjustmentFilterSchema = object().shape({
product_id: string().nullable(), product_id: string().nullable(),
@@ -9,6 +8,6 @@ export const AdjustmentFilterSchema = object().shape({
export type AdjustmentFilterType = { export type AdjustmentFilterType = {
product_id: string | null; product_id: string | null;
warehouse_id: string | null;
transaction_type: string | null; transaction_type: string | null;
warehouse: OptionType<number> | null;
}; };
@@ -122,6 +122,8 @@ const MovementTable = () => {
search: '', search: '',
productFilter: '', productFilter: '',
warehouseFilter: '', warehouseFilter: '',
productName: '',
warehouseName: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -129,6 +131,9 @@ const MovementTable = () => {
productFilter: 'product_id', productFilter: 'product_id',
warehouseFilter: 'warehouse_id', warehouseFilter: 'warehouse_id',
}, },
excludeKeysFromUrl: ['productName', 'warehouseName'],
persist: true,
storeName: 'movement-table',
}); });
// ===== FILTER MODAL STATE ===== // ===== FILTER MODAL STATE =====
@@ -144,12 +149,17 @@ const MovementTable = () => {
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
updateFilter('productFilter', values.product_id || ''); updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', values.warehouse_id || ''); updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : '');
updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : '');
filterModal.closeModal(); filterModal.closeModal();
setSubmitting(false); setSubmitting(false);
}, },
onReset: () => { onReset: () => {
updateFilter('productFilter', ''); updateFilter('productFilter', '');
updateFilter('warehouseFilter', ''); updateFilter('warehouseFilter', '');
updateFilter('productName', '');
updateFilter('warehouseName', '');
filterModal.closeModal();
}, },
}); });
@@ -201,26 +211,35 @@ const MovementTable = () => {
// ===== FILTER HELPERS ===== // ===== FILTER HELPERS =====
const productIdValue = useMemo(() => { const productIdValue = useMemo(() => {
if (!formik.values.product_id) return null; if (!formik.values.product_id) return null;
return ( const found = productOptions.find(
productOptions.find( (opt) => String(opt.value) === formik.values.product_id
(opt) => String(opt.value) === formik.values.product_id
) || null
); );
}, [formik.values.product_id, productOptions]); if (found) return found;
if (tableFilterState.productName) {
return { value: formik.values.product_id, label: tableFilterState.productName };
}
return null;
}, [formik.values.product_id, productOptions, tableFilterState.productName]);
const warehouseIdValue = useMemo(() => { const warehouseIdValue = useMemo(() => {
if (!formik.values.warehouse_id) return null; if (!formik.values.warehouse_id) return null;
return ( const found = warehouseOptions.find(
warehouseOptions.find( (opt) => String(opt.value) === formik.values.warehouse_id
(opt) => String(opt.value) === formik.values.warehouse_id
) || null
); );
}, [formik.values.warehouse_id, warehouseOptions]); if (found) return found;
if (tableFilterState.warehouseName) {
return { value: formik.values.warehouse_id, label: tableFilterState.warehouseName };
}
return null;
}, [formik.values.warehouse_id, warehouseOptions, tableFilterState.warehouseName]);
// ===== HANDLE FILTER MODAL OPEN ===== // ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => { const handleFilterModalOpen = () => {
formik.setValues({
product_id: tableFilterState.productFilter || null,
warehouse_id: tableFilterState.warehouseFilter || null,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
@@ -384,7 +403,7 @@ const MovementTable = () => {
<ButtonFilter <ButtonFilter
values={tableFilterState} values={tableFilterState}
excludeFields={['page', 'pageSize', 'search']} excludeFields={['page', 'pageSize', 'search', 'productName', 'warehouseName']}
onClick={handleFilterModalOpen} onClick={handleFilterModalOpen}
className='px-3 py-2.5' className='px-3 py-2.5'
/> />
@@ -489,13 +508,9 @@ const MovementTable = () => {
{/* Modal Footer */} {/* Modal Footer */}
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'> <div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button <Button
type='button' type='reset'
variant='soft' variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2' className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
onClick={() => {
formik.resetForm();
filterModal.closeModal();
}}
> >
Reset Filter Reset Filter
</Button> </Button>
@@ -4,17 +4,25 @@ import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import Table from '@/components/Table'; import Table from '@/components/Table';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import ButtonFilter from '@/components/helper/ButtonFilter';
import Modal, { useModal } from '@/components/Modal';
import SelectInput, { useSelect } from '@/components/input/SelectInput';
import { OptionType } from '@/components/input/SelectInput';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { cn, formatCurrency, formatNumber } from '@/lib/helper';
import { InventoryProductApi } from '@/services/api/inventory'; import { InventoryProductApi } from '@/services/api/inventory';
import { ProductCategoryApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import { InventoryProduct } from '@/types/api/inventory/product'; import { InventoryProduct } from '@/types/api/inventory/product';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import { useFormik } from 'formik';
import { object, string } from 'yup';
import PopoverButton from '@/components/popover/PopoverButton'; import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent'; import PopoverContent from '@/components/popover/PopoverContent';
import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton'; import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton';
@@ -83,13 +91,76 @@ const InventoryProductTable = () => {
} = useTableFilter({ } = useTableFilter({
initial: { initial: {
search: '', search: '',
categoryFilter: '',
categoryName: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
categoryFilter: 'product_category_id',
},
excludeKeysFromUrl: ['categoryName'],
persist: true,
storeName: 'inventory-product-table',
});
// ===== FILTER MODAL STATE =====
const filterModal = useModal();
// ===== FORMIK SETUP =====
const formik = useFormik<{ category_id: string | null }>({
initialValues: { category_id: null },
validationSchema: object().shape({ category_id: string().nullable() }),
onSubmit: (values, { setSubmitting }) => {
updateFilter('categoryFilter', values.category_id || '');
updateFilter('categoryName', categoryIdValue?.label ? String(categoryIdValue.label) : '');
filterModal.closeModal();
setSubmitting(false);
},
onReset: () => {
updateFilter('categoryFilter', '');
updateFilter('categoryName', '');
filterModal.closeModal();
}, },
}); });
// ===== CATEGORY OPTIONS =====
const {
setInputValue: setCategoryInputValue,
options: categoryOptions,
isLoadingOptions: isLoadingCategoryOptions,
loadMore: loadMoreCategories,
} = useSelect<ProductCategory>(
filterModal.open ? ProductCategoryApi.basePath : null,
'id',
'name',
'search'
);
// ===== FILTER HELPERS =====
const categoryIdValue = useMemo(() => {
if (!formik.values.category_id) return null;
const found = categoryOptions.find(
(opt) => String(opt.value) === formik.values.category_id
);
if (found) return found;
if (tableFilterState.categoryName) {
return { value: formik.values.category_id, label: tableFilterState.categoryName };
}
return null;
}, [formik.values.category_id, categoryOptions, tableFilterState.categoryName]);
// ===== HANDLE FILTER MODAL OPEN =====
const handleFilterModalOpen = () => {
formik.setValues({ category_id: tableFilterState.categoryFilter || null });
filterModal.openModal();
};
const handleFilterCategoryChange = (val: OptionType | OptionType[] | null) => {
const category = val as OptionType | null;
formik.setFieldValue('category_id', category?.value ? String(category.value) : null);
};
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const { data: inventoryProducts, isLoading } = useSWR( const { data: inventoryProducts, isLoading } = useSWR(
@@ -182,6 +253,7 @@ const InventoryProductTable = () => {
); );
return ( return (
<>
<div className='w-full'> <div className='w-full'>
{/* Header Section */} {/* Header Section */}
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'> <div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
@@ -199,7 +271,7 @@ const InventoryProductTable = () => {
</RequirePermission> </RequirePermission>
</div> </div>
{/* Search */} {/* Search and Filter */}
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'> <div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput <DebouncedTextInput
name='search' name='search'
@@ -216,6 +288,12 @@ const InventoryProductTable = () => {
'placeholder:font-semibold placeholder:text-base-content/50', 'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
/> />
<ButtonFilter
values={tableFilterState}
excludeFields={['page', 'pageSize', 'search', 'categoryName']}
onClick={handleFilterModalOpen}
className='px-3 py-2.5'
/>
</div> </div>
</div> </div>
@@ -272,6 +350,62 @@ const InventoryProductTable = () => {
)} )}
</div> </div>
</div> </div>
{/* Filter Modal */}
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
}}
>
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-medium text-sm'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={filterModal.closeModal}
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<div className='p-4 flex flex-col gap-1.5'>
<SelectInput
label='Kategori Produk'
placeholder='Pilih Kategori'
options={categoryOptions}
value={categoryIdValue}
onChange={handleFilterCategoryChange}
onInputChange={setCategoryInputValue}
isLoading={isLoadingCategoryOptions}
isClearable
onMenuScrollToBottom={loadMoreCategories}
className={{ wrapper: 'w-full' }}
/>
</div>
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
<Button
type='reset'
variant='soft'
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
>
Reset Filter
</Button>
<Button
type='submit'
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
disabled={!formik.isValid || formik.isSubmitting}
>
Apply Filter
</Button>
</div>
</form>
</Modal>
</>
); );
}; };
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject, useMemo } from 'react'; import { RefObject, useCallback, useMemo } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
@@ -17,22 +17,31 @@ import {
import { MarketingFilter } from '@/types/api/marketing/marketing'; import { MarketingFilter } from '@/types/api/marketing/marketing';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { MarketingApi } from '@/services/api/marketing/marketing'; import { MarketingApi } from '@/services/api/marketing/marketing';
import { CustomerApi } from '@/services/api/master-data'; import { CustomerApi, ProductApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlockApi } from '@/services/api/production';
import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlock } from '@/types/api/production/project-flock';
import { Product } from '@/types/api/master-data/product';
interface MarketingFilterModal { interface MarketingFilterModal {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: MarketingFilter) => void; onSubmit?: (values: MarketingFilter) => void;
onReset?: () => void; onReset?: () => void;
initialValues?: {
product_ids: OptionType<number>[];
status: OptionType<string> | null;
customer: OptionType<number> | null;
project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null;
};
} }
const MarketingFilterModal = ({ const MarketingFilterModal = ({
ref, ref,
onSubmit, onSubmit,
onReset, onReset,
initialValues,
}: MarketingFilterModal) => { }: MarketingFilterModal) => {
const closeModalHandler = () => { const closeModalHandler = () => {
ref.current?.close(); ref.current?.close();
@@ -40,36 +49,13 @@ const MarketingFilterModal = ({
// ===== OPTIONS ===== // ===== OPTIONS =====
const { const {
rawData: productsRawData, options: productsOptions,
isLoadingOptions: isLoadingProductsOptions, isLoadingOptions: isLoadingProductsOptions,
setInputValue: setProductsInputValue, setInputValue: setProductsInputValue,
loadMore: loadMoreProducts, loadMore: loadMoreProducts,
} = useSelect<BaseMarketing>( } = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
MarketingApi.basePath, include_all: 'true',
'id', });
'so_number',
'search'
);
const productsOptions = useMemo(() => {
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
const productsMap = new Map<number, { value: number; label: string }>();
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
const product = so.product_warehouse?.product;
if (product?.id && product?.name) {
productsMap.set(product.id, {
value: product.id,
label: product.name,
});
}
});
});
return Array.from(productsMap.values());
}, [productsRawData]);
const { const {
options: customersOptions, options: customersOptions,
@@ -102,7 +88,7 @@ const MarketingFilterModal = ({
]; ];
const formik = useFormik<MarketingFilterFormValues>({ const formik = useFormik<MarketingFilterFormValues>({
initialValues: { initialValues: initialValues || {
product_ids: [], product_ids: [],
status: null, status: null,
customer: null, customer: null,
@@ -114,11 +100,17 @@ const MarketingFilterModal = ({
onSubmit: async (values) => { onSubmit: async (values) => {
const formattedValues: MarketingFilter = { const formattedValues: MarketingFilter = {
product_ids: values.product_ids.map((item) => Number(item.value)), product_ids: values.product_ids.map((item) => Number(item.value)),
product_names: values.product_ids.map((item) => item.label),
status: values.status?.value.toString() || '', status: values.status?.value.toString() || '',
status_name: values.status?.label || '-',
customer_id: Number(values.customer?.value), customer_id: Number(values.customer?.value),
project_flock_id: Number(values.project_flock?.value) || undefined, customer_name: values.customer?.label || '-',
project_flock_id: values.project_flock?.value || undefined,
project_flock_name: values.project_flock?.label,
project_flock_kandang_id: project_flock_kandang_id:
Number(values.project_flock_kandang?.value) || undefined, Number(values.project_flock_kandang?.value) || undefined,
project_flock_kandang_name:
values.project_flock_kandang?.label || undefined,
}; };
onSubmit?.(formattedValues); onSubmit?.(formattedValues);
@@ -131,6 +123,22 @@ const MarketingFilterModal = ({
}, },
}); });
const { resetForm } = formik;
const formikResetHandler = useCallback(() => {
resetForm({
values: {
product_ids: [],
status: null,
customer: null,
project_flock: null,
project_flock_kandang: null,
},
});
onReset?.();
closeModalHandler();
}, [resetForm, onReset, closeModalHandler]);
const productChangeHandler = (val: OptionType | OptionType[] | null) => { const productChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('product_ids', val as OptionType[]); formik.setFieldValue('product_ids', val as OptionType[]);
}; };
@@ -176,7 +184,7 @@ const MarketingFilterModal = ({
> >
<form <form
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
onReset={formik.handleReset} onReset={formikResetHandler}
className='w-full flex flex-col' className='w-full flex flex-col'
> >
{/* Modal Header */} {/* Modal Header */}
@@ -189,10 +189,15 @@ const MarketingTable = () => {
initial: { initial: {
search: '', search: '',
product_ids: '', product_ids: '',
product_names: '',
status: '', status: '',
status_name: '',
customer_id: '', customer_id: '',
customer_name: '',
project_flock_id: '', project_flock_id: '',
project_flock_name: '',
project_flock_kandang_id: '', project_flock_kandang_id: '',
project_flock_kandang_name: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -203,6 +208,13 @@ const MarketingTable = () => {
project_flock_id: 'project_flock_id', project_flock_id: 'project_flock_id',
project_flock_kandang_id: 'project_flock_kandang_id', project_flock_kandang_id: 'project_flock_kandang_id',
}, },
excludeKeysFromUrl: [
'product_names',
'status_name',
'customer_name',
'project_flock_name',
'project_flock_kandang_name',
],
persist: true, persist: true,
storeName: 'marketing-table', storeName: 'marketing-table',
@@ -225,17 +237,21 @@ const MarketingTable = () => {
values.product_ids?.map((item) => item.toString()).join(','), values.product_ids?.map((item) => item.toString()).join(','),
true true
); );
updateFilter('product_names', values.product_names?.join(','));
updateFilter('status', values.status ? values.status.toString() : '', true); updateFilter('status', values.status ? values.status.toString() : '', true);
updateFilter('status_name', values.status_name, true);
updateFilter( updateFilter(
'customer_id', 'customer_id',
values.customer_id ? values.customer_id.toString() : '', values.customer_id ? values.customer_id.toString() : '',
true true
); );
updateFilter('customer_name', values.customer_name, true);
updateFilter( updateFilter(
'project_flock_id', 'project_flock_id',
values.project_flock_id ? values.project_flock_id.toString() : '', values.project_flock_id ? values.project_flock_id.toString() : '',
true true
); );
updateFilter('project_flock_name', values.project_flock_name ?? '', true);
updateFilter( updateFilter(
'project_flock_kandang_id', 'project_flock_kandang_id',
values.project_flock_kandang_id values.project_flock_kandang_id
@@ -243,6 +259,11 @@ const MarketingTable = () => {
: '', : '',
true true
); );
updateFilter(
'project_flock_kandang_name',
values.project_flock_kandang_name ?? '',
true
);
}; };
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
@@ -250,10 +271,15 @@ const MarketingTable = () => {
const filterResetHandler = () => { const filterResetHandler = () => {
updateFilter('product_ids', '', true); updateFilter('product_ids', '', true);
updateFilter('product_names', '', true);
updateFilter('status', '', true); updateFilter('status', '', true);
updateFilter('status_name', '', true);
updateFilter('customer_id', '', true); updateFilter('customer_id', '', true);
updateFilter('customer_name', '', true);
updateFilter('project_flock_id', '', true); updateFilter('project_flock_id', '', true);
updateFilter('project_flock_name', '', true);
updateFilter('project_flock_kandang_id', '', true); updateFilter('project_flock_kandang_id', '', true);
updateFilter('project_flock_kandang_name', '', true);
}; };
const approveClickHandler = () => { const approveClickHandler = () => {
@@ -333,6 +359,56 @@ const MarketingTable = () => {
? 'DELIVERY_ORDER' ? 'DELIVERY_ORDER'
: null; : null;
const marketingFilterInitialValues = useMemo(() => {
const productIds = tableFilterState.product_ids
? tableFilterState.product_ids
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
const productLabels = tableFilterState.product_names
? tableFilterState.product_names
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
return {
product_ids: productIds.map((value, idx) => ({
value: Number(value),
label: productLabels[idx] || '-',
})),
status: tableFilterState.status
? {
value: tableFilterState.status,
label: tableFilterState.status_name,
}
: null,
customer: tableFilterState.customer_id
? {
value: Number(tableFilterState.customer_id),
label: tableFilterState.customer_name,
}
: null,
project_flock: tableFilterState.project_flock_id
? {
value: Number(tableFilterState.project_flock_id),
label: tableFilterState.project_flock_name,
}
: null,
project_flock_kandang: tableFilterState.project_flock_kandang_id
? {
value: Number(tableFilterState.project_flock_kandang_id),
label: tableFilterState.project_flock_kandang_name,
}
: null,
};
}, [tableFilterState]);
const approveMarketingHandler = async (notes: string) => { const approveMarketingHandler = async (notes: string) => {
if (idsToProcess.length === 0) { if (idsToProcess.length === 0) {
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`); toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
@@ -735,7 +811,7 @@ const MarketingTable = () => {
</RequirePermission> </RequirePermission>
{idsToProcess.length > 0 && ( {idsToProcess.length > 0 && (
<> <>
<div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px'></div> <div className='divider divider-horizontal w-px p-0 m-0 bg-base-content/10 text-base-content/10 before:bg-base-content/10 before:w-px after:bg-base-content/10 after:w-px' />
<RequirePermission permissions='lti.marketing.sales_order.approve'> <RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button <Button
color='error' color='error'
@@ -749,7 +825,7 @@ const MarketingTable = () => {
width={20} width={20}
height={20} height={20}
/> />
Reject Reject ({idsToProcess.length} Item)
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.marketing.sales_order.approve'> <RequirePermission permissions='lti.marketing.sales_order.approve'>
@@ -765,7 +841,7 @@ const MarketingTable = () => {
width={20} width={20}
height={20} height={20}
/> />
Approve Approve ({idsToProcess.length} Item)
</Button> </Button>
</RequirePermission> </RequirePermission>
</> </>
@@ -774,7 +850,16 @@ const MarketingTable = () => {
<div className='flex flex-row gap-3'> <div className='flex flex-row gap-3'>
<ButtonFilter <ButtonFilter
values={tableFilterState} values={tableFilterState}
excludeFields={['page', 'pageSize', 'search']} excludeFields={[
'page',
'pageSize',
'search',
'product_names',
'status_name',
'customer_name',
'project_flock_name',
'project_flock_kandang_name',
]}
onClick={() => { onClick={() => {
filterModal.openModal(); filterModal.openModal();
}} }}
@@ -1146,6 +1231,7 @@ const MarketingTable = () => {
ref={filterModal.ref} ref={filterModal.ref}
onSubmit={filterSubmitHandler} onSubmit={filterSubmitHandler}
onReset={filterResetHandler} onReset={filterResetHandler}
initialValues={marketingFilterInitialValues}
/> />
</> </>
); );
@@ -172,6 +172,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
kandang_id: '', kandang_id: '',
category: '', category: '',
period: '', period: '',
area_name: '',
location_name: '',
kandang_name: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -183,7 +186,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
category: 'category', category: 'category',
period: 'period', period: 'period',
}, },
excludeKeysFromUrl: ['area_name', 'location_name', 'kandang_name'],
persist: true, persist: true,
storeName: 'project-flock-table', storeName: 'project-flock-table',
}); });
@@ -259,6 +262,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
updateFilter('kandang_id', values.kandang_id || ''); updateFilter('kandang_id', values.kandang_id || '');
updateFilter('category', values.category || ''); updateFilter('category', values.category || '');
updateFilter('period', values.period || ''); updateFilter('period', values.period || '');
updateFilter('area_name', areaValue?.label ? String(areaValue.label) : '');
updateFilter('location_name', locationValue?.label ? String(locationValue.label) : '');
updateFilter('kandang_name', kandangValue?.label ? String(kandangValue.label) : '');
filterModal.closeModal(); filterModal.closeModal();
setSubmitting(false); setSubmitting(false);
}, },
@@ -268,6 +274,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
updateFilter('kandang_id', ''); updateFilter('kandang_id', '');
updateFilter('category', ''); updateFilter('category', '');
updateFilter('period', ''); updateFilter('period', '');
updateFilter('area_name', '');
updateFilter('location_name', '');
updateFilter('kandang_name', '');
setFilterAreaId(undefined); setFilterAreaId(undefined);
setFilterLocationId(undefined); setFilterLocationId(undefined);
filterModal.closeModal(); filterModal.closeModal();
@@ -320,29 +329,37 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
// ===== FILTER HELPERS ===== // ===== FILTER HELPERS =====
const areaValue = useMemo(() => { const areaValue = useMemo(() => {
if (!formik.values.area_id) return null; if (!formik.values.area_id) return null;
return ( const found = areaOptions.find((opt) => String(opt.value) === formik.values.area_id);
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || if (found) return found;
null if (tableFilterState.area_name) {
); return { value: formik.values.area_id, label: tableFilterState.area_name };
}, [formik.values.area_id, areaOptions]); }
return null;
}, [formik.values.area_id, areaOptions, tableFilterState.area_name]);
const locationValue = useMemo(() => { const locationValue = useMemo(() => {
if (!formik.values.location_id) return null; if (!formik.values.location_id) return null;
return ( const found = locationOptions.find(
locationOptions.find( (opt) => String(opt.value) === formik.values.location_id
(opt) => String(opt.value) === formik.values.location_id
) || null
); );
}, [formik.values.location_id, locationOptions]); if (found) return found;
if (tableFilterState.location_name) {
return { value: formik.values.location_id, label: tableFilterState.location_name };
}
return null;
}, [formik.values.location_id, locationOptions, tableFilterState.location_name]);
const kandangValue = useMemo(() => { const kandangValue = useMemo(() => {
if (!formik.values.kandang_id) return null; if (!formik.values.kandang_id) return null;
return ( const found = kandangOptions.find(
kandangOptions.find( (opt) => String(opt.value) === formik.values.kandang_id
(opt) => String(opt.value) === formik.values.kandang_id
) || null
); );
}, [formik.values.kandang_id, kandangOptions]); if (found) return found;
if (tableFilterState.kandang_name) {
return { value: formik.values.kandang_id, label: tableFilterState.kandang_name };
}
return null;
}, [formik.values.kandang_id, kandangOptions, tableFilterState.kandang_name]);
const categoryValue = useMemo(() => { const categoryValue = useMemo(() => {
if (!formik.values.category) return null; if (!formik.values.category) return null;
@@ -967,7 +984,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<ButtonFilter <ButtonFilter
values={tableFilterState} values={tableFilterState}
excludeFields={['page', 'pageSize', 'search']} excludeFields={['page', 'pageSize', 'search', 'area_name', 'location_name', 'kandang_name']}
onClick={handleFilterModalOpen} onClick={handleFilterModalOpen}
className='px-3 py-2.5' className='px-3 py-2.5'
/> />
@@ -13,7 +13,6 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { OptionType, useSelect } from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput';
import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlockApi } from '@/services/api/production';
import { Flock } from '@/types/api/master-data/flock'; import { Flock } from '@/types/api/master-data/flock';
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
import { import {
TransferToLayingFilterSchema, TransferToLayingFilterSchema,
TransferToLayingFilterValues, TransferToLayingFilterValues,
@@ -21,12 +20,14 @@ import {
interface TransferToLayingFilterModal { interface TransferToLayingFilterModal {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: TransferToLayingFilter) => void; initialValues?: Partial<TransferToLayingFilterValues>;
onSubmit?: (values: TransferToLayingFilterValues) => void;
onReset?: () => void; onReset?: () => void;
} }
const TransferToLayingFilterModal = ({ const TransferToLayingFilterModal = ({
ref, ref,
initialValues: initialValuesProp,
onSubmit, onSubmit,
onReset, onReset,
}: TransferToLayingFilterModal) => { }: TransferToLayingFilterModal) => {
@@ -86,28 +87,16 @@ const TransferToLayingFilterModal = ({
const formik = useFormik<TransferToLayingFilterValues>({ const formik = useFormik<TransferToLayingFilterValues>({
initialValues: { initialValues: {
startDate: '', startDate: initialValuesProp?.startDate ?? '',
endDate: '', endDate: initialValuesProp?.endDate ?? '',
flockSource: [], flockSource: initialValuesProp?.flockSource ?? [],
flockDestination: [], flockDestination: initialValuesProp?.flockDestination ?? [],
status: [], status: initialValuesProp?.status ?? [],
}, },
enableReinitialize: true,
validationSchema: TransferToLayingFilterSchema, validationSchema: TransferToLayingFilterSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
const formattedValues = { onSubmit?.(values);
...values,
flockSource: values.flockSource
? (values.flockSource as OptionType[]).map((item) => item.value)
: [],
flockDestination: values.flockDestination
? (values.flockDestination as OptionType[]).map((item) => item.value)
: [],
status: values.status
? (values.status as OptionType[]).map((item) => item.value)
: [],
};
onSubmit?.(formattedValues as TransferToLayingFilter);
closeModalHandler(); closeModalHandler();
}, },
onReset: () => { onReset: () => {
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { ChangeEventHandler, useEffect, useState } from 'react'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -26,10 +26,9 @@ import TransferToLayingFilterModal from '@/components/pages/production/transfer-
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal'; import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton'; import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton';
import { import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
TransferToLaying, import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
TransferToLayingFilter, import { OptionType } from '@/components/input/SelectInput';
} from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { cn, formatDate, formatNumber } from '@/lib/helper'; import { cn, formatDate, formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -142,6 +141,8 @@ const TransferToLayingsTable = () => {
status: '', status: '',
filter_by: '', filter_by: '',
sort_by: '', sort_by: '',
flockSourceNames: '',
flockDestinationNames: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -154,6 +155,9 @@ const TransferToLayingsTable = () => {
filter_by: 'filter_by', filter_by: 'filter_by',
sort_by: 'sort_by', sort_by: 'sort_by',
}, },
excludeKeysFromUrl: ['flockSourceNames', 'flockDestinationNames'],
persist: true,
storeName: 'transfer-to-laying-table',
}); });
const { const {
@@ -431,12 +435,72 @@ const TransferToLayingsTable = () => {
updateFilter('search', e.target.value); updateFilter('search', e.target.value);
}; };
const filterSubmitHandler = (values: TransferToLayingFilter) => { const STATUS_FILTER_OPTIONS = [
updateFilter('startDate', values.startDate); { value: 'PENDING', label: 'Pengajuan' },
updateFilter('endDate', values.endDate); { value: 'APPROVED', label: 'Disetujui' },
updateFilter('flockSource', values.flockSource.join(',')); { value: 'REJECTED', label: 'Ditolak' },
updateFilter('flockDestination', values.flockDestination.join(',')); ];
updateFilter('status', values.status.join(','));
const filterModalInitialValues = useMemo(() => {
const flockSourceIds = tableFilterState.flockSource
? tableFilterState.flockSource.split(',')
: [];
const flockSourceNameList = tableFilterState.flockSourceNames
? tableFilterState.flockSourceNames.split(',')
: [];
const flockSourceOptions = flockSourceIds.filter(Boolean).map((id, i) => ({
value: parseInt(id),
label: flockSourceNameList[i] || id,
}));
const flockDestIds = tableFilterState.flockDestination
? tableFilterState.flockDestination.split(',')
: [];
const flockDestNameList = tableFilterState.flockDestinationNames
? tableFilterState.flockDestinationNames.split(',')
: [];
const flockDestOptions = flockDestIds.filter(Boolean).map((id, i) => ({
value: parseInt(id),
label: flockDestNameList[i] || id,
}));
const statusIds = tableFilterState.status
? tableFilterState.status.split(',')
: [];
const statusOptions = statusIds.filter(Boolean).map((id) => {
const found = STATUS_FILTER_OPTIONS.find((opt) => opt.value === id);
return found || { value: id, label: id };
});
return {
startDate: tableFilterState.startDate || '',
endDate: tableFilterState.endDate || '',
flockSource: flockSourceOptions,
flockDestination: flockDestOptions,
status: statusOptions,
};
}, [
tableFilterState.startDate,
tableFilterState.endDate,
tableFilterState.flockSource,
tableFilterState.flockDestination,
tableFilterState.status,
tableFilterState.flockSourceNames,
tableFilterState.flockDestinationNames,
]);
const filterSubmitHandler = (values: TransferToLayingFilterValues) => {
const flockSourceOpts = (values.flockSource as OptionType[]) || [];
const flockDestOpts = (values.flockDestination as OptionType[]) || [];
const statusOpts = (values.status as OptionType[]) || [];
updateFilter('startDate', values.startDate || '');
updateFilter('endDate', values.endDate || '');
updateFilter('flockSource', flockSourceOpts.map((o) => String(o.value)).join(','));
updateFilter('flockDestination', flockDestOpts.map((o) => String(o.value)).join(','));
updateFilter('status', statusOpts.map((o) => String(o.value)).join(','));
updateFilter('flockSourceNames', flockSourceOpts.map((o) => String(o.label)).join(','));
updateFilter('flockDestinationNames', flockDestOpts.map((o) => String(o.label)).join(','));
}; };
const filterResetHandler = () => { const filterResetHandler = () => {
@@ -445,6 +509,8 @@ const TransferToLayingsTable = () => {
updateFilter('flockSource', ''); updateFilter('flockSource', '');
updateFilter('flockDestination', ''); updateFilter('flockDestination', '');
updateFilter('status', ''); updateFilter('status', '');
updateFilter('flockSourceNames', '');
updateFilter('flockDestinationNames', '');
}; };
const exportToExcelHandler = async () => { const exportToExcelHandler = async () => {
@@ -558,6 +624,8 @@ const TransferToLayingsTable = () => {
'search', 'search',
'filter_by', 'filter_by',
'sort_by', 'sort_by',
'flockSourceNames',
'flockDestinationNames',
]} ]}
fieldGroups={[['startDate', 'endDate']]} fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal} onClick={filterModal.openModal}
@@ -670,6 +738,7 @@ const TransferToLayingsTable = () => {
<TransferToLayingFilterModal <TransferToLayingFilterModal
ref={filterModal.ref} ref={filterModal.ref}
initialValues={filterModalInitialValues}
onSubmit={filterSubmitHandler} onSubmit={filterSubmitHandler}
onReset={filterResetHandler} onReset={filterResetHandler}
/> />
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject, useState, useEffect, useMemo } from 'react'; import { RefObject, useState, useEffect, useMemo, useCallback } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -26,22 +26,32 @@ import { isResponseSuccess } from '@/lib/api-helper';
interface PurchaseFilterModalProps { interface PurchaseFilterModalProps {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
initialValues?: {
poDate: string;
category: OptionType<number>[];
status: OptionType<string>[];
supplier: OptionType<number> | null;
area: OptionType<number> | null;
location: OptionType<number> | null;
project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null;
};
onSubmit?: (values: PurchaseFilter) => void; onSubmit?: (values: PurchaseFilter) => void;
onReset?: () => void; onReset?: () => void;
} }
const PurchaseFilterModal = ({ const PurchaseFilterModal = ({
ref, ref,
initialValues,
onSubmit, onSubmit,
onReset, onReset,
}: PurchaseFilterModalProps) => { }: PurchaseFilterModalProps) => {
const closeModalHandler = () => { const closeModalHandler = useCallback(() => {
ref.current?.close(); ref.current?.close();
}; }, [ref]);
// ===== DATE ERROR STATE ===== // ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
// ===== CLEANUP TOAST ON UNMOUNT ===== // ===== CLEANUP TOAST ON UNMOUNT =====
useEffect(() => { useEffect(() => {
@@ -81,8 +91,12 @@ const PurchaseFilterModal = ({
'search' 'search'
); );
const [selectedAreaId, setSelectedAreaId] = useState(''); const [selectedAreaId, setSelectedAreaId] = useState(
const [selectedLocationId, setSelectedLocationId] = useState(''); initialValues?.area?.value ? String(initialValues.area.value) : ''
);
const [selectedLocationId, setSelectedLocationId] = useState(
initialValues?.location?.value ? String(initialValues.location.value) : ''
);
const { const {
setInputValue: setSupplierInputValue, setInputValue: setSupplierInputValue,
@@ -133,7 +147,8 @@ const PurchaseFilterModal = ({
project_flock: OptionType<number> | null; project_flock: OptionType<number> | null;
project_flock_kandang: OptionType<number> | null; project_flock_kandang: OptionType<number> | null;
}>({ }>({
initialValues: { // enableReinitialize: true,
initialValues: initialValues || {
poDate: '', poDate: '',
category: [], category: [],
status: [], status: [],
@@ -147,12 +162,18 @@ const PurchaseFilterModal = ({
const formattedValues = { const formattedValues = {
...values, ...values,
category: values.category.map((item) => String(item.value)), category: values.category.map((item) => String(item.value)),
category_labels: values.category,
status: values.status.map((item) => String(item.value)), status: values.status.map((item) => String(item.value)),
supplier_id: values.supplier?.value, supplier_id: values.supplier?.value,
supplier_label: values.supplier?.label,
area_id: values.area?.value, area_id: values.area?.value,
area_label: values.area?.label,
location_id: values.location?.value, location_id: values.location?.value,
location_label: values.location?.label,
project_flock_id: values.project_flock?.value, project_flock_id: values.project_flock?.value,
project_flock_label: values.project_flock?.label,
project_flock_kandang_id: values.project_flock_kandang?.value, project_flock_kandang_id: values.project_flock_kandang?.value,
project_flock_kandang_label: values.project_flock_kandang?.label,
}; };
onSubmit?.(formattedValues); onSubmit?.(formattedValues);
@@ -166,6 +187,17 @@ const PurchaseFilterModal = ({
}, },
}); });
const { resetForm, submitForm } = formik;
useEffect(() => {
setSelectedAreaId(
initialValues?.area?.value ? String(initialValues.area.value) : ''
);
setSelectedLocationId(
initialValues?.location?.value ? String(initialValues.location.value) : ''
);
}, [initialValues?.area, initialValues?.location]);
const projectFlockKandangOptions = useMemo(() => { const projectFlockKandangOptions = useMemo(() => {
if ( if (
!formik.values.project_flock || !formik.values.project_flock ||
@@ -197,6 +229,29 @@ const PurchaseFilterModal = ({
formik.setFieldValue('status', val); formik.setFieldValue('status', val);
}; };
const formikResetHandler = useCallback(() => {
resetForm({
values: {
poDate: '',
category: [],
status: [],
supplier: null,
area: null,
location: null,
project_flock: null,
project_flock_kandang: null,
},
});
setSelectedAreaId('');
setSelectedLocationId('');
onReset?.();
closeModalHandler();
}, [resetForm, onReset, closeModalHandler]);
const formikSubmitHandler = useCallback(async () => {
await submitForm();
}, [submitForm]);
return ( return (
<Modal <Modal
ref={ref} ref={ref}
@@ -206,7 +261,7 @@ const PurchaseFilterModal = ({
> >
<form <form
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
onReset={formik.handleReset} onReset={formikResetHandler}
className='w-full flex flex-col' className='w-full flex flex-col'
> >
{/* Modal Header */} {/* Modal Header */}
@@ -220,7 +275,9 @@ const PurchaseFilterModal = ({
type='button' type='button'
variant='ghost' variant='ghost'
color='none' color='none'
onClick={closeModalHandler} onClick={() => {
closeModalHandler();
}}
className='p-0 text-base-content/50 hover:text-base-content' className='p-0 text-base-content/50 hover:text-base-content'
> >
<Icon icon='heroicons:x-mark' width={20} height={20} /> <Icon icon='heroicons:x-mark' width={20} height={20} />
@@ -377,7 +434,8 @@ const PurchaseFilterModal = ({
</Button> </Button>
<Button <Button
type='submit' type='button'
onClick={formikSubmitHandler}
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm' className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
> >
Apply Filter Apply Filter
+164 -98
View File
@@ -1,17 +1,7 @@
'use client'; 'use client';
import axios from 'axios'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
import {
ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { usePathname } from 'next/navigation';
import { useUiStore } from '@/stores/ui/ui.store';
import useSWR from 'swr'; import useSWR from 'swr';
import useSWRInfinite from 'swr/infinite';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -31,17 +21,34 @@ import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/Purchase
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal'; import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import Dropdown from '@/components/dropdown/Dropdown'; import Dropdown from '@/components/dropdown/Dropdown';
import { OptionType } from '@/components/input/SelectInput';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase'; import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase'; import { PurchaseApi } from '@/services/api/purchase';
import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
type PurchaseTableFilters = {
search: string;
po_date: string;
approval_status: string;
product_category_id: string;
product_category_name: string;
supplier_id: string;
supplier_name: string;
area_id: string;
area_name: string;
location_id: string;
location_name: string;
project_flock_id: string;
project_flock_name: string;
project_flock_kandang_id: string;
project_flock_kandang_name: string;
};
// ===== STATUS BADGE UTILITIES ===== // ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = { const statusTextMap: Record<string, string> = {
@@ -150,9 +157,6 @@ const RowOptionsMenu = ({
}; };
const PurchaseTable = () => { const PurchaseTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname();
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
@@ -168,21 +172,28 @@ const PurchaseTable = () => {
// ===== TABLE FILTER STATE ===== // ===== TABLE FILTER STATE =====
const { const {
state: tableFilterState, state: tableFilterState,
setFilters,
updateFilter, updateFilter,
setPage, setPage,
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter<PurchaseTableFilters>({
initial: { initial: {
search: '', search: '',
po_date: '', po_date: '',
approval_status: '', approval_status: '',
product_category_id: '', product_category_id: '',
product_category_name: '',
supplier_id: '', supplier_id: '',
supplier_name: '',
area_id: '', area_id: '',
area_name: '',
location_id: '', location_id: '',
location_name: '',
project_flock_id: '', project_flock_id: '',
project_flock_name: '',
project_flock_kandang_id: '', project_flock_kandang_id: '',
project_flock_kandang_name: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
@@ -196,6 +207,16 @@ const PurchaseTable = () => {
project_flock_id: 'project_flock_id', project_flock_id: 'project_flock_id',
project_flock_kandang_id: 'project_flock_kandang_id', project_flock_kandang_id: 'project_flock_kandang_id',
}, },
excludeKeysFromUrl: [
'product_category_name',
'supplier_name',
'area_name',
'location_name',
'project_flock_name',
'project_flock_kandang_name',
],
persist: true,
storeName: 'purchase-table',
}); });
// ===== MODAL HOOKS ===== // ===== MODAL HOOKS =====
@@ -213,33 +234,6 @@ const PurchaseTable = () => {
PurchaseApi.getAllFetcher PurchaseApi.getAllFetcher
); );
const getKey = (
pageIndex: number,
previousPageData: BaseApiResponse<Expense>[] | null
) => {
if (pageIndex > 0 && !previousPageData) return null;
return `${ExpenseApi.basePath}?page=${pageIndex + 1}&limit=100`;
};
const { data: expensesPages } = useSWRInfinite(
getKey,
ExpenseApi.getAllFetcher
);
const expenseMap = useMemo(() => {
const map = new Map<string, number>();
if (!expensesPages) return map;
expensesPages.forEach((page) => {
if (isResponseSuccess(page)) {
page.data.forEach((expense: Expense) => {
map.set(expense.reference_number, expense.id);
});
}
});
return map;
}, [expensesPages]);
// ===== TABLE COLUMNS DEFINITION ===== // ===== TABLE COLUMNS DEFINITION =====
const purchaseColumns: ColumnDef<Purchase>[] = [ const purchaseColumns: ColumnDef<Purchase>[] = [
{ {
@@ -258,20 +252,16 @@ const PurchaseTable = () => {
return ( return (
<ul className='list-disc pl-4'> <ul className='list-disc pl-4'>
{poExpedition.map((exp, index) => { {poExpedition.map((exp, index) => {
const expenseId = expenseMap.get(exp.refrence); return (
if (expenseId) { <li key={index}>
return ( <Link
<li key={index}> href={`/expense/detail/?expenseId=${exp.id}`}
<Link className='p-0 h-auto text-primary underline'
href={`/expense/detail/?expenseId=${expenseId}`} >
className='p-0 h-auto text-primary underline' {exp.refrence}
> </Link>
{exp.refrence} </li>
</Link> );
</li>
);
}
return <li key={index}>{exp.refrence}</li>;
})} })}
</ul> </ul>
); );
@@ -422,58 +412,127 @@ const PurchaseTable = () => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]); }, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => {
setTableState('purchase-table', pathname);
}, [pathname, setTableState]);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback( const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => { (e) => {
setSearchValue(e.target.value);
updateFilter('search', e.target.value); updateFilter('search', e.target.value);
}, },
[updateFilter, setSearchValue] [updateFilter]
); );
const filterSubmitHandler = (values: PurchaseFilter) => { const filterSubmitHandler = (values: PurchaseFilter) => {
updateFilter('po_date', values.poDate); setFilters({
updateFilter('product_category_id', values.category.join(',')); po_date: values.poDate,
updateFilter('approval_status', values.status.join(',')); product_category_id: values.category.join(','),
updateFilter( product_category_name:
'supplier_id', values.category_labels?.map((item) => item.label).join(',') || '',
values.supplier_id ? String(values.supplier_id) : '' approval_status: values.status.join(','),
); supplier_id: values.supplier_id ? String(values.supplier_id) : '',
updateFilter('area_id', values.area_id ? String(values.area_id) : ''); supplier_name: values.supplier_label || '',
updateFilter( area_id: values.area_id ? String(values.area_id) : '',
'location_id', area_name: values.area_label || '',
values.location_id ? String(values.location_id) : '' location_id: values.location_id ? String(values.location_id) : '',
); location_name: values.location_label || '',
updateFilter( project_flock_id: values.project_flock_id
'project_flock_id', ? String(values.project_flock_id)
values.project_flock_id ? String(values.project_flock_id) : '' : '',
); project_flock_name: values.project_flock_label || '',
updateFilter( project_flock_kandang_id: values.project_flock_kandang_id
'project_flock_kandang_id',
values.project_flock_kandang_id
? String(values.project_flock_kandang_id) ? String(values.project_flock_kandang_id)
: '' : '',
); project_flock_kandang_name: values.project_flock_kandang_label || '',
});
}; };
const filterResetHandler = () => { const filterResetHandler = () => {
updateFilter('po_date', ''); setFilters({
updateFilter('product_category_id', ''); po_date: '',
updateFilter('approval_status', ''); product_category_id: '',
updateFilter('supplier_id', ''); product_category_name: '',
updateFilter('area_id', ''); approval_status: '',
updateFilter('location_id', ''); supplier_id: '',
updateFilter('project_flock_id', ''); supplier_name: '',
updateFilter('project_flock_kandang_id', ''); area_id: '',
area_name: '',
location_id: '',
location_name: '',
project_flock_id: '',
project_flock_name: '',
project_flock_kandang_id: '',
project_flock_kandang_name: '',
});
}; };
const purchaseFilterInitialValues = useMemo(() => {
const categoryIds = tableFilterState.product_category_id
? tableFilterState.product_category_id
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
const categoryLabels = tableFilterState.product_category_name
? tableFilterState.product_category_name
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
const approvalStatuses = tableFilterState.approval_status
? tableFilterState.approval_status
.split(',')
.map((item) => item.trim())
.filter(Boolean)
: [];
return {
poDate: tableFilterState.po_date,
category: categoryIds.map((value, index) => ({
value: Number(value),
label: categoryLabels[index] || value,
})),
status: approvalStatuses.map((value) => ({
value,
label:
PURCHASE_ORDER_APPROVAL_LINE.find((item) => item.step_name === value)
?.step_name || value,
})),
supplier: tableFilterState.supplier_id
? ({
value: Number(tableFilterState.supplier_id),
label:
tableFilterState.supplier_name || tableFilterState.supplier_id,
} as OptionType<number>)
: null,
area: tableFilterState.area_id
? ({
value: Number(tableFilterState.area_id),
label: tableFilterState.area_name || tableFilterState.area_id,
} as OptionType<number>)
: null,
location: tableFilterState.location_id
? ({
value: Number(tableFilterState.location_id),
label:
tableFilterState.location_name || tableFilterState.location_id,
} as OptionType<number>)
: null,
project_flock: tableFilterState.project_flock_id
? ({
value: Number(tableFilterState.project_flock_id),
label:
tableFilterState.project_flock_name ||
tableFilterState.project_flock_id,
} as OptionType<number>)
: null,
project_flock_kandang: tableFilterState.project_flock_kandang_id
? ({
value: Number(tableFilterState.project_flock_kandang_id),
label:
tableFilterState.project_flock_kandang_name ||
tableFilterState.project_flock_kandang_id,
} as OptionType<number>)
: null,
};
}, [tableFilterState]);
const exportToExcel = useCallback(async () => { const exportToExcel = useCallback(async () => {
setIsLoadingExportingToExcel(true); setIsLoadingExportingToExcel(true);
@@ -584,6 +643,12 @@ const PurchaseTable = () => {
'search', 'search',
'filter_by', 'filter_by',
'sort_by', 'sort_by',
'product_category_name',
'supplier_name',
'area_name',
'location_name',
'project_flock_name',
'project_flock_kandang_name',
]} ]}
fieldGroups={[['startDate', 'endDate']]} fieldGroups={[['startDate', 'endDate']]}
onClick={filterModal.openModal} onClick={filterModal.openModal}
@@ -705,6 +770,7 @@ const PurchaseTable = () => {
<PurchaseFilterModal <PurchaseFilterModal
ref={filterModal.ref} ref={filterModal.ref}
initialValues={purchaseFilterInitialValues}
onSubmit={filterSubmitHandler} onSubmit={filterSubmitHandler}
onReset={filterResetHandler} onReset={filterResetHandler}
/> />
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject, useMemo, useState } from 'react'; import { RefObject, useEffect, useMemo, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as yup from 'yup'; import * as yup from 'yup';
@@ -60,6 +60,11 @@ const ReportDepreciationFilterModal = ({
string | undefined string | undefined
>(initialValues?.location_id || undefined); >(initialValues?.location_id || undefined);
useEffect(() => {
setSelectedAreaId(initialValues?.area_id || undefined);
setSelectedLocationId(initialValues?.location_id || undefined);
}, [initialValues?.area_id, initialValues?.location_id]);
const closeModalHandler = () => { const closeModalHandler = () => {
ref.current?.close(); ref.current?.close();
}; };
@@ -97,6 +102,7 @@ const ReportDepreciationFilterModal = ({
const formik = useFormik<ReportDepreciationFilterValues>({ const formik = useFormik<ReportDepreciationFilterValues>({
initialValues: initialValues || defaultInitialValues, initialValues: initialValues || defaultInitialValues,
enableReinitialize: true,
validationSchema: ReportDepreciationFilterSchema, validationSchema: ReportDepreciationFilterSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
onSubmit?.(values); onSubmit?.(values);
@@ -126,8 +126,35 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
const restoredLocation = filterParams.location_id
? locationOptions.find((opt) => String(opt.value) === filterParams.location_id) ||
{ value: filterParams.location_id, label: filterParams.location_id }
: null;
const restoredSupplier = filterParams.supplier_id
? supplierOptions.find((opt) => String(opt.value) === filterParams.supplier_id) ||
{ value: filterParams.supplier_id, label: filterParams.supplier_id }
: null;
const restoredKandang = filterParams.kandang_id
? projectFlockKandangOptions.find((opt) => String(opt.value) === filterParams.kandang_id) ||
{ value: filterParams.kandang_id, label: filterParams.kandang_id }
: null;
const restoredNonstock = filterParams.nonstock_id
? nonstockOptions.find((opt) => String(opt.value) === filterParams.nonstock_id) ||
{ value: filterParams.nonstock_id, label: filterParams.nonstock_id }
: null;
const restoredCategory = filterParams.category
? categoryOptions.find((opt) => opt.value === filterParams.category) || null
: null;
formik.setValues({
location_id: restoredLocation,
supplier_id: restoredSupplier,
kandang_id: restoredKandang,
nonstock_id: restoredNonstock,
realization_date: filterParams.realization_date || null,
category: restoredCategory,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
// ===== OPTIONS ===== // ===== OPTIONS =====
@@ -38,6 +38,7 @@ import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton
import { OptionType } from '@/components/table/TableRowSizeSelector'; import { OptionType } from '@/components/table/TableRowSizeSelector';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import Pagination from '@/components/Pagination';
interface CustomerPaymentTabProps { interface CustomerPaymentTabProps {
tabId: string; tabId: string;
@@ -58,7 +59,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
// ===== PAGINATION STATE ===== // ===== PAGINATION STATE =====
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<FilterParams>({}); const [filterParams, setFilterParams] = useState<FilterParams>({});
@@ -117,8 +118,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
formik.setValues({
start_date: filterParams.start_date || null,
end_date: filterParams.end_date || null,
customer_ids: filterParams.customer_ids || null,
filter_by: filterParams.filter_by || null,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
const getPaymentStatusBadgeColor = (notes: string): Color => { const getPaymentStatusBadgeColor = (notes: string): Color => {
@@ -249,6 +255,14 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
[customerPayment] [customerPayment]
); );
const meta = useMemo(
() =>
isResponseSuccess(customerPayment) && customerPayment.meta
? customerPayment.meta
: null,
[customerPayment]
);
// ===== EXPORT DATA FETCHER ===== // ===== EXPORT DATA FETCHER =====
const customerPaymentExport = useCallback(async (): Promise< const customerPaymentExport = useCallback(async (): Promise<
CustomerPaymentReport[] | null CustomerPaymentReport[] | null
@@ -811,6 +825,27 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
</Card> </Card>
); );
})} })}
{!isLoading && data.length > 0 && meta && (
<div className='mt-5 px-3'>
<Pagination
totalItems={meta.total_results || 0}
itemsPerPage={meta.limit || 0}
currentPage={meta.page || 0}
onPrevPage={() =>
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
}
onNextPage={() =>
setCurrentPage((curr) =>
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
)
}
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
rowOptions={[10, 20, 50, 100]}
onRowChange={setPageSize}
/>
</div>
)}
</div> </div>
{/* Filter Modal */} {/* Filter Modal */}
@@ -1,6 +1,7 @@
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Dropdown from '@/components/Dropdown'; import Dropdown from '@/components/Dropdown';
import Pagination from '@/components/Pagination';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import { OptionType, useSelect } from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal'; import Modal, { useModal } from '@/components/Modal';
@@ -78,6 +79,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== PAGINATION STATE =====
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({ const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
start_date: undefined, start_date: undefined,
@@ -128,7 +133,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
filter_by: values.filterBy?.value?.toString() || undefined, filter_by: values.filterBy?.value?.toString() || undefined,
}); });
filterModal.closeModal(); filterModal.closeModal();
// setIsSubmitted(true); setCurrentPage(1);
}, },
onReset: () => { onReset: () => {
setFilterParams({ setFilterParams({
@@ -137,14 +142,30 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
supplier_ids: undefined, supplier_ids: undefined,
filter_by: undefined, filter_by: undefined,
}); });
// setIsSubmitted(false); setCurrentPage(1);
filterModal.closeModal(); filterModal.closeModal();
}, },
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
const restoredFilterBy =
dataTypeOptions.find((opt) => opt.value === filterParams.filter_by) ||
null;
const supplierIdList = filterParams.supplier_ids
? filterParams.supplier_ids.split(',')
: [];
const restoredSupplierIds = supplierOptions.filter((opt) =>
supplierIdList.includes(String(opt.value))
);
formik.setValues({
startDate: filterParams.start_date || null,
endDate: filterParams.end_date || null,
supplierIds: restoredSupplierIds.length > 0 ? restoredSupplierIds : null,
filterBy: restoredFilterBy,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
// ===== DATA FETCHING ===== // ===== DATA FETCHING =====
@@ -155,6 +176,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
filter_by: filterParams.filter_by, filter_by: filterParams.filter_by,
start_date: filterParams.start_date, start_date: filterParams.start_date,
end_date: filterParams.end_date, end_date: filterParams.end_date,
page: currentPage,
limit: pageSize,
}; };
return ['debt-supplier-report', params]; return ['debt-supplier-report', params];
@@ -164,7 +187,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
params.supplier_ids, params.supplier_ids,
params.filter_by, params.filter_by,
params.start_date, params.start_date,
params.end_date params.end_date,
params.page,
params.limit
) )
); );
@@ -176,6 +201,14 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
[debtSupplier] [debtSupplier]
); );
const meta = useMemo(
() =>
isResponseSuccess(debtSupplier) && debtSupplier.meta
? debtSupplier.meta
: null,
[debtSupplier]
);
// ===== EXPORT DATA FETCHER ===== // ===== EXPORT DATA FETCHER =====
const debtSupplierExport = useCallback(async (): Promise< const debtSupplierExport = useCallback(async (): Promise<
DebtSupplier[] | null DebtSupplier[] | null
@@ -717,6 +750,27 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
</Card> </Card>
); );
})} })}
{!isLoading && data.length > 0 && meta && (
<div className='mt-5 px-3'>
<Pagination
totalItems={meta.total_results || 0}
itemsPerPage={meta.limit || 0}
currentPage={meta.page || 0}
onPrevPage={() =>
setCurrentPage((curr) => (curr > 1 ? curr - 1 : curr))
}
onNextPage={() =>
setCurrentPage((curr) =>
meta.total_pages && curr < meta.total_pages ? curr + 1 : curr
)
}
onPageChange={(pageNumber) => setCurrentPage(pageNumber)}
rowOptions={[10, 20, 50, 100]}
onRowChange={setPageSize}
/>
</div>
)}
</div> </div>
{/* Filter Modal */} {/* Filter Modal */}
@@ -156,8 +156,17 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
formik.setValues({
start_date: filterParams.start_date || null,
end_date: filterParams.end_date || null,
area_ids: filterParams.area_id || null,
supplier_ids: filterParams.supplier_id || null,
product_ids: filterParams.product_id || null,
product_category_ids: filterParams.product_category_id || null,
filter_by: filterParams.filter_by || null,
sort_by: filterParams.sort_by || null,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
const { setFieldValue } = formik; const { setFieldValue } = formik;
@@ -156,8 +156,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
formik.setValues({
page: formik.values.page,
pageSize: formik.values.pageSize,
search: formik.values.search,
area_id: filterParams.area_id || null,
location_id: filterParams.location_id || null,
warehouse_id: filterParams.warehouse_id || null,
customer_id: filterParams.customer_id || null,
start_date: filterParams.start_date || null,
end_date: filterParams.end_date || null,
filter_by: filterParams.filter_by || null,
marketing_type: filterParams.marketing_type || null,
sort_by: filterParams.sort_by || null,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
// ===== SEARCH CHANGE HANDLER ===== // ===== SEARCH CHANGE HANDLER =====
@@ -152,8 +152,19 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
formik.setValues({
page: formik.values.page,
pageSize: formik.values.pageSize,
area_id: filterParams.area_id || null,
location_id: filterParams.location_id || null,
kandang_id: filterParams.kandang_id || null,
weight_min: filterParams.weight_min || null,
weight_max: filterParams.weight_max || null,
period: filterParams.period || null,
sort_by: filterParams.sort_by || null,
show_unrecorded: filterParams.show_unrecorded ?? false,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
// ===== WEIGHT CHANGE HANDLERS ===== // ===== WEIGHT CHANGE HANDLERS =====
@@ -263,8 +263,30 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
}); });
handleFilterModalOpenRef.current = () => { handleFilterModalOpenRef.current = () => {
const restoredAreaId = filterParams.area_id
? areaOptions.find((opt) => String(opt.value) === filterParams.area_id) ||
{ value: filterParams.area_id, label: filterParams.area_id }
: null;
const restoredLocationId = filterParams.location_id
? locationOptions.find((opt) => String(opt.value) === filterParams.location_id) ||
{ value: filterParams.location_id, label: filterParams.location_id }
: null;
const restoredProjectFlockId = filterParams.project_flock_id
? projectFlockOptions.find((opt) => String(opt.value) === filterParams.project_flock_id) ||
{ value: filterParams.project_flock_id, label: filterParams.project_flock_id }
: null;
const restoredKandangId = filterParams.project_flock_kandang_id
? projectFlockKandangOptions.find((opt) => String(opt.value) === filterParams.project_flock_kandang_id) ||
{ value: filterParams.project_flock_kandang_id, label: filterParams.project_flock_kandang_id }
: null;
formik.setValues({
area_id: restoredAreaId,
location_id: restoredLocationId,
project_flock_id: restoredProjectFlockId,
kandang_id: restoredKandangId,
});
filterModal.openModal(); filterModal.openModal();
formik.validateForm();
}; };
const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] = const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] =
@@ -53,7 +53,6 @@ import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang'; import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
// Static categories
const CATEGORIES = [ const CATEGORIES = [
{ value: 'pullet_open', label: 'Pullet Open' }, { value: 'pullet_open', label: 'Pullet Open' },
{ value: 'pullet_close', label: 'Pullet Close' }, { value: 'pullet_close', label: 'Pullet Close' },
@@ -62,6 +61,14 @@ const CATEGORIES = [
{ value: 'empty_kandang', label: 'Kandang Kosong' }, { value: 'empty_kandang', label: 'Kandang Kosong' },
]; ];
const CATEGORY_LABELS: { [key: string]: string } = {
pullet_open: 'Pullet Open',
pullet_close: 'Pullet Close',
produksi_open: 'Produksi Open',
produksi_close: 'Produksi Close',
empty_kandang: 'Kandang Kosong',
};
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam']; const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
const TIME_TYPE_LABELS: { [key: string]: string } = { const TIME_TYPE_LABELS: { [key: string]: string } = {
Umum: 'Umum', Umum: 'Umum',
@@ -98,6 +105,8 @@ export function DailyChecklistContent() {
const [emptyKandang, setEmptyKandang] = useState(false); const [emptyKandang, setEmptyKandang] = useState(false);
const [emptyKandangEndDate, setEmptyKandangEndDate] = useState(''); const [emptyKandangEndDate, setEmptyKandangEndDate] = useState('');
const isKandangEmpty = selectedCategory === 'empty_kandang';
const { const {
options: kandangOptions, options: kandangOptions,
isLoadingMore: isLoadingMoreKandang, isLoadingMore: isLoadingMoreKandang,
@@ -244,7 +253,6 @@ export function DailyChecklistContent() {
} }
}, [selectedCategory]); }, [selectedCategory]);
// Format date for display
const formatDateForDisplay = (dateStr: string) => { const formatDateForDisplay = (dateStr: string) => {
if (!dateStr) return 'Pilih tanggal'; if (!dateStr) return 'Pilih tanggal';
const [year, month, day] = dateStr.split('-'); const [year, month, day] = dateStr.split('-');
@@ -257,6 +265,36 @@ export function DailyChecklistContent() {
}); });
}; };
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
});
};
const isMobileDevice = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
const getStatusMessage = () => {
switch (checklistStatus) {
case 'DRAFT':
return 'Checklist harian perlu disubmit';
case 'SUBMITTED':
return 'Checklist harian menunggu persetujuan';
case 'APPROVED':
return 'Checklist harian telah disetujui';
case 'REJECTED':
return 'Checklist harian telah ditolak';
default:
return '';
}
};
// Fetch master data on mount // Fetch master data on mount
useEffect(() => { useEffect(() => {
setInitialLoading(false); setInitialLoading(false);
@@ -298,7 +336,7 @@ export function DailyChecklistContent() {
if (isResponseError(checklist)) { if (isResponseError(checklist)) {
console.error('Error upserting checklist:', checklist.message); console.error('Error upserting checklist:', checklist.message);
toast.error('Gagal memuat checklist'); toast.error('Gagal memuat checklist: ' + checklist.message);
return; return;
} }
@@ -311,6 +349,12 @@ export function DailyChecklistContent() {
if (isResponseError(existingPhases)) { if (isResponseError(existingPhases)) {
console.error('Error loading phases:', existingPhases.message); console.error('Error loading phases:', existingPhases.message);
} else if (
existingPhases &&
existingPhases.data &&
existingPhases.data.phases.length === 0
) {
toast.success('Berhasil membuat daily checklist!');
} else if ( } else if (
existingPhases && existingPhases &&
existingPhases.data && existingPhases.data &&
@@ -834,7 +878,43 @@ export function DailyChecklistContent() {
} }
setChecklistStatus('SUBMITTED'); setChecklistStatus('SUBMITTED');
toast.success('Checklist berhasil disubmit untuk approval');
const shareToWhatsApp = () => {
const kandangName = kandangOptions.find(
(k) => String(k.value) === kandangId
)?.label || kandangId;
const statusMsg = getStatusMessage();
const category = selectedCategory || '';
const message = encodeURIComponent(
`Daily Checklist\n\nTanggal: ${formatDate(date)}\nKandang: ${kandangName}\nKategori: ${CATEGORY_LABELS[category] || category}\nStatus: SUBMITTED${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
);
const isMobile = isMobileDevice();
const whatsappUrl = isMobile
? `https://wa.me/?text=${message}`
: `https://web.whatsapp.com/send?text=${message}`;
window.open(whatsappUrl, '_blank');
};
toast.success('Checklist berhasil disubmit untuk approval', {
action: {
label: 'Bagikan ke WhatsApp',
onClick: shareToWhatsApp,
},
description: (
<button
onClick={() =>
router.push(
`/daily-checklist/list-daily-checklist/detail/?checklistId=${dailyChecklistId}`
)
}
className='text-blue-600 hover:text-blue-800 underline font-medium'
>
Lihat Detail
</button>
),
});
} catch (error) { } catch (error) {
console.error('Error submitting:', error); console.error('Error submitting:', error);
toast.error('Terjadi kesalahan'); toast.error('Terjadi kesalahan');
@@ -1118,7 +1198,7 @@ export function DailyChecklistContent() {
</div> </div>
{/* Phase Selection Section */} {/* Phase Selection Section */}
{dailyChecklistId && ( {!isKandangEmpty && dailyChecklistId && (
<div className='mb-6 pb-6 border-b border-gray-200'> <div className='mb-6 pb-6 border-b border-gray-200'>
{isChecklistStatusDraft && ( {isChecklistStatusDraft && (
<div className='flex items-center justify-between mb-3'> <div className='flex items-center justify-between mb-3'>
@@ -1159,298 +1239,314 @@ export function DailyChecklistContent() {
)} )}
{/* ABK Assignment Section */} {/* ABK Assignment Section */}
{dailyChecklistId && selectedPhaseIds.length > 0 && ( {!isKandangEmpty &&
<div className='mb-6 pb-6 border-b border-gray-200'> dailyChecklistId &&
{isChecklistStatusDraft && ( selectedPhaseIds.length > 0 && (
<div className='flex items-center justify-between mb-3'> <div className='mb-6 pb-6 border-b border-gray-200'>
<Label>ABK Assignment</Label> {isChecklistStatusDraft && (
<Button <div className='flex items-center justify-between mb-3'>
onClick={handleAddAbk} <Label>ABK Assignment</Label>
size='sm' <Button
variant='outline' onClick={handleAddAbk}
className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50' size='sm'
disabled={!kandangId || !isChecklistStatusDraft} variant='outline'
> className='border-[#0069e0] text-[#0069e0] hover:bg-blue-50'
<Plus className='w-4 h-4 mr-1' /> disabled={!kandangId || !isChecklistStatusDraft}
Tambah ABK
</Button>
</div>
)}
{selectedEmployees.length > 0 ? (
<div className='flex flex-wrap gap-2'>
{selectedEmployees.map((emp) => (
<Badge
key={emp.id}
variant='secondary'
className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg'
> >
{emp.name} <Plus className='w-4 h-4 mr-1' />
{isChecklistStatusDraft && ( Tambah ABK
<button </Button>
onClick={() => handleRemoveAbk(String(emp.id))} </div>
className='ml-2 hover:text-gray-900' )}
>
<X className='w-3 h-3' /> {selectedEmployees.length > 0 ? (
</button> <div className='flex flex-wrap gap-2'>
)} {selectedEmployees.map((emp) => (
</Badge> <Badge
))} key={emp.id}
</div> variant='secondary'
) : ( className='px-3 py-1.5 bg-gray-100 text-gray-700 border border-gray-200 rounded-lg'
<p className='text-sm text-gray-500'>Belum ada ABK dipilih</p> >
)} {emp.name}
</div> {isChecklistStatusDraft && (
)} <button
onClick={() => handleRemoveAbk(String(emp.id))}
className='ml-2 hover:text-gray-900'
>
<X className='w-3 h-3' />
</button>
)}
</Badge>
))}
</div>
) : (
<p className='text-sm text-gray-500'>
Belum ada ABK dipilih
</p>
)}
</div>
)}
{/* Activity Checklist Table */} {/* Activity Checklist Table */}
{dailyChecklistId && {!isKandangEmpty && (
selectedPhaseIds.length > 0 && <>
selectedEmployees.length > 0 ? ( {dailyChecklistId &&
<div> selectedPhaseIds.length > 0 &&
<h3 className='font-semibold text-gray-900 mb-4'> selectedEmployees.length > 0 ? (
Checklist Aktivitas <div>
</h3> <h3 className='font-semibold text-gray-900 mb-4'>
{Object.keys(activitiesByPhase).length > 0 ? ( Checklist Aktivitas
<div className='overflow-x-auto'> </h3>
<table className='w-full border border-gray-200 rounded-lg'> {Object.keys(activitiesByPhase).length > 0 ? (
<thead> <div className='overflow-x-auto'>
<tr className='bg-gray-50 border-b border-gray-200'> <table className='w-full border border-gray-200 rounded-lg'>
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700 border-r border-gray-200 min-w-[200px]'> <thead>
Aktivitas <tr className='bg-gray-50 border-b border-gray-200'>
</th> <th className='text-left py-3 px-4 text-sm font-semibold text-gray-700 border-r border-gray-200 min-w-[200px]'>
{sortedSelectedEmployees.map((emp) => ( Aktivitas
<th </th>
key={emp.id} {sortedSelectedEmployees.map((emp) => (
className='text-center py-3 px-4 text-sm font-semibold text-gray-700 border-r border-gray-200 min-w-[100px]' <th
> key={emp.id}
{emp.name} className='text-center py-3 px-4 text-sm font-semibold text-gray-700 border-r border-gray-200 min-w-[100px]'
</th>
))}
<th className='text-left py-3 px-4 text-sm font-semibold text-gray-700 min-w-[200px]'>
Catatan
</th>
</tr>
</thead>
<tbody>
{Object.keys(groupActivitiesByPhase()).flatMap(
(phaseId) => {
const phaseData = groupActivitiesByPhase()[phaseId];
const { phase, timeGroups } = phaseData;
const timeTypes = Object.keys(timeGroups).sort(
(a, b) =>
TIME_TYPE_ORDER.indexOf(a) -
TIME_TYPE_ORDER.indexOf(b)
);
// Count total activities in this phase
const totalActivities = timeTypes.reduce(
(sum, timeType) =>
sum + timeGroups[timeType].length,
0
);
// Build all rows for this phase
const rows = [];
// PHASE Header (Main parent) - BLUE
rows.push(
<tr
key={`phase-${phaseId}`}
className='bg-blue-50 border-b border-blue-200'
>
<td
colSpan={selectedEmployees.length + 2}
className='py-2.5 px-4'
> >
<div className='flex items-center gap-2'> {emp.name}
<span className='text-sm font-semibold text-blue-900'> </th>
{phase.name} ))}
</span> <th className='text-left py-3 px-4 text-sm font-semibold text-gray-700 min-w-[200px]'>
<Badge Catatan
variant='secondary' </th>
className='text-xs bg-blue-100 text-blue-700 border-blue-200 rounded-lg' </tr>
> </thead>
{totalActivities} aktivitas <tbody>
</Badge> {Object.keys(groupActivitiesByPhase()).flatMap(
</div> (phaseId) => {
</td> const phaseData =
</tr> groupActivitiesByPhase()[phaseId];
); const { phase, timeGroups } = phaseData;
// TIME_TYPE sub-headers and activities const timeTypes = Object.keys(timeGroups).sort(
timeTypes.forEach((timeType) => { (a, b) =>
const activities = timeGroups[timeType]; TIME_TYPE_ORDER.indexOf(a) -
const hasMultipleTimeTypes = timeTypes.length > 1; TIME_TYPE_ORDER.indexOf(b)
);
// TIME Header (optional, only if phase has multiple time types) - GRAY SOFT // Count total activities in this phase
if (hasMultipleTimeTypes) { const totalActivities = timeTypes.reduce(
(sum, timeType) =>
sum + timeGroups[timeType].length,
0
);
// Build all rows for this phase
const rows = [];
// PHASE Header (Main parent) - BLUE
rows.push( rows.push(
<tr <tr
key={`time-${phaseId}-${timeType}`} key={`phase-${phaseId}`}
className='bg-gray-50 border-b border-gray-200' className='bg-blue-50 border-b border-blue-200'
> >
<td <td
colSpan={selectedEmployees.length + 2} colSpan={selectedEmployees.length + 2}
className='py-2 px-4 pl-8' className='py-2.5 px-4'
> >
<span className='text-xs font-medium text-gray-600'> <div className='flex items-center gap-2'>
{TIME_TYPE_LABELS[timeType]} ( <span className='text-sm font-semibold text-blue-900'>
{activities.length} aktivitas) {phase.name}
</span> </span>
<Badge
variant='secondary'
className='text-xs bg-blue-100 text-blue-700 border-blue-200 rounded-lg'
>
{totalActivities} aktivitas
</Badge>
</div>
</td> </td>
</tr> </tr>
); );
}
// ACTIVITY rows (Child rows with checkboxes) // TIME_TYPE sub-headers and activities
activities.sort((a, b) => timeTypes.forEach((timeType) => {
a.name.localeCompare(b.name, undefined, { const activities = timeGroups[timeType];
sensitivity: 'base', const hasMultipleTimeTypes =
}) timeTypes.length > 1;
);
activities.forEach((activity, index) => { // TIME Header (optional, only if phase has multiple time types) - GRAY SOFT
const taskId = if (hasMultipleTimeTypes) {
taskIdsByPhaseActivityId[activity.id]; rows.push(
const indentClass = hasMultipleTimeTypes <tr
? 'pl-12' key={`time-${phaseId}-${timeType}`}
: 'pl-8'; className='bg-gray-50 border-b border-gray-200'
rows.push(
<tr
key={`activity-${activity.id}`}
className={
index % 2 === 0
? 'bg-white'
: 'bg-gray-50/50'
}
>
<td
className={`py-3 px-4 ${indentClass} border-r border-gray-200`}
>
<p className='text-sm text-gray-900'>
{activity.name}
</p>
{activity.description && (
<p className='text-xs text-gray-500 mt-0.5'>
{activity.description}
</p>
)}
</td>
{sortedSelectedEmployees.map((emp) => (
<td
key={emp.id}
className='text-center py-3 px-4 border-r border-gray-200'
> >
<input <td
type='checkbox' colSpan={selectedEmployees.length + 2}
checked={ className='py-2 px-4 pl-8'
assignments[taskId]?.[emp.id] >
?.checked || false <span className='text-xs font-medium text-gray-600'>
} {TIME_TYPE_LABELS[timeType]} (
onChange={(e) => {activities.length} aktivitas)
handleCheckboxChange( </span>
String(activity.id), </td>
String(emp.id), </tr>
e.target.checked );
) }
}
disabled={!isChecklistStatusDraft}
className='checkbox-clean'
/>
</td>
))}
<td className='py-3 px-4'>
<DebouncedTextArea
delay={500}
name='notes'
rows={1}
placeholder='Catatan (opsional)'
value={
taskId && selectedEmployees.length > 0
? assignments[taskId]?.[
selectedEmployees[0].id
]?.note || ''
: ''
}
onChange={(e) => {
if (selectedEmployees.length > 0) {
handleNoteChange(
String(activity.id),
String(selectedEmployees[0].id),
e.target.value
);
}
}}
disabled={!isChecklistStatusDraft}
/>
</td>
</tr>
);
});
});
return rows; // ACTIVITY rows (Child rows with checkboxes)
} activities.sort((a, b) =>
)} a.name.localeCompare(b.name, undefined, {
</tbody> sensitivity: 'base',
</table> })
);
activities.forEach((activity, index) => {
const taskId =
taskIdsByPhaseActivityId[activity.id];
const indentClass = hasMultipleTimeTypes
? 'pl-12'
: 'pl-8';
rows.push(
<tr
key={`activity-${activity.id}`}
className={
index % 2 === 0
? 'bg-white'
: 'bg-gray-50/50'
}
>
<td
className={`py-3 px-4 ${indentClass} border-r border-gray-200`}
>
<p className='text-sm text-gray-900'>
{activity.name}
</p>
{activity.description && (
<p className='text-xs text-gray-500 mt-0.5'>
{activity.description}
</p>
)}
</td>
{sortedSelectedEmployees.map((emp) => (
<td
key={emp.id}
className='text-center py-3 px-4 border-r border-gray-200'
>
<input
type='checkbox'
checked={
assignments[taskId]?.[emp.id]
?.checked || false
}
onChange={(e) =>
handleCheckboxChange(
String(activity.id),
String(emp.id),
e.target.checked
)
}
disabled={!isChecklistStatusDraft}
className='checkbox-clean'
/>
</td>
))}
<td className='py-3 px-4'>
<DebouncedTextArea
delay={500}
name='notes'
rows={1}
placeholder='Catatan (opsional)'
value={
taskId &&
selectedEmployees.length > 0
? assignments[taskId]?.[
selectedEmployees[0].id
]?.note || ''
: ''
}
onChange={(e) => {
if (
selectedEmployees.length > 0
) {
handleNoteChange(
String(activity.id),
String(
selectedEmployees[0].id
),
e.target.value
);
}
}}
disabled={!isChecklistStatusDraft}
/>
</td>
</tr>
);
});
});
return rows;
}
)}
</tbody>
</table>
</div>
) : (
<div className='flex flex-col items-center justify-center py-16 text-center'>
<ListChecks className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Tidak Ada Aktivitas
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Tidak ada aktivitas untuk fase yang dipilih. Silakan
tambahkan aktivitas di Master Aktivitas.
</p>
</div>
)}
</div> </div>
) : ( ) : (
<div className='flex flex-col items-center justify-center py-16 text-center'> <div className='flex flex-col items-center justify-center py-16 text-center'>
<ListChecks className='w-16 h-16 text-gray-300 mb-4' /> {!dailyChecklistId ? (
<h3 className='text-lg font-semibold text-gray-700 mb-2'> <div>
Tidak Ada Aktivitas <FilePlus className='w-16 h-16 text-gray-300 mb-4 mx-auto' />
</h3> <h3 className='text-lg font-semibold text-gray-700 mb-2'>
<p className='text-sm text-gray-500 max-w-md'> Mulai Checklist Baru
Tidak ada aktivitas untuk fase yang dipilih. Silakan </h3>
tambahkan aktivitas di Master Aktivitas. <p className='text-sm text-gray-500 max-w-md'>
</p> Pilih tanggal, kandang, dan kategori untuk memulai
checklist harian Anda.
</p>
</div>
) : selectedPhaseIds.length === 0 ? (
<div className='flex flex-col items-center text-center'>
<FilePlus className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih Fase / Tahap
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap
aktivitas yang akan dikerjakan.
</p>
</div>
) : (
<div>
<FilePlus className='w-16 h-16 text-gray-300 mb-4 mx-auto' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih ABK
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja
yang akan ditugaskan.
</p>
</div>
)}
</div> </div>
)} )}
</div> </>
) : (
<div className='flex flex-col items-center justify-center py-16 text-center'>
{!dailyChecklistId ? (
<div>
<FilePlus className='w-16 h-16 text-gray-300 mb-4 mx-auto' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Mulai Checklist Baru
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Pilih tanggal, kandang, dan kategori untuk memulai
checklist harian Anda.
</p>
</div>
) : selectedPhaseIds.length === 0 ? (
<div className='flex flex-col items-center text-center'>
<FilePlus className='w-16 h-16 text-gray-300 mb-4' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih Fase / Tahap
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap
aktivitas yang akan dikerjakan.
</p>
</div>
) : (
<div>
<FilePlus className='w-16 h-16 text-gray-300 mb-4 mx-auto' />
<h3 className='text-lg font-semibold text-gray-700 mb-2'>
Pilih ABK
</h3>
<p className='text-sm text-gray-500 max-w-md'>
Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja
yang akan ditugaskan.
</p>
</div>
)}
</div>
)} )}
{dailyChecklistId && {!isKandangEmpty &&
dailyChecklistId &&
selectedPhaseIds.length > 0 && selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 && ( selectedEmployees.length > 0 && (
<> <>
@@ -1548,7 +1644,8 @@ export function DailyChecklistContent() {
)} )}
{/* Action Buttons */} {/* Action Buttons */}
{dailyChecklistId && {!isKandangEmpty &&
dailyChecklistId &&
selectedPhaseIds.length > 0 && selectedPhaseIds.length > 0 &&
selectedEmployees.length > 0 && selectedEmployees.length > 0 &&
isChecklistStatusDraft && ( isChecklistStatusDraft && (
@@ -2,7 +2,14 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import * as React from 'react'; import * as React from 'react';
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; import {
ArrowLeft,
CheckCircle,
XCircle,
AlertCircle,
Share2,
} from 'lucide-react';
import * as htmlToImage from 'html-to-image';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
@@ -137,6 +144,8 @@ export function DetailDailyChecklistContent() {
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [isGeneratingImage, setIsGeneratingImage] = useState(false);
useEffect(() => { useEffect(() => {
if (checklistId) { if (checklistId) {
fetchChecklistDetail(); fetchChecklistDetail();
@@ -547,6 +556,102 @@ export function DetailDailyChecklistContent() {
}); });
}; };
const isMobileDevice = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
};
const getStatusMessage = () => {
switch (header?.status) {
case 'DRAFT':
return 'Checklist harian perlu disubmit';
case 'SUBMITTED':
return 'Checklist harian menunggu persetujuan';
case 'APPROVED':
return 'Checklist harian telah disetujui';
case 'REJECTED':
return 'Checklist harian telah ditolak';
default:
return '';
}
};
const shareHandler = async () => {
const isMobile = isMobileDevice();
if (isMobile) {
setIsGeneratingImage(true);
}
const baseTitle = `Daily Checklist - ${formatDate(header?.date || '')} - ${header?.kandang_name} - ${header?.category}`;
const statusMsg = getStatusMessage();
const statusInfo = `\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}`;
const urlMessage = `\n\nView full checklist: ${window.location.href}`;
const fullMessage = baseTitle + statusInfo + urlMessage;
let shareData: ShareData;
if (isMobile) {
const htmlBlob = await htmlToImage.toBlob(document.body);
const imgFile = new File(
[htmlBlob!],
`daily-checklist-${header?.date}-${header?.kandang_name}-${header?.category}.png`,
{
type: 'image/png',
}
);
shareData = {
files: [imgFile],
title: baseTitle,
text: fullMessage,
url: window.location.href,
};
} else {
shareData = {
title: baseTitle,
text: fullMessage,
url: window.location.href,
};
}
setIsGeneratingImage(false);
try {
if (!navigator.canShare(shareData)) {
toast.error(
'Gagal membagikan checklist, coba dengan perangkat yang berbeda'
);
return;
}
await navigator.share(shareData);
toast.success('Checklist berhasil dibagikan');
} catch (error) {
toast.error('Gagal membagikan checklist');
}
};
const shareToWhatsAppHandler = async () => {
const isMobile = isMobileDevice();
setIsGeneratingImage(true);
const statusMsg = getStatusMessage();
const category = header?.category || '';
const message = encodeURIComponent(
`Daily Checklist\n\nTanggal: ${formatDate(header?.date || '')}\nKandang: ${header?.kandang_name}\nKategori: ${CATEGORY_LABELS[category] || category}\nProgress: ${header?.progress_percent}%\nStatus: ${header?.status}${statusMsg ? ` - ${statusMsg}` : ''}\n\nLihat detail lengkap: ${window.location.href}`
);
setIsGeneratingImage(false);
const whatsappUrl = isMobile
? `https://wa.me/?text=${message}`
: `https://web.whatsapp.com/send?text=${message}`;
window.open(whatsappUrl, '_blank');
};
if (loading) { if (loading) {
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
@@ -573,8 +678,8 @@ export function DetailDailyChecklistContent() {
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
<div className='p-6'> <div className='p-6'>
{/* Page Title with Back Button */} {/* Action Buttons */}
<div className='mb-6 flex items-center gap-4'> <div className='mb-6 flex items-start sm:items-center justify-between gap-4 flex-wrap'>
<Button <Button
variant='outline' variant='outline'
size='sm' size='sm'
@@ -584,37 +689,68 @@ export function DetailDailyChecklistContent() {
<ArrowLeft className='w-4 h-4 mr-1' /> <ArrowLeft className='w-4 h-4 mr-1' />
Kembali Kembali
</Button> </Button>
<div className='flex-1'>
<h1 className='text-2xl font-semibold text-gray-900'> <div className='flex items-center gap-2 flex-wrap'>
Detail Daily Checklist {header.status === 'SUBMITTED' && (
</h1> <RequirePermission permissions='lti.daily_checklist.create'>
<p className='text-sm text-gray-600 mt-1'> <div className='flex gap-2 flex-wrap'>
Lihat detail checklist harian <Button
</p> onClick={handleApprove}
disabled={actionLoading}
className='bg-green-600 hover:bg-green-700 text-white'
>
<CheckCircle className='w-4 h-4 mr-2' />
Approve
</Button>
<Button
onClick={handleReject}
disabled={actionLoading}
variant='destructive'
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-2' />
Reject
</Button>
</div>
</RequirePermission>
)}
<Button
variant='outline'
size='sm'
onClick={shareHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Share2 className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan'}
{isGeneratingImage && 'Memuat...'}
</Button>
<Button
variant='outline'
size='sm'
onClick={shareToWhatsAppHandler}
disabled={isGeneratingImage}
className='border-gray-200'
>
<Icon icon='mdi:whatsapp' className='w-4 h-4 mr-1' />
{!isGeneratingImage && 'Bagikan via WhatsApp'}
{isGeneratingImage && 'Memuat...'}
</Button>
</div> </div>
{header.status === 'SUBMITTED' && ( </div>
<RequirePermission permissions='lti.daily_checklist.create'>
<div className='flex gap-2'> {/* Page Title */}
<Button <div className='mb-6'>
onClick={handleApprove} <h1 className='text-2xl font-semibold text-gray-900'>
disabled={actionLoading} Detail Daily Checklist
className='bg-green-600 hover:bg-green-700 text-white' </h1>
> <p className='text-sm text-gray-600 mt-1'>
<CheckCircle className='w-4 h-4 mr-2' /> Lihat detail checklist harian
Approve </p>
</Button>
<Button
onClick={handleReject}
disabled={actionLoading}
variant='destructive'
className='bg-red-600 hover:bg-red-700 text-white'
>
<XCircle className='w-4 h-4 mr-2' />
Reject
</Button>
</div>
</RequirePermission>
)}
</div> </div>
{/* Header Info Card */} {/* Header Info Card */}
+5 -1
View File
@@ -15,7 +15,9 @@ export class DebtSupplierApiService extends BaseApiService<
supplier_ids?: string, supplier_ids?: string,
filter_by?: string, filter_by?: string,
start_date?: string, start_date?: string,
end_date?: string end_date?: string,
page?: number,
limit?: number
): Promise<BaseApiResponse<DebtSupplier[]> | undefined> { ): Promise<BaseApiResponse<DebtSupplier[]> | undefined> {
return await this.customRequest<BaseApiResponse<DebtSupplier[]>>( return await this.customRequest<BaseApiResponse<DebtSupplier[]>>(
`debt-supplier`, `debt-supplier`,
@@ -26,6 +28,8 @@ export class DebtSupplierApiService extends BaseApiService<
filter_by: filter_by, filter_by: filter_by,
start_date: start_date, start_date: start_date,
end_date: end_date, end_date: end_date,
page: page,
limit: limit,
}, },
} }
); );
+20 -4
View File
@@ -31,6 +31,8 @@ export type UseTableFilterOptions<TExtra extends Record<string, unknown>> = {
paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>; paramMap?: Partial<Record<keyof TableFilterState<TExtra>, string>>;
/** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */ /** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */
omitDefaultsInUrl?: boolean; omitDefaultsInUrl?: boolean;
/** Optional list of state keys that should never be serialized into the URL/query string */
excludeKeysFromUrl?: Partial<(keyof TableFilterState<TExtra>)[]>;
persist?: boolean; persist?: boolean;
storeName?: string; storeName?: string;
@@ -218,9 +220,12 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
); );
const extras = useMemo(() => { const extras = useMemo(() => {
const { page, pageSize, ...rest } = state as TableFilterState< const stateWithExtras = state as TableFilterState<Record<string, unknown>>;
Record<string, unknown> const rest = Object.fromEntries(
>; Object.entries(stateWithExtras).filter(
([key]) => key !== 'page' && key !== 'pageSize'
)
);
return rest as TExtra; return rest as TExtra;
}, [state]); }, [state]);
@@ -240,8 +245,13 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
const baseline = options?.omitDefaultsInUrl const baseline = options?.omitDefaultsInUrl
? (defaults as Record<string, unknown>) ? (defaults as Record<string, unknown>)
: null; : null;
const excludedKeys = new Set<string>(
(options?.excludeKeysFromUrl as string[] | undefined) ?? []
);
for (const key of Object.keys(source)) { for (const key of Object.keys(source)) {
if (excludedKeys.has(key)) continue;
const value = source[key]; const value = source[key];
if (value === undefined || value === null) continue; if (value === undefined || value === null) continue;
@@ -260,7 +270,13 @@ export function useTableFilter<TExtra extends Record<string, unknown>>(
if (serialized !== null) params.set(mapped, serialized); if (serialized !== null) params.set(mapped, serialized);
} }
return params; return params;
}, [state, defaults, options?.omitDefaultsInUrl, mapKey]); }, [
state,
defaults,
options?.omitDefaultsInUrl,
options?.excludeKeysFromUrl,
mapKey,
]);
/** Build query string (prefixed with '?', or empty string if none) */ /** Build query string (prefixed with '?', or empty string if none) */
const toQueryString = useCallback(() => { const toQueryString = useCallback(() => {
+1 -1
View File
@@ -5,7 +5,7 @@ import { RequestOptions } from '@/services/http/base';
import { redirectToSSO } from '@/lib/auth-helper'; import { redirectToSSO } from '@/lib/auth-helper';
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 }); const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 30_000 });
axiosClient.interceptors.response.use( axiosClient.interceptors.response.use(
(response) => response, (response) => response,
+5
View File
@@ -95,10 +95,15 @@ export type Marketing = BaseMetadata & BaseMarketing;
*/ */
export type MarketingFilter = { export type MarketingFilter = {
product_ids: number[]; product_ids: number[];
product_names: string[];
status: string; status: string;
status_name: string;
customer_id: number; customer_id: number;
customer_name: string;
project_flock_id?: number; project_flock_id?: number;
project_flock_name?: string;
project_flock_kandang_id?: number; project_flock_kandang_id?: number;
project_flock_kandang_name?: string;
}; };
/** /**
+6
View File
@@ -148,10 +148,16 @@ export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload;
export type PurchaseFilter = { export type PurchaseFilter = {
poDate: string; poDate: string;
category: string[]; category: string[];
category_labels?: { label: string; value: number }[];
status: string[]; status: string[];
supplier_id?: number; supplier_id?: number;
supplier_label?: string;
area_id?: number; area_id?: number;
area_label?: string;
location_id?: number; location_id?: number;
location_label?: string;
project_flock_id?: number; project_flock_id?: number;
project_flock_label?: string;
project_flock_kandang_id?: number; project_flock_kandang_id?: number;
project_flock_kandang_label?: string;
}; };