diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 211af1f7..a4cb9cbd 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -29,7 +29,7 @@ import { FINANCE_TRANSACTION_TYPE_OPTIONS, } from '@/config/constant'; import { FinanceApi } from '@/services/api/finance'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; import Modal, { useModal } from '@/components/Modal'; @@ -39,6 +39,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import RequirePermission from '@/components/helper/RequirePermission'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import Dropdown from '@/components/dropdown/Dropdown'; import { FinanceTableFilterSchema, FinanceTableFilterValues, @@ -233,6 +234,7 @@ const FinanceTable = () => { const [selectedSortBy, setSelectedSortBy] = useState(null); const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isExportLoading, setIsExportLoading] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); @@ -552,6 +554,20 @@ const FinanceTable = () => { filterModal.openModal(); }; + const exportToExcel = async () => { + setIsExportLoading(true); + try { + await FinanceApi.exportToExcel(getTableFilterQueryString()); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch (error) { + toast.error( + await getErrorMessage(error, 'Gagal mengekspor data finance.') + ); + } finally { + setIsExportLoading(false); + } + }; + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -759,6 +775,51 @@ const FinanceTable = () => { onClick={handleFilterModalOpen} className='px-3 py-2.5' /> + + +
+ + + Ekspor + +
+ + +
+ + } + > + +
diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx index 856f2074..55db1aea 100644 --- a/src/components/pages/purchase/PurchaseFilterModal.tsx +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -10,6 +10,7 @@ import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInput from '@/components/input/SelectInput'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import { OptionType, useSelect } from '@/components/input/SelectInput'; import { PurchaseFilter } from '@/types/api/purchase/purchase'; @@ -24,10 +25,20 @@ import { ProjectFlockApi } from '@/services/api/production'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { isResponseSuccess } from '@/lib/api-helper'; +const filterByOptions: OptionType[] = [ + { value: 'po_date', label: 'Tanggal PO' }, + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'due_date', label: 'Tanggal Jatuh Tempo' }, + { value: 'created_at', label: 'Tanggal Dibuat' }, +]; + interface PurchaseFilterModalProps { ref: RefObject; initialValues?: { poDate: string; + start_date: string; + end_date: string; + filterBy: OptionType | undefined; category: OptionType[]; status: OptionType[]; supplier: OptionType | null; @@ -51,6 +62,7 @@ const PurchaseFilterModal = ({ }, [ref]); // ===== DATE ERROR STATE ===== + const [hasDateError, setHasDateError] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); // ===== CLEANUP TOAST ON UNMOUNT ===== @@ -139,6 +151,9 @@ const PurchaseFilterModal = ({ const formik = useFormik<{ poDate: string; + start_date: string; + end_date: string; + filterBy: OptionType | undefined; category: { label: string; value: number }[]; status: { label: string; value: string }[]; supplier: OptionType | null; @@ -150,6 +165,9 @@ const PurchaseFilterModal = ({ // enableReinitialize: true, initialValues: initialValues || { poDate: '', + start_date: '', + end_date: '', + filterBy: undefined, category: [], status: [], supplier: null, @@ -230,9 +248,17 @@ const PurchaseFilterModal = ({ }; const formikResetHandler = useCallback(() => { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } resetForm({ values: { poDate: '', + start_date: '', + end_date: '', + filterBy: undefined, category: [], status: [], supplier: null, @@ -246,7 +272,56 @@ const PurchaseFilterModal = ({ setSelectedLocationId(''); onReset?.(); closeModalHandler(); - }, [resetForm, onReset, closeModalHandler]); + }, [resetForm, onReset, closeModalHandler, dateErrorShown]); + + const handleStartDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value); + + if (value && formik.values.end_date) { + if (new Date(formik.values.end_date) < new Date(value)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh sebelum tanggal mulai', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }; + + const handleEndDateChange = (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value); + + if (value && formik.values.start_date) { + if (new Date(value) < new Date(formik.values.start_date)) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh sebelum tanggal mulai', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; const formikSubmitHandler = useCallback(async () => { await submitForm(); @@ -287,6 +362,44 @@ const PurchaseFilterModal = ({ {/* Modal Body */}
+
+ +
+ +
+ +
+
+ + + formik.setFieldValue( + 'filterBy', + !Array.isArray(val) ? (val ?? undefined) : undefined + ) + } + isClearable + /> + Apply Filter diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index fdd2f9be..1e2da838 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -28,7 +28,7 @@ import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal import Dropdown from '@/components/dropdown/Dropdown'; import { OptionType } from '@/components/input/SelectInput'; -import { cn, formatDate } from '@/lib/helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -41,6 +41,9 @@ type PurchaseTableFilters = { search: string; sort_by: string; order_by: string; + start_date: string; + end_date: string; + filter_by: string; po_date: string; approval_status: string; product_category_id: string; @@ -177,6 +180,9 @@ const PurchaseTable = () => { search: '', sort_by: '', order_by: '', + start_date: '', + end_date: '', + filter_by: '', po_date: '', approval_status: '', product_category_id: '', @@ -197,6 +203,9 @@ const PurchaseTable = () => { pageSize: 'limit', sort_by: 'sort_by', order_by: 'sort_order', + start_date: 'start_date', + end_date: 'end_date', + filter_by: 'filter_by', po_date: 'po_date', approval_status: 'approval_status', product_category_id: 'product_category_id', @@ -297,36 +306,11 @@ const PurchaseTable = () => { ); }, }, - { - accessorKey: 'supplier', - header: 'Vendor', - cell: (props) => props.row.original.supplier.name, - }, { accessorKey: 'requester_name', header: 'Nama Pengaju', cell: (props) => props.row.original.requester_name || '-', }, - { - accessorKey: 'products', - header: 'Produk', - cell: (props) => { - const products = props.row.original.products; - if (!products || products.length === 0) return '-'; - return ( -
    - {products.map((product, index) => ( -
  • {product.name}
  • - ))} -
- ); - }, - }, - { - accessorKey: 'location', - header: 'Lokasi', - cell: (props) => props.row.original.location?.name || '-', - }, { accessorKey: 'po_date', header: 'Tgl. PO', @@ -364,6 +348,202 @@ const PurchaseTable = () => { return `${diffDays} hari`; }, }, + { + accessorKey: 'supplier', + header: 'Vendor', + cell: (props) => props.row.original.supplier.name, + }, + { + accessorKey: 'location', + header: 'Lokasi', + cell: (props) => props.row.original.location?.name || '-', + }, + { + accessorKey: 'warehouse', + header: 'Gudang', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {item.warehouse?.name ?? '-'}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'products', + header: 'Produk', + cell: (props) => { + const products = props.row.original.products; + if (!products || products.length === 0) return '-'; + return ( +
    + {products.map((product, index) => ( +
  • {product.name}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'total_qty', + header: 'Kuantitas', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {formatNumber(item.total_qty ?? 0)}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'uom', + header: 'Satuan', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {item.product?.uom?.name ?? '-'}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'price', + header: 'Harga', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {formatCurrency(item.price ?? 0)}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'total_price', + header: 'Total Harga', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {formatCurrency(item.total_price ?? 0)}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'products_total', + header: 'Total Harga Produk', + cell: (props) => formatCurrency(props.row.original.products_total ?? 0), + }, + { + accessorKey: 'expedition_vendor', + header: 'Vendor Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • {item.expedition_vendor?.name ?? '-'}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'expedition_qty', + header: 'Qty Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • + {item.expedition_qty != null + ? formatNumber(item.expedition_qty) + : '-'} +
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'transport_per_item', + header: 'Harga Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • + {item.transport_per_item != null + ? formatCurrency(item.transport_per_item) + : '-'} +
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'item_expedition_total', + header: 'Total Ekspedisi', + enableSorting: false, + cell: (props) => { + const items = props.row.original.items; + if (!items || items.length === 0) return '-'; + return ( +
    + {items.map((item, index) => ( +
  • + {item.expedition_total != null + ? formatCurrency(item.expedition_total) + : '-'} +
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'expedition_total', + header: 'Total Ekspedisi Semua Produk', + cell: (props) => formatCurrency(props.row.original.expedition_total ?? 0), + }, + { + accessorKey: 'grand_total_all', + header: 'Grand Total All', + cell: (props) => formatCurrency(props.row.original.grand_total_all ?? 0), + }, { accessorKey: 'status', header: 'Status Approval', @@ -410,6 +590,11 @@ const PurchaseTable = () => { ); }, }, + { + accessorKey: 'notes', + header: 'Notes', + cell: (props) => props.row.original.notes || '-', + }, { accessorKey: 'created_at', header: 'Tanggal Dibuat', @@ -476,6 +661,9 @@ const PurchaseTable = () => { const filterSubmitHandler = (values: PurchaseFilter) => { setFilters({ + start_date: values.start_date || '', + end_date: values.end_date || '', + filter_by: values.filterBy?.value || '', po_date: values.poDate, product_category_id: values.category.join(','), product_category_name: @@ -500,6 +688,9 @@ const PurchaseTable = () => { const filterResetHandler = () => { setFilters({ + start_date: '', + end_date: '', + filter_by: '', po_date: '', product_category_id: '', product_category_name: '', @@ -518,6 +709,13 @@ const PurchaseTable = () => { }; const purchaseFilterInitialValues = useMemo(() => { + const filterByLabelMap: Record = { + po_date: 'Tanggal PO', + received_date: 'Tanggal Terima', + due_date: 'Tanggal Jatuh Tempo', + created_at: 'Tanggal Dibuat', + }; + const categoryIds = tableFilterState.product_category_id ? tableFilterState.product_category_id .split(',') @@ -539,6 +737,16 @@ const PurchaseTable = () => { return { poDate: tableFilterState.po_date, + start_date: tableFilterState.start_date, + end_date: tableFilterState.end_date, + filterBy: tableFilterState.filter_by + ? { + value: tableFilterState.filter_by, + label: + filterByLabelMap[tableFilterState.filter_by] || + tableFilterState.filter_by, + } + : undefined, category: categoryIds.map((value, index) => ({ value: Number(value), label: categoryLabels[index] || value, @@ -706,7 +914,7 @@ const PurchaseTable = () => { 'project_flock_name', 'project_flock_kandang_name', ]} - fieldGroups={[['startDate', 'endDate']]} + fieldGroups={[['start_date', 'end_date']]} onClick={filterModal.openModal} className='px-3 py-2.5' /> diff --git a/src/services/api/finance.ts b/src/services/api/finance.ts index f9ba367f..291551f3 100644 --- a/src/services/api/finance.ts +++ b/src/services/api/finance.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { formatDate } from '@/lib/helper'; import { CreateFinancePayment, CreateInitialBalance, @@ -174,6 +175,30 @@ export class FinanceApiService extends BaseApiService< } } + async exportToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('export', 'excel'); + params.set('page', '1'); + params.set('limit', '99999999999'); + + const res = await httpClient( + `${this.basePath}/transactions?${params.toString()}`, + { method: 'GET', responseType: 'blob' } + ); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + `finance-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx` + ); + document.body.appendChild(link); + link.click(); + link.remove(); + } + async delete(id: number) { try { const deletePath = `${this.basePath}/transactions/${id}`; diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 98e76aab..d790e895 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -57,6 +57,8 @@ export type PurchaseItem = { alias?: string; category?: string; } | null; + expedition_qty?: number; + expedition_total?: number; }; export type BasePurchase = { @@ -81,6 +83,9 @@ export type BasePurchase = { po_expedition?: { id: number; refrence: string }[]; created_user?: CreatedUser; products?: PurchaseItemProduct[]; + products_total?: number; + expedition_total?: number; + grand_total_all?: number; }; export type Purchase = BaseMetadata & BasePurchase; @@ -149,6 +154,9 @@ export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; export type PurchaseFilter = { poDate: string; + start_date?: string; + end_date?: string; + filterBy?: { label: string; value: string }; category: string[]; category_labels?: { label: string; value: number }[]; status: string[];