mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into 'production'
Development See merge request mbugroup/lti-web-client!432
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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'
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-21
@@ -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
|
||||||
|
|||||||
@@ -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 =====
|
||||||
|
|||||||
+23
-1
@@ -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 && (
|
||||||
|
|||||||
+169
-33
@@ -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 */}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Vendored
+6
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user