diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index c09b168a..f5da407f 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; -import { useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -10,16 +10,14 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent'; import { Expense } from '@/types/api/expense'; -import { getExpenseListReturnTo } from '@/lib/expense-list-navigation'; interface ExpenseDetailProps { initialValues?: Expense; } const ExpenseDetail: React.FC = ({ initialValues }) => { + const router = useRouter(); const [activeTab, setActiveTab] = useState('request'); - const searchParams = useSearchParams(); - const returnTo = getExpenseListReturnTo(searchParams); const expenseDetailTabs = useMemo(() => { const validTabs = [ @@ -50,8 +48,8 @@ const ExpenseDetail: React.FC = ({ initialValues }) => {
+ className='px-3 py-2.5' + /> @@ -874,19 +910,9 @@ const FinanceTable = () => { {/* Modal Footer */}
diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index ed34efc2..1ca68235 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -119,6 +119,8 @@ const InventoryAdjustmentTable = () => { productFilter: '', warehouseFilter: '', transactionTypeFilter: '', + productName: '', + warehouseName: '', }, paramMap: { page: 'page', @@ -131,6 +133,9 @@ const InventoryAdjustmentTable = () => { warehouseFilter: 'warehouse_id', transactionTypeFilter: 'transaction_type', }, + excludeKeysFromUrl: ['productName', 'warehouseName'], + persist: true, + storeName: 'inventory-adjustment-table', }); // ===== FILTER MODAL STATE ===== @@ -140,14 +145,16 @@ const InventoryAdjustmentTable = () => { const formik = useFormik({ initialValues: { product_id: null, - warehouse: null, + warehouse_id: null, transaction_type: null, }, validationSchema: AdjustmentFilterSchema, onSubmit: (values, { setSubmitting }) => { updateFilter('productFilter', values.product_id || ''); - updateFilter('warehouseFilter', String(values.warehouse?.value) || ''); + updateFilter('warehouseFilter', values.warehouse_id || ''); updateFilter('transactionTypeFilter', values.transaction_type || ''); + updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : ''); + updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : ''); filterModal.closeModal(); setSubmitting(false); }, @@ -155,6 +162,9 @@ const InventoryAdjustmentTable = () => { updateFilter('productFilter', ''); updateFilter('warehouseFilter', ''); updateFilter('transactionTypeFilter', ''); + updateFilter('productName', ''); + updateFilter('warehouseName', ''); + filterModal.closeModal(); }, }); @@ -205,7 +215,8 @@ const InventoryAdjustmentTable = () => { const handleFilterWarehouseChange = ( 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( @@ -220,12 +231,27 @@ const InventoryAdjustmentTable = () => { // ===== FILTER HELPERS ===== const productIdValue = useMemo(() => { if (!formik.values.product_id) return null; - return ( - productOptions.find( - (opt) => String(opt.value) === formik.values.product_id - ) || null + const found = productOptions.find( + (opt) => String(opt.value) === formik.values.product_id ); - }, [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(() => { if (!formik.values.transaction_type) return null; @@ -238,8 +264,12 @@ const InventoryAdjustmentTable = () => { // ===== HANDLE FILTER MODAL OPEN ===== const handleFilterModalOpen = () => { + formik.setValues({ + product_id: tableFilterState.productFilter || null, + warehouse_id: tableFilterState.warehouseFilter || null, + transaction_type: tableFilterState.transactionTypeFilter || null, + }); filterModal.openModal(); - formik.validateForm(); }; const { @@ -507,6 +537,8 @@ const InventoryAdjustmentTable = () => { 'productSort', 'warehouseSort', 'stockSort', + 'productName', + 'warehouseName', ]} onClick={handleFilterModalOpen} className='px-3 py-2.5' @@ -608,7 +640,7 @@ const InventoryAdjustmentTable = () => { label='Gudang' placeholder='Pilih Gudang' options={warehouseOptions} - value={formik.values.warehouse} + value={warehouseIdValue} onChange={handleFilterWarehouseChange} onInputChange={setWarehouseInputValue} isLoading={isLoadingWarehouseOptions} @@ -630,13 +662,9 @@ const InventoryAdjustmentTable = () => { {/* Modal Footer */}
diff --git a/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts index e4015e07..4568618f 100644 --- a/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts +++ b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts @@ -1,5 +1,4 @@ import { string, object } from 'yup'; -import { OptionType } from '@/components/input/SelectInput'; export const AdjustmentFilterSchema = object().shape({ product_id: string().nullable(), @@ -9,6 +8,6 @@ export const AdjustmentFilterSchema = object().shape({ export type AdjustmentFilterType = { product_id: string | null; + warehouse_id: string | null; transaction_type: string | null; - warehouse: OptionType | null; }; diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index 2b6f11e6..d2a808d3 100644 --- a/src/components/pages/inventory/movement/MovementTable.tsx +++ b/src/components/pages/inventory/movement/MovementTable.tsx @@ -122,6 +122,8 @@ const MovementTable = () => { search: '', productFilter: '', warehouseFilter: '', + productName: '', + warehouseName: '', }, paramMap: { page: 'page', @@ -129,6 +131,9 @@ const MovementTable = () => { productFilter: 'product_id', warehouseFilter: 'warehouse_id', }, + excludeKeysFromUrl: ['productName', 'warehouseName'], + persist: true, + storeName: 'movement-table', }); // ===== FILTER MODAL STATE ===== @@ -144,12 +149,17 @@ const MovementTable = () => { onSubmit: (values, { setSubmitting }) => { updateFilter('productFilter', values.product_id || ''); updateFilter('warehouseFilter', values.warehouse_id || ''); + updateFilter('productName', productIdValue?.label ? String(productIdValue.label) : ''); + updateFilter('warehouseName', warehouseIdValue?.label ? String(warehouseIdValue.label) : ''); filterModal.closeModal(); setSubmitting(false); }, onReset: () => { updateFilter('productFilter', ''); updateFilter('warehouseFilter', ''); + updateFilter('productName', ''); + updateFilter('warehouseName', ''); + filterModal.closeModal(); }, }); @@ -201,26 +211,35 @@ const MovementTable = () => { // ===== FILTER HELPERS ===== const productIdValue = useMemo(() => { if (!formik.values.product_id) return null; - return ( - productOptions.find( - (opt) => String(opt.value) === formik.values.product_id - ) || null + const found = productOptions.find( + (opt) => String(opt.value) === formik.values.product_id ); - }, [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; - return ( - warehouseOptions.find( - (opt) => String(opt.value) === formik.values.warehouse_id - ) || null + const found = warehouseOptions.find( + (opt) => String(opt.value) === formik.values.warehouse_id ); - }, [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 ===== const handleFilterModalOpen = () => { + formik.setValues({ + product_id: tableFilterState.productFilter || null, + warehouse_id: tableFilterState.warehouseFilter || null, + }); filterModal.openModal(); - formik.validateForm(); }; const [sorting, setSorting] = useState([]); @@ -384,7 +403,7 @@ const MovementTable = () => { @@ -489,13 +508,9 @@ const MovementTable = () => { {/* Modal Footer */}
diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index 21ded2bc..ad712e24 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -4,17 +4,25 @@ import Button from '@/components/Button'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Table from '@/components/Table'; 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 { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { InventoryProductApi } from '@/services/api/inventory'; +import { ProductCategoryApi } from '@/services/api/master-data'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useUiStore } from '@/stores/ui/ui.store'; import { InventoryProduct } from '@/types/api/inventory/product'; +import { ProductCategory } from '@/types/api/master-data/product-category'; import { Icon } from '@iconify/react'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import { usePathname } from 'next/navigation'; import useSWR from 'swr'; +import { useFormik } from 'formik'; +import { object, string } from 'yup'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton'; @@ -83,13 +91,76 @@ const InventoryProductTable = () => { } = useTableFilter({ initial: { search: '', + categoryFilter: '', + categoryName: '', }, paramMap: { page: 'page', 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( + 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([]); const { data: inventoryProducts, isLoading } = useSWR( @@ -182,6 +253,7 @@ const InventoryProductTable = () => { ); return ( + <>
{/* Header Section */}
@@ -199,7 +271,7 @@ const InventoryProductTable = () => {
- {/* Search */} + {/* Search and Filter */}
{ 'placeholder:font-semibold placeholder:text-base-content/50', }} /> +
@@ -272,6 +350,62 @@ const InventoryProductTable = () => { )}
+ + {/* Filter Modal */} + +
+
+ +

Filter Data

+
+ +
+
+
+ +
+
+ + +
+
+
+ ); }; diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index 86298598..8e1ab8c0 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useMemo } from 'react'; +import { RefObject, useCallback, useMemo } from 'react'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Modal from '@/components/Modal'; @@ -17,22 +17,31 @@ import { import { MarketingFilter } from '@/types/api/marketing/marketing'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; 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 { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlock } from '@/types/api/production/project-flock'; +import { Product } from '@/types/api/master-data/product'; interface MarketingFilterModal { ref: RefObject; onSubmit?: (values: MarketingFilter) => void; onReset?: () => void; + initialValues?: { + product_ids: OptionType[]; + status: OptionType | null; + customer: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; + }; } const MarketingFilterModal = ({ ref, onSubmit, onReset, + initialValues, }: MarketingFilterModal) => { const closeModalHandler = () => { ref.current?.close(); @@ -40,36 +49,13 @@ const MarketingFilterModal = ({ // ===== OPTIONS ===== const { - rawData: productsRawData, + options: productsOptions, isLoadingOptions: isLoadingProductsOptions, setInputValue: setProductsInputValue, loadMore: loadMoreProducts, - } = useSelect( - MarketingApi.basePath, - 'id', - 'so_number', - 'search' - ); - - const productsOptions = useMemo(() => { - if (!productsRawData || !isResponseSuccess(productsRawData)) return []; - - const productsMap = new Map(); - - 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]); + } = useSelect(ProductApi.basePath, 'id', 'name', 'search', { + include_all: 'true', + }); const { options: customersOptions, @@ -102,7 +88,7 @@ const MarketingFilterModal = ({ ]; const formik = useFormik({ - initialValues: { + initialValues: initialValues || { product_ids: [], status: null, customer: null, @@ -114,11 +100,17 @@ const MarketingFilterModal = ({ onSubmit: async (values) => { const formattedValues: MarketingFilter = { 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_name: values.status?.label || '-', 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: Number(values.project_flock_kandang?.value) || undefined, + project_flock_kandang_name: + values.project_flock_kandang?.label || undefined, }; 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) => { formik.setFieldValue('product_ids', val as OptionType[]); }; @@ -176,7 +184,7 @@ const MarketingFilterModal = ({ >
{/* Modal Header */} diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 1bcc54d5..06949171 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -189,10 +189,15 @@ const MarketingTable = () => { initial: { search: '', product_ids: '', + product_names: '', status: '', + status_name: '', customer_id: '', + customer_name: '', project_flock_id: '', + project_flock_name: '', project_flock_kandang_id: '', + project_flock_kandang_name: '', }, paramMap: { page: 'page', @@ -203,6 +208,13 @@ const MarketingTable = () => { project_flock_id: 'project_flock_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, storeName: 'marketing-table', @@ -225,17 +237,21 @@ const MarketingTable = () => { values.product_ids?.map((item) => item.toString()).join(','), true ); + updateFilter('product_names', values.product_names?.join(',')); updateFilter('status', values.status ? values.status.toString() : '', true); + updateFilter('status_name', values.status_name, true); updateFilter( 'customer_id', values.customer_id ? values.customer_id.toString() : '', true ); + updateFilter('customer_name', values.customer_name, true); updateFilter( 'project_flock_id', values.project_flock_id ? values.project_flock_id.toString() : '', true ); + updateFilter('project_flock_name', values.project_flock_name ?? '', true); updateFilter( 'project_flock_kandang_id', values.project_flock_kandang_id @@ -243,6 +259,11 @@ const MarketingTable = () => { : '', true ); + updateFilter( + 'project_flock_kandang_name', + values.project_flock_kandang_name ?? '', + true + ); }; const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = @@ -250,10 +271,15 @@ const MarketingTable = () => { const filterResetHandler = () => { updateFilter('product_ids', '', true); + updateFilter('product_names', '', true); updateFilter('status', '', true); + updateFilter('status_name', '', true); updateFilter('customer_id', '', true); + updateFilter('customer_name', '', true); updateFilter('project_flock_id', '', true); + updateFilter('project_flock_name', '', true); updateFilter('project_flock_kandang_id', '', true); + updateFilter('project_flock_kandang_name', '', true); }; const approveClickHandler = () => { @@ -333,6 +359,56 @@ const MarketingTable = () => { ? 'DELIVERY_ORDER' : 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) => { if (idsToProcess.length === 0) { toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`); @@ -735,7 +811,7 @@ const MarketingTable = () => { {idsToProcess.length > 0 && ( <> -
+
@@ -765,7 +841,7 @@ const MarketingTable = () => { width={20} height={20} /> - Approve + Approve ({idsToProcess.length} Item) @@ -774,7 +850,16 @@ const MarketingTable = () => {
{ filterModal.openModal(); }} @@ -1146,6 +1231,7 @@ const MarketingTable = () => { ref={filterModal.ref} onSubmit={filterSubmitHandler} onReset={filterResetHandler} + initialValues={marketingFilterInitialValues} /> ); diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index e3018e38..1e6145f3 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -172,6 +172,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { kandang_id: '', category: '', period: '', + area_name: '', + location_name: '', + kandang_name: '', }, paramMap: { page: 'page', @@ -183,7 +186,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { category: 'category', period: 'period', }, - + excludeKeysFromUrl: ['area_name', 'location_name', 'kandang_name'], persist: true, storeName: 'project-flock-table', }); @@ -259,6 +262,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { updateFilter('kandang_id', values.kandang_id || ''); updateFilter('category', values.category || ''); 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(); setSubmitting(false); }, @@ -268,6 +274,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { updateFilter('kandang_id', ''); updateFilter('category', ''); updateFilter('period', ''); + updateFilter('area_name', ''); + updateFilter('location_name', ''); + updateFilter('kandang_name', ''); setFilterAreaId(undefined); setFilterLocationId(undefined); filterModal.closeModal(); @@ -320,29 +329,37 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { // ===== FILTER HELPERS ===== const areaValue = useMemo(() => { if (!formik.values.area_id) return null; - return ( - areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || - null - ); - }, [formik.values.area_id, areaOptions]); + const found = areaOptions.find((opt) => String(opt.value) === formik.values.area_id); + if (found) return found; + if (tableFilterState.area_name) { + return { value: formik.values.area_id, label: tableFilterState.area_name }; + } + return null; + }, [formik.values.area_id, areaOptions, tableFilterState.area_name]); const locationValue = useMemo(() => { if (!formik.values.location_id) return null; - return ( - locationOptions.find( - (opt) => String(opt.value) === formik.values.location_id - ) || null + const found = locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id ); - }, [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(() => { if (!formik.values.kandang_id) return null; - return ( - kandangOptions.find( - (opt) => String(opt.value) === formik.values.kandang_id - ) || null + const found = kandangOptions.find( + (opt) => String(opt.value) === formik.values.kandang_id ); - }, [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(() => { if (!formik.values.category) return null; @@ -967,7 +984,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx index b00af1eb..e0ed8fc8 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFilterModal.tsx @@ -13,7 +13,6 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import { OptionType, useSelect } from '@/components/input/SelectInput'; import { ProjectFlockApi } from '@/services/api/production'; import { Flock } from '@/types/api/master-data/flock'; -import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying'; import { TransferToLayingFilterSchema, TransferToLayingFilterValues, @@ -21,12 +20,14 @@ import { interface TransferToLayingFilterModal { ref: RefObject; - onSubmit?: (values: TransferToLayingFilter) => void; + initialValues?: Partial; + onSubmit?: (values: TransferToLayingFilterValues) => void; onReset?: () => void; } const TransferToLayingFilterModal = ({ ref, + initialValues: initialValuesProp, onSubmit, onReset, }: TransferToLayingFilterModal) => { @@ -86,28 +87,16 @@ const TransferToLayingFilterModal = ({ const formik = useFormik({ initialValues: { - startDate: '', - endDate: '', - flockSource: [], - flockDestination: [], - status: [], + startDate: initialValuesProp?.startDate ?? '', + endDate: initialValuesProp?.endDate ?? '', + flockSource: initialValuesProp?.flockSource ?? [], + flockDestination: initialValuesProp?.flockDestination ?? [], + status: initialValuesProp?.status ?? [], }, + enableReinitialize: true, validationSchema: TransferToLayingFilterSchema, onSubmit: async (values) => { - const formattedValues = { - ...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); + onSubmit?.(values); closeModalHandler(); }, onReset: () => { diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index e46cd476..3891cd4a 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import { usePathname } from 'next/navigation'; import { useUiStore } from '@/stores/ui/ui.store'; 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 TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton'; -import { - TransferToLaying, - TransferToLayingFilter, -} from '@/types/api/production/transfer-to-laying'; +import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; +import { TransferToLayingFilterValues } from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter'; +import { OptionType } from '@/components/input/SelectInput'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { cn, formatDate, formatNumber } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -142,6 +141,8 @@ const TransferToLayingsTable = () => { status: '', filter_by: '', sort_by: '', + flockSourceNames: '', + flockDestinationNames: '', }, paramMap: { page: 'page', @@ -154,6 +155,9 @@ const TransferToLayingsTable = () => { filter_by: 'filter_by', sort_by: 'sort_by', }, + excludeKeysFromUrl: ['flockSourceNames', 'flockDestinationNames'], + persist: true, + storeName: 'transfer-to-laying-table', }); const { @@ -431,12 +435,72 @@ const TransferToLayingsTable = () => { updateFilter('search', e.target.value); }; - const filterSubmitHandler = (values: TransferToLayingFilter) => { - updateFilter('startDate', values.startDate); - updateFilter('endDate', values.endDate); - updateFilter('flockSource', values.flockSource.join(',')); - updateFilter('flockDestination', values.flockDestination.join(',')); - updateFilter('status', values.status.join(',')); + const STATUS_FILTER_OPTIONS = [ + { value: 'PENDING', label: 'Pengajuan' }, + { value: 'APPROVED', label: 'Disetujui' }, + { value: 'REJECTED', label: 'Ditolak' }, + ]; + + 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 = () => { @@ -445,6 +509,8 @@ const TransferToLayingsTable = () => { updateFilter('flockSource', ''); updateFilter('flockDestination', ''); updateFilter('status', ''); + updateFilter('flockSourceNames', ''); + updateFilter('flockDestinationNames', ''); }; const exportToExcelHandler = async () => { @@ -558,6 +624,8 @@ const TransferToLayingsTable = () => { 'search', 'filter_by', 'sort_by', + 'flockSourceNames', + 'flockDestinationNames', ]} fieldGroups={[['startDate', 'endDate']]} onClick={filterModal.openModal} @@ -670,6 +738,7 @@ const TransferToLayingsTable = () => { diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx index bcb80fe6..856f2074 100644 --- a/src/components/pages/purchase/PurchaseFilterModal.tsx +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useState, useEffect, useMemo } from 'react'; +import { RefObject, useState, useEffect, useMemo, useCallback } from 'react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; @@ -26,22 +26,32 @@ import { isResponseSuccess } from '@/lib/api-helper'; interface PurchaseFilterModalProps { ref: RefObject; + initialValues?: { + poDate: string; + category: OptionType[]; + status: OptionType[]; + supplier: OptionType | null; + area: OptionType | null; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; + }; onSubmit?: (values: PurchaseFilter) => void; onReset?: () => void; } const PurchaseFilterModal = ({ ref, + initialValues, onSubmit, onReset, }: PurchaseFilterModalProps) => { - const closeModalHandler = () => { + const closeModalHandler = useCallback(() => { ref.current?.close(); - }; + }, [ref]); // ===== DATE ERROR STATE ===== const [dateErrorShown, setDateErrorShown] = useState(false); - const [hasDateError, setHasDateError] = useState(false); // ===== CLEANUP TOAST ON UNMOUNT ===== useEffect(() => { @@ -81,8 +91,12 @@ const PurchaseFilterModal = ({ 'search' ); - const [selectedAreaId, setSelectedAreaId] = useState(''); - const [selectedLocationId, setSelectedLocationId] = useState(''); + const [selectedAreaId, setSelectedAreaId] = useState( + initialValues?.area?.value ? String(initialValues.area.value) : '' + ); + const [selectedLocationId, setSelectedLocationId] = useState( + initialValues?.location?.value ? String(initialValues.location.value) : '' + ); const { setInputValue: setSupplierInputValue, @@ -133,7 +147,8 @@ const PurchaseFilterModal = ({ project_flock: OptionType | null; project_flock_kandang: OptionType | null; }>({ - initialValues: { + // enableReinitialize: true, + initialValues: initialValues || { poDate: '', category: [], status: [], @@ -147,12 +162,18 @@ const PurchaseFilterModal = ({ const formattedValues = { ...values, category: values.category.map((item) => String(item.value)), + category_labels: values.category, status: values.status.map((item) => String(item.value)), supplier_id: values.supplier?.value, + supplier_label: values.supplier?.label, area_id: values.area?.value, + area_label: values.area?.label, location_id: values.location?.value, + location_label: values.location?.label, 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_label: values.project_flock_kandang?.label, }; 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(() => { if ( !formik.values.project_flock || @@ -197,6 +229,29 @@ const PurchaseFilterModal = ({ 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 ( {/* Modal Header */} @@ -220,7 +275,9 @@ const PurchaseFilterModal = ({ type='button' variant='ghost' color='none' - onClick={closeModalHandler} + onClick={() => { + closeModalHandler(); + }} className='p-0 text-base-content/50 hover:text-base-content' > @@ -377,7 +434,8 @@ const PurchaseFilterModal = ({
{/* Filter Modal */} diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index a425a66b..8d760208 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; +import Pagination from '@/components/Pagination'; import DateInput from '@/components/input/DateInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; @@ -78,6 +79,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + // ===== PAGINATION STATE ===== + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + // ===== SUBMISSION STATE ===== const [filterParams, setFilterParams] = useState({ start_date: undefined, @@ -128,7 +133,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { filter_by: values.filterBy?.value?.toString() || undefined, }); filterModal.closeModal(); - // setIsSubmitted(true); + setCurrentPage(1); }, onReset: () => { setFilterParams({ @@ -137,14 +142,30 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { supplier_ids: undefined, filter_by: undefined, }); - // setIsSubmitted(false); + setCurrentPage(1); filterModal.closeModal(); }, }); 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(); - formik.validateForm(); }; // ===== DATA FETCHING ===== @@ -155,6 +176,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { filter_by: filterParams.filter_by, start_date: filterParams.start_date, end_date: filterParams.end_date, + page: currentPage, + limit: pageSize, }; return ['debt-supplier-report', params]; @@ -164,7 +187,9 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { params.supplier_ids, params.filter_by, params.start_date, - params.end_date + params.end_date, + params.page, + params.limit ) ); @@ -176,6 +201,14 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { [debtSupplier] ); + const meta = useMemo( + () => + isResponseSuccess(debtSupplier) && debtSupplier.meta + ? debtSupplier.meta + : null, + [debtSupplier] + ); + // ===== EXPORT DATA FETCHER ===== const debtSupplierExport = useCallback(async (): Promise< DebtSupplier[] | null @@ -717,6 +750,27 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { ); })} + + {!isLoading && data.length > 0 && meta && ( +
+ + 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} + /> +
+ )}
{/* Filter Modal */} diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index a0a4cabc..9a9bb6c2 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -156,8 +156,17 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { }); 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(); - formik.validateForm(); }; const { setFieldValue } = formik; diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 01e1eea9..167be1fa 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -156,8 +156,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }); 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(); - formik.validateForm(); }; // ===== SEARCH CHANGE HANDLER ===== diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index c290618a..6ad9a7f0 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -152,8 +152,19 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { }); 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(); - formik.validateForm(); }; // ===== WEIGHT CHANGE HANDLERS ===== diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index 8f5fbdc9..973b3083 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -263,8 +263,30 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { }); 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(); - formik.validateForm(); }; const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] = diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index e0a95cd0..b83c7b6a 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -53,7 +53,6 @@ import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import { Icon } from '@iconify/react'; import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang'; -// Static categories const CATEGORIES = [ { value: 'pullet_open', label: 'Pullet Open' }, { value: 'pullet_close', label: 'Pullet Close' }, @@ -62,6 +61,14 @@ const CATEGORIES = [ { 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_LABELS: { [key: string]: string } = { Umum: 'Umum', @@ -98,6 +105,8 @@ export function DailyChecklistContent() { const [emptyKandang, setEmptyKandang] = useState(false); const [emptyKandangEndDate, setEmptyKandangEndDate] = useState(''); + const isKandangEmpty = selectedCategory === 'empty_kandang'; + const { options: kandangOptions, isLoadingMore: isLoadingMoreKandang, @@ -244,7 +253,6 @@ export function DailyChecklistContent() { } }, [selectedCategory]); - // Format date for display const formatDateForDisplay = (dateStr: string) => { if (!dateStr) return 'Pilih tanggal'; 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 useEffect(() => { setInitialLoading(false); @@ -298,7 +336,7 @@ export function DailyChecklistContent() { if (isResponseError(checklist)) { console.error('Error upserting checklist:', checklist.message); - toast.error('Gagal memuat checklist'); + toast.error('Gagal memuat checklist: ' + checklist.message); return; } @@ -311,6 +349,12 @@ export function DailyChecklistContent() { if (isResponseError(existingPhases)) { 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 ( existingPhases && existingPhases.data && @@ -834,7 +878,43 @@ export function DailyChecklistContent() { } 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: ( + + ), + }); } catch (error) { console.error('Error submitting:', error); toast.error('Terjadi kesalahan'); @@ -1118,7 +1198,7 @@ export function DailyChecklistContent() {
{/* Phase Selection Section */} - {dailyChecklistId && ( + {!isKandangEmpty && dailyChecklistId && (
{isChecklistStatusDraft && (
@@ -1159,298 +1239,314 @@ export function DailyChecklistContent() { )} {/* ABK Assignment Section */} - {dailyChecklistId && selectedPhaseIds.length > 0 && ( -
- {isChecklistStatusDraft && ( -
- - -
- )} - - {selectedEmployees.length > 0 ? ( -
- {selectedEmployees.map((emp) => ( - 0 && ( +
+ {isChecklistStatusDraft && ( +
+ + - )} - - ))} -
- ) : ( -

Belum ada ABK dipilih

- )} -
- )} + + Tambah ABK + +
+ )} + + {selectedEmployees.length > 0 ? ( +
+ {selectedEmployees.map((emp) => ( + + {emp.name} + {isChecklistStatusDraft && ( + + )} + + ))} +
+ ) : ( +

+ Belum ada ABK dipilih +

+ )} +
+ )} {/* Activity Checklist Table */} - {dailyChecklistId && - selectedPhaseIds.length > 0 && - selectedEmployees.length > 0 ? ( -
-

- Checklist Aktivitas -

- {Object.keys(activitiesByPhase).length > 0 ? ( -
- - - - - {sortedSelectedEmployees.map((emp) => ( - - ))} - - - - - {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( - - + + {sortedSelectedEmployees.map((emp) => ( + + ))} + + + ); + }); + }); + + return rows; + } + )} + +
- Aktivitas - - {emp.name} - - Catatan -
+ {dailyChecklistId && + selectedPhaseIds.length > 0 && + selectedEmployees.length > 0 ? ( +
+

+ Checklist Aktivitas +

+ {Object.keys(activitiesByPhase).length > 0 ? ( +
+ + + + + {sortedSelectedEmployees.map((emp) => ( + - ); + {emp.name} + + ))} + + + + + {Object.keys(groupActivitiesByPhase()).flatMap( + (phaseId) => { + const phaseData = + groupActivitiesByPhase()[phaseId]; + const { phase, timeGroups } = phaseData; - // TIME_TYPE sub-headers and activities - timeTypes.forEach((timeType) => { - const activities = timeGroups[timeType]; - const hasMultipleTimeTypes = timeTypes.length > 1; + const timeTypes = Object.keys(timeGroups).sort( + (a, b) => + TIME_TYPE_ORDER.indexOf(a) - + TIME_TYPE_ORDER.indexOf(b) + ); - // TIME Header (optional, only if phase has multiple time types) - GRAY SOFT - if (hasMultipleTimeTypes) { + // 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( ); - } - // ACTIVITY rows (Child rows with checkboxes) - activities.sort((a, b) => - a.name.localeCompare(b.name, undefined, { - sensitivity: 'base', - }) - ); + // TIME_TYPE sub-headers and activities + timeTypes.forEach((timeType) => { + const activities = timeGroups[timeType]; + const hasMultipleTimeTypes = + timeTypes.length > 1; - activities.forEach((activity, index) => { - const taskId = - taskIdsByPhaseActivityId[activity.id]; - const indentClass = hasMultipleTimeTypes - ? 'pl-12' - : 'pl-8'; - - rows.push( - - - {sortedSelectedEmployees.map((emp) => ( - - ))} - - - ); - }); - }); + + + ); + } - return rows; - } - )} - -
+ Aktivitas + -
- - {phase.name} - - - {totalActivities} aktivitas - -
- -
+ Catatan +
- - {TIME_TYPE_LABELS[timeType]} ( - {activities.length} aktivitas) - +
+ + {phase.name} + + + {totalActivities} aktivitas + +
-

- {activity.name} -

- {activity.description && ( -

- {activity.description} -

- )} -
- - handleCheckboxChange( - String(activity.id), - String(emp.id), - e.target.checked - ) - } - disabled={!isChecklistStatusDraft} - className='checkbox-clean' - /> - - 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} - /> -
+ + {TIME_TYPE_LABELS[timeType]} ( + {activities.length} aktivitas) + +
+ // ACTIVITY rows (Child rows with checkboxes) + activities.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: 'base', + }) + ); + + activities.forEach((activity, index) => { + const taskId = + taskIdsByPhaseActivityId[activity.id]; + const indentClass = hasMultipleTimeTypes + ? 'pl-12' + : 'pl-8'; + + rows.push( +
+

+ {activity.name} +

+ {activity.description && ( +

+ {activity.description} +

+ )} +
+ + handleCheckboxChange( + String(activity.id), + String(emp.id), + e.target.checked + ) + } + disabled={!isChecklistStatusDraft} + className='checkbox-clean' + /> + + 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} + /> +
+
+ ) : ( +
+ +

+ Tidak Ada Aktivitas +

+

+ Tidak ada aktivitas untuk fase yang dipilih. Silakan + tambahkan aktivitas di Master Aktivitas. +

+
+ )}
) : (
- -

- Tidak Ada Aktivitas -

-

- Tidak ada aktivitas untuk fase yang dipilih. Silakan - tambahkan aktivitas di Master Aktivitas. -

+ {!dailyChecklistId ? ( +
+ +

+ Mulai Checklist Baru +

+

+ Pilih tanggal, kandang, dan kategori untuk memulai + checklist harian Anda. +

+
+ ) : selectedPhaseIds.length === 0 ? ( +
+ +

+ Pilih Fase / Tahap +

+

+ Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap + aktivitas yang akan dikerjakan. +

+
+ ) : ( +
+ +

+ Pilih ABK +

+

+ Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja + yang akan ditugaskan. +

+
+ )}
)} -
- ) : ( -
- {!dailyChecklistId ? ( -
- -

- Mulai Checklist Baru -

-

- Pilih tanggal, kandang, dan kategori untuk memulai - checklist harian Anda. -

-
- ) : selectedPhaseIds.length === 0 ? ( -
- -

- Pilih Fase / Tahap -

-

- Klik tombol {'"'}Pilih Fase{'"'} untuk memilih tahap - aktivitas yang akan dikerjakan. -

-
- ) : ( -
- -

- Pilih ABK -

-

- Klik tombol {'"'}Tambah ABK{'"'} untuk memilih pekerja - yang akan ditugaskan. -

-
- )} -
+ )} - {dailyChecklistId && + {!isKandangEmpty && + dailyChecklistId && selectedPhaseIds.length > 0 && selectedEmployees.length > 0 && ( <> @@ -1548,7 +1644,8 @@ export function DailyChecklistContent() { )} {/* Action Buttons */} - {dailyChecklistId && + {!isKandangEmpty && + dailyChecklistId && selectedPhaseIds.length > 0 && selectedEmployees.length > 0 && isChecklistStatusDraft && ( diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index c9d8d21d..9af93ee8 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -2,7 +2,14 @@ import { useState, useEffect } 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 { Button } from '@/figma-make/components/base/button'; import { Badge } from '@/figma-make/components/base/badge'; @@ -137,6 +144,8 @@ export function DetailDailyChecklistContent() { const [rejectReason, setRejectReason] = useState(''); const [actionLoading, setActionLoading] = useState(false); + const [isGeneratingImage, setIsGeneratingImage] = useState(false); + useEffect(() => { if (checklistId) { 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) { return (
@@ -573,8 +678,8 @@ export function DetailDailyChecklistContent() { return (
- {/* Page Title with Back Button */} -
+ {/* Action Buttons */} +
-
-

- Detail Daily Checklist -

-

- Lihat detail checklist harian -

+ +
+ {header.status === 'SUBMITTED' && ( + +
+ + +
+
+ )} + + + +
- {header.status === 'SUBMITTED' && ( - -
- - -
-
- )} +
+ + {/* Page Title */} +
+

+ Detail Daily Checklist +

+

+ Lihat detail checklist harian +

{/* Header Info Card */} diff --git a/src/services/api/report/debt-supplier.ts b/src/services/api/report/debt-supplier.ts index 706c873e..dad46d18 100644 --- a/src/services/api/report/debt-supplier.ts +++ b/src/services/api/report/debt-supplier.ts @@ -15,7 +15,9 @@ export class DebtSupplierApiService extends BaseApiService< supplier_ids?: string, filter_by?: string, start_date?: string, - end_date?: string + end_date?: string, + page?: number, + limit?: number ): Promise | undefined> { return await this.customRequest>( `debt-supplier`, @@ -26,6 +28,8 @@ export class DebtSupplierApiService extends BaseApiService< filter_by: filter_by, start_date: start_date, end_date: end_date, + page: page, + limit: limit, }, } ); diff --git a/src/services/hooks/useTableFilter.tsx b/src/services/hooks/useTableFilter.tsx index 43fc173c..5db57bcb 100644 --- a/src/services/hooks/useTableFilter.tsx +++ b/src/services/hooks/useTableFilter.tsx @@ -31,6 +31,8 @@ export type UseTableFilterOptions> = { paramMap?: Partial, string>>; /** If true, `toSearchParams`/`toQueryString` will omit values equal to defaults */ omitDefaultsInUrl?: boolean; + /** Optional list of state keys that should never be serialized into the URL/query string */ + excludeKeysFromUrl?: Partial<(keyof TableFilterState)[]>; persist?: boolean; storeName?: string; @@ -218,9 +220,12 @@ export function useTableFilter>( ); const extras = useMemo(() => { - const { page, pageSize, ...rest } = state as TableFilterState< - Record - >; + const stateWithExtras = state as TableFilterState>; + const rest = Object.fromEntries( + Object.entries(stateWithExtras).filter( + ([key]) => key !== 'page' && key !== 'pageSize' + ) + ); return rest as TExtra; }, [state]); @@ -240,8 +245,13 @@ export function useTableFilter>( const baseline = options?.omitDefaultsInUrl ? (defaults as Record) : null; + const excludedKeys = new Set( + (options?.excludeKeysFromUrl as string[] | undefined) ?? [] + ); for (const key of Object.keys(source)) { + if (excludedKeys.has(key)) continue; + const value = source[key]; if (value === undefined || value === null) continue; @@ -260,7 +270,13 @@ export function useTableFilter>( if (serialized !== null) params.set(mapped, serialized); } return params; - }, [state, defaults, options?.omitDefaultsInUrl, mapKey]); + }, [ + state, + defaults, + options?.omitDefaultsInUrl, + options?.excludeKeysFromUrl, + mapKey, + ]); /** Build query string (prefixed with '?', or empty string if none) */ const toQueryString = useCallback(() => { diff --git a/src/services/http/client.ts b/src/services/http/client.ts index c70a82ea..cb22c2f4 100644 --- a/src/services/http/client.ts +++ b/src/services/http/client.ts @@ -5,7 +5,7 @@ import { RequestOptions } from '@/services/http/base'; import { redirectToSSO } from '@/lib/auth-helper'; 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( (response) => response, diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index e9967a2b..ee3a95d3 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -95,10 +95,15 @@ export type Marketing = BaseMetadata & BaseMarketing; */ export type MarketingFilter = { product_ids: number[]; + product_names: string[]; status: string; + status_name: string; customer_id: number; + customer_name: string; project_flock_id?: number; + project_flock_name?: string; project_flock_kandang_id?: number; + project_flock_kandang_name?: string; }; /** diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index d2dec108..0fe5562e 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -148,10 +148,16 @@ export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; export type PurchaseFilter = { poDate: string; category: string[]; + category_labels?: { label: string; value: number }[]; status: string[]; supplier_id?: number; + supplier_label?: string; area_id?: number; + area_label?: string; location_id?: number; + location_label?: string; project_flock_id?: number; + project_flock_label?: string; project_flock_kandang_id?: number; + project_flock_kandang_label?: string; };