diff --git a/.husky/pre-commit b/.husky/pre-commit index f799d12f..de6b606b 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,4 @@ npm run format npm run lint -npm run typecheck \ No newline at end of file +npm run typecheck +git add . diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 61fa7fa6..52b3e773 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -41,7 +41,7 @@ import Dropdown from '@/components/dropdown/Dropdown'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; import { cn, formatCurrency, formatDate } from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { BaseApiResponse } from '@/types/api/api-general'; @@ -51,7 +51,16 @@ type ExpenseTableFilters = { transactionDate: string; realizationDate: string; locationId: string; + locationName: string; vendorId: string; + vendorName: string; + category: string; + approvalStatus: string; + realizationStatus: string; + projectFlockId: string; + projectFlockName: string; + projectFlockKandangId: string; + projectFlockKandangName: string; userId: string; }; @@ -75,43 +84,6 @@ type ApprovalStatusValue = const isApprovalDateRequired = (status?: ApprovalStatusValue) => status === 'REALISASI' || status === 'SELESAI'; -const getExportErrorMessage = async ( - error: unknown, - fallbackMessage: string -) => { - if (axios.isAxiosError(error)) { - const responseData = error.response?.data; - - if (responseData instanceof Blob) { - try { - const parsed = JSON.parse(await responseData.text()) as { - message?: string; - }; - return parsed.message || fallbackMessage; - } catch { - return fallbackMessage; - } - } - - if ( - responseData && - typeof responseData === 'object' && - 'message' in responseData && - typeof responseData.message === 'string' - ) { - return responseData.message; - } - - return error.message || fallbackMessage; - } - - if (error instanceof Error) { - return error.message; - } - - return fallbackMessage; -}; - const RowOptionsMenu = ({ popoverPosition = 'bottom', props, @@ -235,6 +207,7 @@ const ExpensesTable = () => { setPage, setPageSize, toQueryString: getTableFilterQueryString, + reset: resetFilter, } = useTableFilter({ initial: { page: 1, @@ -244,7 +217,16 @@ const ExpensesTable = () => { transactionDate: '', realizationDate: '', locationId: '', + locationName: '', vendorId: '', + vendorName: '', + category: '', + approvalStatus: '', + realizationStatus: '', + projectFlockId: '', + projectFlockName: '', + projectFlockKandangId: '', + projectFlockKandangName: '', userId: '', }, paramMap: { @@ -254,7 +236,16 @@ const ExpensesTable = () => { transactionDate: 'transaction_date', realizationDate: 'realization_date', locationId: 'location_id', + locationName: 'location_name', vendorId: 'vendor_id', + vendorName: 'vendor_name', + category: 'category', + approvalStatus: 'approval_status', + realizationStatus: 'realization_status', + projectFlockId: 'project_flock_id', + projectFlockName: 'project_flock_name', + projectFlockKandangId: 'project_flock_kandang_id', + projectFlockKandangName: 'project_flock_kandang_name', userId: 'user_id', }, @@ -286,6 +277,8 @@ const ExpensesTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [, setApprovalNotes] = useState(''); const [bulkApprovalStatus, setBulkApprovalStatus] = @@ -575,7 +568,7 @@ const ExpensesTable = () => { toast.success('Ekspor berhasil'); } catch (error) { toast.error( - await getExportErrorMessage(error, 'Gagal mengekspor input progress') + await getErrorMessage(error, 'Gagal mengekspor input progress') ); } finally { setIsExportProgressLoading(false); @@ -740,22 +733,70 @@ const ExpensesTable = () => { const handleFilterSubmit = (values: { transaction_date?: string | null; realization_date?: string | null; - location_id?: string | null; - vendor_id?: string | null; + location?: { value: number; label: string } | null; + vendor?: { value: number; label: string } | null; + category?: OptionType | null; + approval_status?: OptionType | null; + realization_status?: OptionType | null; + project_flock?: OptionType | null; + project_flock_kandang?: OptionType | null; }) => { updateFilter('transactionDate', values.transaction_date || ''); updateFilter('realizationDate', values.realization_date || ''); - updateFilter('locationId', values.location_id || ''); - updateFilter('vendorId', values.vendor_id || ''); + updateFilter( + 'locationId', + values.location?.value ? String(values.location?.value) : '' + ); + updateFilter( + 'locationName', + values.location?.label ? String(values.location?.label) : '' + ); + updateFilter( + 'vendorId', + values.vendor?.value ? String(values.vendor?.value) : '' + ); + updateFilter( + 'vendorName', + values.vendor?.label ? String(values.vendor?.label) : '' + ); + updateFilter('category', values.category?.value || ''); + updateFilter('approvalStatus', values.approval_status?.value || ''); + updateFilter('realizationStatus', values.realization_status?.value || ''); + updateFilter( + 'projectFlockId', + values.project_flock?.value ? String(values.project_flock.value) : '' + ); + updateFilter('projectFlockName', values.project_flock?.label || ''); + updateFilter( + 'projectFlockKandangId', + values.project_flock_kandang?.value + ? String(values.project_flock_kandang.value) + : '' + ); + updateFilter( + 'projectFlockKandangName', + values.project_flock_kandang?.label || '' + ); }; const handleFilterReset = () => { - updateFilter('transactionDate', ''); - updateFilter('realizationDate', ''); - updateFilter('locationId', ''); - updateFilter('vendorId', ''); + resetFilter(); }; + const exportToExcel = useCallback(async () => { + setIsLoadingExportingToExcel(true); + + try { + await ExpenseApi.exportToExcel(getTableFilterQueryString()); + } catch (error) { + toast.error( + await getErrorMessage(error, 'Gagal mengekspor data pengeluaran') + ); + } finally { + setIsLoadingExportingToExcel(false); + } + }, [getTableFilterQueryString]); + // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -927,6 +968,10 @@ const ExpensesTable = () => { 'search', 'nameSort', 'userId', + 'locationName', + 'vendorName', + 'projectFlockName', + 'projectFlockKandangName', ]} onClick={handleFilterModalOpen} className='px-3 py-2.5' @@ -965,6 +1010,17 @@ const ExpensesTable = () => { } > + + diff --git a/src/components/pages/production/recording/filter/RecordingFilter.ts b/src/components/pages/production/recording/filter/RecordingFilter.ts index 955ae744..01caced0 100644 --- a/src/components/pages/production/recording/filter/RecordingFilter.ts +++ b/src/components/pages/production/recording/filter/RecordingFilter.ts @@ -3,13 +3,19 @@ import { string, object } from 'yup'; export const RecordingFilterSchema = object().shape({ area_id: string().nullable(), location_id: string().nullable(), + project_flock_id: string().nullable(), kandang_id: string().nullable(), project_flock_kandang_id: string().nullable(), + approval_status: string().nullable(), + project_flock_category: string().nullable(), }); export type RecordingFilterType = { area_id: string | null; location_id: string | null; + project_flock_id: string | null; kandang_id: string | null; project_flock_kandang_id: string | null; + approval_status: string | null; + project_flock_category: string | null; }; diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx index a9cd00cd..bcb80fe6 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 } from 'react'; +import { RefObject, useState, useEffect, useMemo } from 'react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; @@ -9,12 +9,20 @@ import Modal from '@/components/Modal'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInput from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; import { PurchaseFilter } from '@/types/api/purchase/purchase'; +import { AreaApi, LocationApi, SupplierApi } from '@/services/api/master-data'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategoryApi } from '@/services/api/master-data'; +import { Area } from '@/types/api/master-data/area'; +import { Location } from '@/types/api/master-data/location'; +import { Supplier } from '@/types/api/master-data/supplier'; import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line'; +import { ProjectFlockApi } from '@/services/api/production'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { isResponseSuccess } from '@/lib/api-helper'; interface PurchaseFilterModalProps { ref: RefObject; @@ -73,32 +81,112 @@ const PurchaseFilterModal = ({ 'search' ); + const [selectedAreaId, setSelectedAreaId] = useState(''); + const [selectedLocationId, setSelectedLocationId] = useState(''); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: selectedAreaId || '', + }); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedLocationId || '', + } + ); + const formik = useFormik<{ poDate: string; category: { label: string; value: number }[]; status: { label: string; value: string }[]; + supplier: OptionType | null; + area: OptionType | null; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; }>({ initialValues: { poDate: '', category: [], status: [], + supplier: null, + area: null, + location: null, + project_flock: null, + project_flock_kandang: null, }, onSubmit: async (values) => { const formattedValues = { ...values, category: values.category.map((item) => String(item.value)), status: values.status.map((item) => String(item.value)), + supplier_id: values.supplier?.value, + area_id: values.area?.value, + location_id: values.location?.value, + project_flock_id: values.project_flock?.value, + project_flock_kandang_id: values.project_flock_kandang?.value, }; onSubmit?.(formattedValues); closeModalHandler(); }, onReset: () => { + setSelectedAreaId(''); + setSelectedLocationId(''); onReset?.(); closeModalHandler(); }, }); + const projectFlockKandangOptions = useMemo(() => { + if ( + !formik.values.project_flock || + !projectFlocksRawData || + !isResponseSuccess(projectFlocksRawData) + ) { + return []; + } + + const selectedProjectFlock = projectFlocksRawData.data.find( + (item) => item.id === formik.values.project_flock?.value + ); + + return ( + selectedProjectFlock?.kandangs?.map((item) => ({ + value: item.project_flock_kandang_id, + label: item.name, + })) || [] + ); + }, [formik.values.project_flock, projectFlocksRawData]); + const productCategoryChangeHandler = ( val: OptionType | OptionType[] | null ) => { @@ -172,6 +260,108 @@ const PurchaseFilterModal = ({ value: item.step_name, }))} /> + + + formik.setFieldValue( + 'supplier', + !Array.isArray(val) + ? (val as OptionType | null) + : null + ) + } + options={supplierOptions} + isLoading={isLoadingSupplierOptions} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isClearable + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | null) + : null; + formik.setFieldValue('area', nextValue); + formik.setFieldValue('location', null); + formik.setFieldValue('project_flock', null); + formik.setFieldValue('project_flock_kandang', null); + setSelectedAreaId( + nextValue?.value ? String(nextValue.value) : '' + ); + setSelectedLocationId(''); + }} + options={areaOptions} + isLoading={isLoadingAreaOptions} + onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} + isClearable + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | null) + : null; + formik.setFieldValue('location', nextValue); + formik.setFieldValue('project_flock', null); + formik.setFieldValue('project_flock_kandang', null); + setSelectedLocationId( + nextValue?.value ? String(nextValue.value) : '' + ); + }} + options={locationOptions} + isLoading={isLoadingLocationOptions} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + isDisabled={!formik.values.area} + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | null) + : null; + formik.setFieldValue('project_flock', nextValue); + formik.setFieldValue('project_flock_kandang', null); + }} + options={projectFlockOptions} + isLoading={isLoadingProjectFlockOptions} + onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} + isClearable + isDisabled={!formik.values.location} + /> + + + formik.setFieldValue( + 'project_flock_kandang', + !Array.isArray(val) + ? (val as OptionType | null) + : null + ) + } + options={projectFlockKandangOptions} + isClearable + isDisabled={!formik.values.project_flock} + /> diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 67555522..820602a6 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -33,7 +33,7 @@ import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal import Dropdown from '@/components/dropdown/Dropdown'; import { cn, formatDate } from '@/lib/helper'; -import { 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'; @@ -43,43 +43,6 @@ import { ExpenseApi } from '@/services/api/expense'; import { Expense } from '@/types/api/expense'; import { Color } from '@/types/theme'; -const getExportErrorMessage = async ( - error: unknown, - fallbackMessage: string -) => { - if (axios.isAxiosError(error)) { - const responseData = error.response?.data; - - if (responseData instanceof Blob) { - try { - const parsed = JSON.parse(await responseData.text()) as { - message?: string; - }; - return parsed.message || fallbackMessage; - } catch { - return fallbackMessage; - } - } - - if ( - responseData && - typeof responseData === 'object' && - 'message' in responseData && - typeof responseData.message === 'string' - ) { - return responseData.message; - } - - return error.message || fallbackMessage; - } - - if (error instanceof Error) { - return error.message; - } - - return fallbackMessage; -}; - // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', @@ -192,6 +155,8 @@ const PurchaseTable = () => { // ===== STATE MANAGEMENT ===== const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [selectedPurchase, setSelectedPurchase] = useState( null @@ -213,6 +178,11 @@ const PurchaseTable = () => { po_date: '', approval_status: '', product_category_id: '', + supplier_id: '', + area_id: '', + location_id: '', + project_flock_id: '', + project_flock_kandang_id: '', }, paramMap: { page: 'page', @@ -220,6 +190,11 @@ const PurchaseTable = () => { po_date: 'po_date', approval_status: 'approval_status', product_category_id: 'product_category_id', + supplier_id: 'supplier_id', + area_id: 'area_id', + location_id: 'location_id', + project_flock_id: 'project_flock_id', + project_flock_kandang_id: 'project_flock_kandang_id', }, }); @@ -467,14 +442,52 @@ const PurchaseTable = () => { updateFilter('po_date', values.poDate); updateFilter('product_category_id', values.category.join(',')); updateFilter('approval_status', values.status.join(',')); + updateFilter( + 'supplier_id', + values.supplier_id ? String(values.supplier_id) : '' + ); + updateFilter('area_id', values.area_id ? String(values.area_id) : ''); + updateFilter( + 'location_id', + values.location_id ? String(values.location_id) : '' + ); + updateFilter( + 'project_flock_id', + values.project_flock_id ? String(values.project_flock_id) : '' + ); + updateFilter( + 'project_flock_kandang_id', + values.project_flock_kandang_id + ? String(values.project_flock_kandang_id) + : '' + ); }; const filterResetHandler = () => { updateFilter('po_date', ''); updateFilter('product_category_id', ''); updateFilter('approval_status', ''); + updateFilter('supplier_id', ''); + updateFilter('area_id', ''); + updateFilter('location_id', ''); + updateFilter('project_flock_id', ''); + updateFilter('project_flock_kandang_id', ''); }; + const exportToExcel = useCallback(async () => { + setIsLoadingExportingToExcel(true); + + try { + await PurchaseApi.exportToExcel(getTableFilterQueryString()); + } catch (error) { + toast.error( + await getErrorMessage(error, 'Gagal mengekspor data pembelian') + ); + } finally { + setIsLoadingExportingToExcel(false); + } + }, [getTableFilterQueryString]); + const resetExportProgressForm = useCallback(() => { setExportProgressStartDate(''); setExportProgressEndDate(''); @@ -513,7 +526,7 @@ const PurchaseTable = () => { toast.success('Ekspor berhasil'); } catch (error) { toast.error( - await getExportErrorMessage(error, 'Gagal mengekspor input progress') + await getErrorMessage(error, 'Gagal mengekspor input progress') ); } finally { setIsExportProgressLoading(false); @@ -610,6 +623,17 @@ const PurchaseTable = () => { } > + +