From 4e5745d23765d96963e5418b6cbf30412815ad2c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 15:53:32 +0700 Subject: [PATCH 01/82] refactor(FE): Add tab state management and skeleton for PurchasesPerSupplierTab --- .../logistic-stock/LogisticStockTabs.tsx | 23 +- .../skeleton/PurchasePerSupplierSkeleton.tsx | 37 + .../tab/PurchasesPerSupplierTab.tsx | 994 +++++++++++------- .../logistic-stock-tab.store.ts | 51 + 4 files changed, 712 insertions(+), 393 deletions(-) create mode 100644 src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx create mode 100644 src/stores/logistic-stock-tab/logistic-stock-tab.store.ts diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 1e2d2824..a7b844f3 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -1,14 +1,19 @@ 'use client'; +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; +import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; const LogisticStockTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useLogisticStockTabStore((state) => state.tabActions); + const tabs = [ { id: '1', label: 'Rekapitulasi Pembelian Per Supplier', - content: , + content: , }, // { // id: '2', @@ -23,8 +28,20 @@ const LogisticStockTabs = () => { ]; return ( -
- +
+
); }; diff --git a/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx b/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx new file mode 100644 index 00000000..a5268b2f --- /dev/null +++ b/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { ColumnDef } from '@tanstack/react-table'; + +const PurchasePerSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+ +
+ +
+ + ); +}; + +export default PurchasePerSupplierSkeleton; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index e1659470..4176e8ba 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1,11 +1,9 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import Card from '@/components/Card'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; +import { useSelect, OptionType } from '@/components/input/SelectInput'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; @@ -14,24 +12,30 @@ import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/report/logistic-stock'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierSummary, } from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import Pagination from '@/components/Pagination'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; +import Modal from '@/components/Modal'; +import { useModal } from '@/components/Modal'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; +import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; +import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; -const PurchasesPerSupplierTab = () => { +interface PurchasesPerSupplierTabProps { + tabId: string; +} + +const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -39,31 +43,14 @@ const PurchasesPerSupplierTab = () => { // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const [pageSize] = useState(10); // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); - // ===== TABLE FILTER STATE ===== - const { state: tableFilterState, updateFilter } = useTableFilter({ - initial: { - area_id: [] as string[], - supplier_id: [] as string[], - product_id: [] as string[], - product_category_id: [] as string[], - received_date: '', - po_date: '', - start_date: '', - end_date: '', - sort_by: '', - filter_by: 'received_date', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); + const filterModal = useModal(); + // ===== OPTIONS (Declare before filter state) ===== const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', @@ -100,117 +87,232 @@ const PurchasesPerSupplierTab = () => { [] ); - const areaChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'area_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== APPLIED FILTER STATE (Yang sudah di-apply) ===== + const [appliedFilterArea, setAppliedFilterArea] = useState< + typeof areaOptions + >([]); + const [appliedFilterSupplier, setAppliedFilterSupplier] = useState< + typeof supplierOptions + >([]); + const [appliedFilterProduct, setAppliedFilterProduct] = useState< + typeof productOptions + >([]); + const [appliedFilterProductCategory, setAppliedFilterProductCategory] = + useState([]); + const [appliedFilterByType, setAppliedFilterByType] = useState< + (typeof dataTypeOptions)[0] | null + >(null); + const [appliedFilterSortBy, setAppliedFilterSortBy] = useState< + (typeof sortByOptions)[0] | null + >(null); + const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); + const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); - const supplierChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'supplier_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] + // ===== PENDING FILTER STATE (Yang ada di modal, belum di-apply) ===== + const [filterArea, setFilterArea] = useState([]); + const [filterSupplier, setFilterSupplier] = useState( + [] ); + const [filterProduct, setFilterProduct] = useState([]); + const [filterProductCategory, setFilterProductCategory] = useState< + typeof productCategoryOptions + >([]); + const [filterByType, setFilterByType] = useState< + (typeof dataTypeOptions)[0] | null + >(null); + const [filterSortBy, setFilterSortBy] = useState< + (typeof sortByOptions)[0] | null + >(null); + const [filterStartDate, setFilterStartDate] = useState(''); + const [filterEndDate, setFilterEndDate] = useState(''); - const productChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'product_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== FILTER HANDLERS ===== + const handleFilterModalOpen = useCallback(() => { + setFilterArea(appliedFilterArea); + setFilterSupplier(appliedFilterSupplier); + setFilterProduct(appliedFilterProduct); + setFilterProductCategory(appliedFilterProductCategory); + setFilterByType(appliedFilterByType); + setFilterSortBy(appliedFilterSortBy); + setFilterStartDate(appliedFilterStartDate); + setFilterEndDate(appliedFilterEndDate); + filterModal.openModal(); + }, [ + filterModal, + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterByType, + appliedFilterSortBy, + appliedFilterStartDate, + appliedFilterEndDate, + ]); - const productCategoryChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'product_category_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const dataTypeChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - const filterValue = - (newVal?.value as 'received_date' | 'po_date') || 'received_date'; - updateFilter('filter_by', filterValue); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const sortByHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC'; - updateFilter('sort_by', sortValue); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const startDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('start_date', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const endDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('end_date', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const resetFilters = useCallback(() => { - updateFilter('area_id', []); - updateFilter('supplier_id', []); - updateFilter('product_id', []); - updateFilter('product_category_id', []); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - updateFilter('start_date', ''); - updateFilter('end_date', ''); - updateFilter('sort_by', ''); - updateFilter('filter_by', 'received_date'); + const handleResetFilters = useCallback(() => { setIsSubmitted(false); - }, [updateFilter]); + setFilterArea([]); + setFilterSupplier([]); + setFilterProduct([]); + setFilterProductCategory([]); + setFilterByType(null); + setFilterSortBy(null); + setFilterStartDate(''); + setFilterEndDate(''); + setAppliedFilterArea([]); + setAppliedFilterSupplier([]); + setAppliedFilterProduct([]); + setAppliedFilterProductCategory([]); + setAppliedFilterByType(null); + setAppliedFilterSortBy(null); + setAppliedFilterStartDate(''); + setAppliedFilterEndDate(''); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, [dateErrorShown]); - const handleSubmit = useCallback(() => { + const handleApplyFilters = useCallback(() => { + setAppliedFilterArea(filterArea); + setAppliedFilterSupplier(filterSupplier); + setAppliedFilterProduct(filterProduct); + setAppliedFilterProductCategory(filterProductCategory); + setAppliedFilterByType(filterByType); + setAppliedFilterSortBy(filterSortBy); + setAppliedFilterStartDate(filterStartDate); + setAppliedFilterEndDate(filterEndDate); setIsSubmitted(true); setCurrentPage(1); - }, []); + filterModal.closeModal(); + }, [ + filterModal, + filterArea, + filterSupplier, + filterProduct, + filterProductCategory, + filterByType, + filterSortBy, + filterStartDate, + filterEndDate, + ]); + + const handleStartDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterStartDate(value); + + if (value && filterEndDate) { + const startDate = new Date(value); + const endDateObj = new Date(filterEndDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }, + [filterEndDate, dateErrorShown] + ); + + const handleEndDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterEndDate(value); + + if (value && filterStartDate) { + const startDateObj = new Date(filterStartDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + [filterStartDate, dateErrorShown] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + // Date filter (start_date + end_date = 1 filter) + if (appliedFilterStartDate || appliedFilterEndDate) { + count += 1; + } + + // Area filter + if (appliedFilterArea.length > 0) { + count += 1; + } + + // Supplier filter + if (appliedFilterSupplier.length > 0) { + count += 1; + } + + // Product filter + if (appliedFilterProduct.length > 0) { + count += 1; + } + + // Product category filter + if (appliedFilterProductCategory.length > 0) { + count += 1; + } + + // Filter by type filter + if (appliedFilterByType) { + count += 1; + } + + // Sort by filter + if (appliedFilterSortBy) { + count += 1; + } + + return count; + }, [ + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterByType, + appliedFilterSortBy, + ]); + + const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( @@ -218,33 +320,38 @@ const PurchasesPerSupplierTab = () => { ? () => { const params = { area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') + appliedFilterArea.length > 0 + ? appliedFilterArea.map((v) => String(v.value)).join(',') : undefined, supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') + appliedFilterSupplier.length > 0 + ? appliedFilterSupplier.map((v) => String(v.value)).join(',') : undefined, product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') + appliedFilterProduct.length > 0 + ? appliedFilterProduct.map((v) => String(v.value)).join(',') : undefined, product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') + appliedFilterProductCategory.length > 0 + ? appliedFilterProductCategory + .map((v) => String(v.value)) + .join(',') : undefined, received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'received_date' + ? appliedFilterStartDate || undefined : undefined, po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'po_date' + ? appliedFilterStartDate || undefined : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, + sort_by: + (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, + filter_by: + (appliedFilterByType?.value as 'received_date' | 'po_date') || + undefined, page: currentPage, limit: pageSize, }; @@ -289,33 +396,35 @@ const PurchasesPerSupplierTab = () => { > => { const params = { area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') + appliedFilterArea.length > 0 + ? appliedFilterArea.map((v) => String(v.value)).join(',') : undefined, supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') + appliedFilterSupplier.length > 0 + ? appliedFilterSupplier.map((v) => String(v.value)).join(',') : undefined, product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') + appliedFilterProduct.length > 0 + ? appliedFilterProduct.map((v) => String(v.value)).join(',') : undefined, product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') + appliedFilterProductCategory.length > 0 + ? appliedFilterProductCategory.map((v) => String(v.value)).join(',') : undefined, received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'received_date' + ? appliedFilterStartDate || undefined : undefined, po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'po_date' + ? appliedFilterStartDate || undefined : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, + sort_by: (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, + filter_by: + (appliedFilterByType?.value as 'received_date' | 'po_date') || + undefined, limit: 100, page: 1, }; @@ -338,7 +447,16 @@ const PurchasesPerSupplierTab = () => { return isResponseSuccess(response) ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) : null; - }, [tableFilterState]); + }, [ + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterByType, + appliedFilterSortBy, + ]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -379,48 +497,26 @@ const PurchasesPerSupplierTab = () => { } const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' + appliedFilterArea.length > 0 + ? appliedFilterArea.map((c) => c.label).join(', ') || 'Semua Area' : 'Semua Area'; const supplierName = - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id - .map( - (id) => - supplierOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Supplier' + appliedFilterSupplier.length > 0 + ? appliedFilterSupplier.map((c) => c.label).join(', ') || + 'Semua Supplier' : 'Semua Supplier'; const productName = - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id - .map( - (id) => - productOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Produk' + appliedFilterProduct.length > 0 + ? appliedFilterProduct.map((c) => c.label).join(', ') || + 'Semua Produk' : 'Semua Produk'; const productCategoryName = - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id - .map( - (id) => - productCategoryOptions.find((opt) => opt.value === Number(id)) - ?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Kategori Produk' + appliedFilterProductCategory.length > 0 + ? appliedFilterProductCategory.map((c) => c.label).join(', ') || + 'Semua Kategori Produk' : 'Semua Kategori Produk'; const exportParams = { @@ -428,9 +524,11 @@ const PurchasesPerSupplierTab = () => { supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, - filter_by: tableFilterState.filter_by || 'received_date', - start_date: tableFilterState.start_date || '', - end_date: tableFilterState.end_date || '', + filter_by: + (appliedFilterByType?.value as 'received_date' | 'po_date') || + undefined, + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, }; await generatePurchasesPerSupplierPDF({ @@ -445,33 +543,101 @@ const PurchasesPerSupplierTab = () => { } }, [ logisticPurchasePerSupplierExport, - tableFilterState, - areaOptions, - supplierOptions, - productOptions, - productCategoryOptions, + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterByType, ]); - // ===== PAGINATION HANDLERS ===== - const handlePageChange = (page: number) => { - setCurrentPage(page); - }; + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useLogisticStockTabStore( + (state) => state.setTabActions + ); + const clearTabActions = useLogisticStockTabStore( + (state) => state.clearTabActions + ); - const handleRowChange = (pageSize: number) => { - setPageSize(pageSize); - }; + useEffect(() => { + setTabActions( + tabId, +
+ - const handleNextPage = () => { - if (meta && currentPage < meta.total_pages) { - setCurrentPage(currentPage + 1); - } - }; + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); - const handlePrevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); const getTableColumns = ( summary: LogisticPurchasePerSupplierSummary @@ -485,11 +651,11 @@ const PurchasesPerSupplierTab = () => { cell: (props) => props.row.index + 1, footer: () =>
Total
, }, - { id: 'received_date', header: 'Tanggal Terima', accessorKey: 'receive_date', + enableSorting: false, cell: (props) => { const value = props.row.original.receive_date; return formatDate(value, 'DD MMM YYYY'); @@ -499,6 +665,7 @@ const PurchasesPerSupplierTab = () => { id: 'po_date', header: 'Tanggal PO', accessorKey: 'po_date', + enableSorting: false, cell: (props) => { const value = props.row.original.po_date; return formatDate(value, 'DD MMM YYYY'); @@ -508,6 +675,7 @@ const PurchasesPerSupplierTab = () => { id: 'po_number', header: 'No. Referensi', accessorKey: 'po_number', + enableSorting: false, cell: (props) => { const value = props.row.original.po_number; return value || '-'; @@ -517,6 +685,7 @@ const PurchasesPerSupplierTab = () => { id: 'product_name', header: 'Nama Produk', accessorKey: 'product.name', + enableSorting: false, cell: (props) => { const product = props.row.original.product; return product?.name || '-'; @@ -526,6 +695,7 @@ const PurchasesPerSupplierTab = () => { id: 'destination_warehouse', header: 'Tujuan', accessorKey: 'warehouse.name', + enableSorting: false, cell: (props) => { const warehouse = props.row.original.warehouse; return warehouse?.name || '-'; @@ -535,6 +705,7 @@ const PurchasesPerSupplierTab = () => { id: 'qty', header: 'QTY', accessorKey: 'qty', + enableSorting: false, cell: (props) => { const value = props.row.original.qty; return
{formatNumber(value)}
; @@ -549,6 +720,7 @@ const PurchasesPerSupplierTab = () => { id: 'price', header: 'Harga Beli (Rp)', accessorKey: 'unit_price', + enableSorting: false, cell: (props) => { const value = props.row.original.unit_price; return
{formatCurrency(value)}
; @@ -563,6 +735,7 @@ const PurchasesPerSupplierTab = () => { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', accessorKey: 'purchase_value', + enableSorting: false, cell: (props) => { const value = props.row.original.purchase_value; return
{formatCurrency(value)}
; @@ -577,6 +750,7 @@ const PurchasesPerSupplierTab = () => { id: 'transport', header: 'Transport (Rp)', accessorKey: 'transport_unit_price', + enableSorting: false, cell: (props) => { const value = props.row.original.transport_unit_price; return
{formatCurrency(value)}
; @@ -591,6 +765,7 @@ const PurchasesPerSupplierTab = () => { id: 'value_transport', header: 'Value Transport (Rp)', accessorKey: 'transport_value', + enableSorting: false, cell: (props) => { const value = props.row.original.transport_value; return
{formatCurrency(value)}
; @@ -605,6 +780,7 @@ const PurchasesPerSupplierTab = () => { id: 'total', header: 'Jumlah (Rp)', accessorKey: 'total_amount', + enableSorting: false, cell: (props) => { const value = props.row.original.total_amount; return
{formatCurrency(value)}
; @@ -619,6 +795,7 @@ const PurchasesPerSupplierTab = () => { id: 'expedition_vendor_name', header: 'Ekspedisi', accessorKey: 'expedition', + enableSorting: false, cell: (props) => { const value = props.row.original.expedition; return value || '-'; @@ -628,6 +805,7 @@ const PurchasesPerSupplierTab = () => { id: 'travel_number', header: 'Surat Jalan', accessorKey: 'delivery_number', + enableSorting: false, cell: (props) => { const value = props.row.original.delivery_number; return value || '-'; @@ -638,156 +816,50 @@ const PurchasesPerSupplierTab = () => { }; return ( -
- -
- - (tableFilterState.area_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={areaChangeHandler} - isLoading={isLoadingAreas} - isClearable - /> - - (tableFilterState.supplier_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={supplierChangeHandler} - isLoading={isLoadingSuppliers} - isClearable - /> - - (tableFilterState.product_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={productChangeHandler} - isLoading={isLoadingProducts} - isClearable - /> -
-
- - (tableFilterState.product_category_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={productCategoryChangeHandler} - isLoading={isLoadingProductCategories} - isClearable - /> -
- option.value === tableFilterState.filter_by - ) || null - } - onChange={dataTypeChangeHandler} - isLoading={false} - isClearable={false} - /> - option.value === tableFilterState.sort_by - ) || null - } - onChange={sortByHandler} - isLoading={false} - isClearable={false} - /> -
-
- - -
-
-
- - - - Export - - - } - align='end' - > - - - - - -
- + <> +
{!isSubmitted ? ( -
- Silakan pilih filter dan klik tombol Submit untuk menampilkan data. -
+ + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? ( -
- -
+ + } + title='Memuat Data Pembelian Per Supplier' + subtitle='Silakan tunggu sebentar...' + /> ) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
+ + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((supplierReport) => { const summary = supplierReport.summary || { @@ -808,15 +880,17 @@ const PurchasesPerSupplierTab = () => { title={supplierReport.supplier.name} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} className={{ - wrapper: 'w-full rounded-2xl', + wrapper: 'w-full rounded-lg border-none', body: 'p-0', title: - 'py-1.5 px-3 bg-primary text-white text-lg font-normal', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', subtitle: - 'px-3 pb-1 bg-primary text-white text-sm font-normal', + 'px-2 pb-1.5 bg-primary text-white text-xs font-normal', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} + defaultCollapsed={true} >
{ renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full mb-0!', - tableWrapperClassName: 'overflow-x-auto', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -846,22 +921,161 @@ const PurchasesPerSupplierTab = () => { ); }) )} - - {meta && data.length > 0 && ( -
- + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+ {/* Date Filter */} +
+ +
+ +
+ +
+
+ + {/* Area Filter */} + { + setFilterArea(Array.isArray(val) ? val : val ? [val] : []); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Supplier Filter */} + { + setFilterSupplier(Array.isArray(val) ? val : val ? [val] : []); + }} + isLoading={isLoadingSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Filter */} + { + setFilterProduct(Array.isArray(val) ? val : val ? [val] : []); + }} + isLoading={isLoadingProducts} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Category Filter */} + { + setFilterProductCategory( + Array.isArray(val) ? val : val ? [val] : [] + ); + }} + isLoading={isLoadingProductCategories} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Filter By Type */} + { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> + + {/* Sort By */} + { + if (val && !Array.isArray(val)) { + setFilterSortBy(val); + } + }} + className={{ wrapper: 'w-full' }} />
- )} -
+ + {/* Modal Footer */} +
+ + +
+ + ); }; diff --git a/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts b/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts new file mode 100644 index 00000000..f9e142b1 --- /dev/null +++ b/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts @@ -0,0 +1,51 @@ +'use client'; + +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export type LogisticStockTabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useLogisticStockTabStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set( + (state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + }), + false, + 'setTabActions' + ), + + clearTabActions: (tabId) => + set( + (state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }, + false, + 'clearTabActions' + ), + + clearAllTabActions: () => + set({ tabActions: {} }, false, 'clearAllTabActions'), + }), + { + name: 'LogisticStockTabStore', + } + ) +); From ed781da372b1a89c4f07240eff5d3cdd02cf57a2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 16:12:14 +0700 Subject: [PATCH 02/82] refactor(FE): Change Tabs variant from 'lifted' to 'boxed' --- .../pages/report/logistic-stock/LogisticStockTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index a7b844f3..f06e63dc 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -31,7 +31,7 @@ const LogisticStockTabs = () => {
Date: Wed, 11 Feb 2026 16:17:15 +0700 Subject: [PATCH 03/82] refactor(FE): Make dropdown filters clearable in PurchasesPerSupplierTab --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 4176e8ba..023df222 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1035,11 +1035,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { options={dataTypeOptions} value={filterByType} onChange={(val) => { - if (val && !Array.isArray(val)) { + if (!Array.isArray(val)) { setFilterByType(val); } }} className={{ wrapper: 'w-full' }} + isClearable={true} /> {/* Sort By */} @@ -1049,11 +1050,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { options={sortByOptions} value={filterSortBy} onChange={(val) => { - if (val && !Array.isArray(val)) { + if (!Array.isArray(val)) { setFilterSortBy(val); } }} className={{ wrapper: 'w-full' }} + isClearable={true} /> From 52d58d0921ff351c3a6d69f3fa444f053c9ee21c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 16:44:10 +0700 Subject: [PATCH 04/82] refactor(FE): Refactor PurchasesPerSupplierTab to use Formik for filters --- .../filter/PurchasesPerSupplierFilter.ts | 89 +++ .../tab/PurchasesPerSupplierTab.tsx | 755 ++++++++---------- 2 files changed, 439 insertions(+), 405 deletions(-) create mode 100644 src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts diff --git a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts new file mode 100644 index 00000000..70a01615 --- /dev/null +++ b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts @@ -0,0 +1,89 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type PurchasesPerSupplierFilterType = { + start_date: string | null | undefined; + end_date: string | null | undefined; + area_ids: OptionType[] | null | undefined; + supplier_ids: OptionType[] | null | undefined; + product_ids: OptionType[] | null | undefined; + product_category_ids: OptionType[] | null | undefined; + filter_by: OptionType | null | undefined; + sort_by: OptionType | null | undefined; +}; + +export const PurchasesPerSupplierFilterSchema: yup.ObjectSchema = + yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + area_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + supplier_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + product_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + product_category_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + filter_by: yup + .object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + .optional() + .nullable(), + sort_by: yup + .object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + .optional() + .nullable(), + }); + +export type PurchasesPerSupplierFilterValues = yup.InferType< + typeof PurchasesPerSupplierFilterSchema +>; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 023df222..4a070b56 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1,40 +1,55 @@ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import useSWR from 'swr'; +import Button from '@/components/Button'; import Card from '@/components/Card'; -import { useSelect, OptionType } from '@/components/input/SelectInput'; -import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; -import SelectInputRadio from '@/components/input/SelectInputRadio'; +import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import Modal, { useModal } from '@/components/Modal'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/report/logistic-stock'; -import Table from '@/components/Table'; -import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierSummary, } from '@/types/api/report/logistic-stock'; -import { isResponseSuccess } from '@/lib/api-helper'; -import Button from '@/components/Button'; -import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; -import Modal from '@/components/Modal'; -import { useModal } from '@/components/Modal'; -import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; -import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; -import toast from 'react-hot-toast'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; +import useSWR from 'swr'; +import { useFormik } from 'formik'; +import { + PurchasesPerSupplierFilterSchema, + PurchasesPerSupplierFilterType, +} from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; +import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { tabId: string; } +interface FilterParams { + area_ids?: string; + supplier_ids?: string; + product_ids?: string; + product_category_ids?: string; + start_date?: string; + end_date?: string; + sort_by?: string; + filter_by?: string; +} + const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -46,11 +61,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const [pageSize] = useState(10); // ===== SUBMISSION STATE ===== + const [filterParams, setFilterParams] = useState({}); const [isSubmitted, setIsSubmitted] = useState(false); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); const filterModal = useModal(); - // ===== OPTIONS (Declare before filter state) ===== + // ===== OPTIONS ===== const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', @@ -87,127 +105,66 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { [] ); - // ===== APPLIED FILTER STATE (Yang sudah di-apply) ===== - const [appliedFilterArea, setAppliedFilterArea] = useState< - typeof areaOptions - >([]); - const [appliedFilterSupplier, setAppliedFilterSupplier] = useState< - typeof supplierOptions - >([]); - const [appliedFilterProduct, setAppliedFilterProduct] = useState< - typeof productOptions - >([]); - const [appliedFilterProductCategory, setAppliedFilterProductCategory] = - useState([]); - const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [appliedFilterSortBy, setAppliedFilterSortBy] = useState< - (typeof sortByOptions)[0] | null - >(null); - const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); - const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); - const [dateErrorShown, setDateErrorShown] = useState(false); - const [hasDateError, setHasDateError] = useState(false); - - // ===== PENDING FILTER STATE (Yang ada di modal, belum di-apply) ===== - const [filterArea, setFilterArea] = useState([]); - const [filterSupplier, setFilterSupplier] = useState( - [] - ); - const [filterProduct, setFilterProduct] = useState([]); - const [filterProductCategory, setFilterProductCategory] = useState< - typeof productCategoryOptions - >([]); - const [filterByType, setFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [filterSortBy, setFilterSortBy] = useState< - (typeof sortByOptions)[0] | null - >(null); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); - - // ===== FILTER HANDLERS ===== - const handleFilterModalOpen = useCallback(() => { - setFilterArea(appliedFilterArea); - setFilterSupplier(appliedFilterSupplier); - setFilterProduct(appliedFilterProduct); - setFilterProductCategory(appliedFilterProductCategory); - setFilterByType(appliedFilterByType); - setFilterSortBy(appliedFilterSortBy); - setFilterStartDate(appliedFilterStartDate); - setFilterEndDate(appliedFilterEndDate); + const handleFilterModalOpen = () => { filterModal.openModal(); - }, [ - filterModal, - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterByType, - appliedFilterSortBy, - appliedFilterStartDate, - appliedFilterEndDate, - ]); + }; - const handleResetFilters = useCallback(() => { - setIsSubmitted(false); - setFilterArea([]); - setFilterSupplier([]); - setFilterProduct([]); - setFilterProductCategory([]); - setFilterByType(null); - setFilterSortBy(null); - setFilterStartDate(''); - setFilterEndDate(''); - setAppliedFilterArea([]); - setAppliedFilterSupplier([]); - setAppliedFilterProduct([]); - setAppliedFilterProductCategory([]); - setAppliedFilterByType(null); - setAppliedFilterSortBy(null); - setAppliedFilterStartDate(''); - setAppliedFilterEndDate(''); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }, [dateErrorShown]); - - const handleApplyFilters = useCallback(() => { - setAppliedFilterArea(filterArea); - setAppliedFilterSupplier(filterSupplier); - setAppliedFilterProduct(filterProduct); - setAppliedFilterProductCategory(filterProductCategory); - setAppliedFilterByType(filterByType); - setAppliedFilterSortBy(filterSortBy); - setAppliedFilterStartDate(filterStartDate); - setAppliedFilterEndDate(filterEndDate); - setIsSubmitted(true); - setCurrentPage(1); - filterModal.closeModal(); - }, [ - filterModal, - filterArea, - filterSupplier, - filterProduct, - filterProductCategory, - filterByType, - filterSortBy, - filterStartDate, - filterEndDate, - ]); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + start_date: null, + end_date: null, + area_ids: null, + supplier_ids: null, + product_ids: null, + product_category_ids: null, + filter_by: null, + sort_by: null, + }, + validationSchema: PurchasesPerSupplierFilterSchema, + onSubmit: (values) => { + setFilterParams({ + start_date: values.start_date?.toString() || undefined, + end_date: values.end_date?.toString() || undefined, + area_ids: + values.area_ids?.map((v) => String(v.value)).join(',') || undefined, + supplier_ids: + values.supplier_ids?.map((v) => String(v.value)).join(',') || + undefined, + product_ids: + values.product_ids?.map((v) => String(v.value)).join(',') || + undefined, + product_category_ids: + values.product_category_ids?.map((v) => String(v.value)).join(',') || + undefined, + filter_by: values.filter_by?.value?.toString() || undefined, + sort_by: values.sort_by?.value?.toString() || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setCurrentPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setCurrentPage(1); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); + // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterStartDate(value); + formik.setFieldValue('start_date', value || null); - if (value && filterEndDate) { + if (value && formik.values.end_date) { const startDate = new Date(value); - const endDateObj = new Date(filterEndDate); + const endDateObj = new Date(formik.values.end_date); if (endDateObj < startDate) { setHasDateError(true); @@ -228,16 +185,16 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setHasDateError(false); } }, - [filterEndDate, dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterEndDate(value); + formik.setFieldValue('end_date', value || null); - if (value && filterStartDate) { - const startDateObj = new Date(filterStartDate); + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); const endDate = new Date(value); if (endDate < startDateObj) { @@ -258,59 +215,43 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setDateErrorShown(false); } }, - [filterStartDate, dateErrorShown] + [formik, dateErrorShown] ); // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; - // Date filter (start_date + end_date = 1 filter) - if (appliedFilterStartDate || appliedFilterEndDate) { + if (filterParams.start_date || filterParams.end_date) { count += 1; } - // Area filter - if (appliedFilterArea.length > 0) { + if (filterParams.area_ids) { count += 1; } - // Supplier filter - if (appliedFilterSupplier.length > 0) { + if (filterParams.supplier_ids) { count += 1; } - // Product filter - if (appliedFilterProduct.length > 0) { + if (filterParams.product_ids) { count += 1; } - // Product category filter - if (appliedFilterProductCategory.length > 0) { + if (filterParams.product_category_ids) { count += 1; } - // Filter by type filter - if (appliedFilterByType) { + if (filterParams.filter_by) { count += 1; } - // Sort by filter - if (appliedFilterSortBy) { + if (filterParams.sort_by) { count += 1; } return count; - }, [ - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterByType, - appliedFilterSortBy, - ]); + }, [filterParams]); const hasFilters = activeFiltersCount > 0; @@ -319,39 +260,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { isSubmitted ? () => { const params = { - area_id: - appliedFilterArea.length > 0 - ? appliedFilterArea.map((v) => String(v.value)).join(',') - : undefined, - supplier_id: - appliedFilterSupplier.length > 0 - ? appliedFilterSupplier.map((v) => String(v.value)).join(',') - : undefined, - product_id: - appliedFilterProduct.length > 0 - ? appliedFilterProduct.map((v) => String(v.value)).join(',') - : undefined, - product_category_id: - appliedFilterProductCategory.length > 0 - ? appliedFilterProductCategory - .map((v) => String(v.value)) - .join(',') - : undefined, - received_date: - appliedFilterByType?.value === 'received_date' - ? appliedFilterStartDate || undefined - : undefined, - po_date: - appliedFilterByType?.value === 'po_date' - ? appliedFilterStartDate || undefined - : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - sort_by: - (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, - filter_by: - (appliedFilterByType?.value as 'received_date' | 'po_date') || - undefined, + area_ids: filterParams.area_ids, + supplier_ids: filterParams.supplier_ids, + product_ids: filterParams.product_ids, + product_category_ids: filterParams.product_category_ids, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + sort_by: filterParams.sort_by, + filter_by: filterParams.filter_by, page: currentPage, limit: pageSize, }; @@ -361,12 +277,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { : null, ([, params]) => LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_id, - params.supplier_id, - params.product_id, - params.product_category_id, - params.received_date, - params.po_date, + params.area_ids, + params.supplier_ids, + params.product_ids, + params.product_category_ids, + params.filter_by === 'received_date' ? params.start_date : undefined, + params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, @@ -395,47 +311,25 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { LogisticPurchasePerSupplierReport[] | null > => { const params = { - area_id: - appliedFilterArea.length > 0 - ? appliedFilterArea.map((v) => String(v.value)).join(',') - : undefined, - supplier_id: - appliedFilterSupplier.length > 0 - ? appliedFilterSupplier.map((v) => String(v.value)).join(',') - : undefined, - product_id: - appliedFilterProduct.length > 0 - ? appliedFilterProduct.map((v) => String(v.value)).join(',') - : undefined, - product_category_id: - appliedFilterProductCategory.length > 0 - ? appliedFilterProductCategory.map((v) => String(v.value)).join(',') - : undefined, - received_date: - appliedFilterByType?.value === 'received_date' - ? appliedFilterStartDate || undefined - : undefined, - po_date: - appliedFilterByType?.value === 'po_date' - ? appliedFilterStartDate || undefined - : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - sort_by: (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, - filter_by: - (appliedFilterByType?.value as 'received_date' | 'po_date') || - undefined, + area_ids: filterParams.area_ids, + supplier_ids: filterParams.supplier_ids, + product_ids: filterParams.product_ids, + product_category_ids: filterParams.product_category_ids, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + sort_by: filterParams.sort_by, + filter_by: filterParams.filter_by, limit: 100, page: 1, }; const response = await LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_id, - params.supplier_id, - params.product_id, - params.product_category_id, - params.received_date, - params.po_date, + params.area_ids, + params.supplier_ids, + params.product_ids, + params.product_category_ids, + params.filter_by === 'received_date' ? params.start_date : undefined, + params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, @@ -447,16 +341,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) : null; - }, [ - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, - appliedFilterSortBy, - ]); + }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -496,39 +381,52 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return; } - const areaName = - appliedFilterArea.length > 0 - ? appliedFilterArea.map((c) => c.label).join(', ') || 'Semua Area' - : 'Semua Area'; + const areaName = filterParams.area_ids + ? areaOptions + .filter((opt) => + filterParams.area_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Area' + : 'Semua Area'; - const supplierName = - appliedFilterSupplier.length > 0 - ? appliedFilterSupplier.map((c) => c.label).join(', ') || - 'Semua Supplier' - : 'Semua Supplier'; + const supplierName = filterParams.supplier_ids + ? supplierOptions + .filter((opt) => + filterParams.supplier_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Supplier' + : 'Semua Supplier'; - const productName = - appliedFilterProduct.length > 0 - ? appliedFilterProduct.map((c) => c.label).join(', ') || - 'Semua Produk' - : 'Semua Produk'; + const productName = filterParams.product_ids + ? productOptions + .filter((opt) => + filterParams.product_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Produk' + : 'Semua Produk'; - const productCategoryName = - appliedFilterProductCategory.length > 0 - ? appliedFilterProductCategory.map((c) => c.label).join(', ') || - 'Semua Kategori Produk' - : 'Semua Kategori Produk'; + const productCategoryName = filterParams.product_category_ids + ? productCategoryOptions + .filter((opt) => + filterParams.product_category_ids + ?.split(',') + .includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Kategori Produk' + : 'Semua Kategori Produk'; const exportParams = { area_name: areaName, supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, - filter_by: - (appliedFilterByType?.value as 'received_date' | 'po_date') || - undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + filter_by: filterParams.filter_by, + start_date: filterParams.start_date, + end_date: filterParams.end_date, }; await generatePurchasesPerSupplierPDF({ @@ -543,13 +441,11 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { } }, [ logisticPurchasePerSupplierExport, - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, + filterParams, + areaOptions, + supplierOptions, + productOptions, + productCategoryOptions, ]); // ===== REGISTER TAB ACTIONS TO STORE ===== @@ -624,6 +520,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ tabId, hasFilters, @@ -633,6 +530,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setTabActions, ]); + // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); @@ -945,137 +843,184 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { -
- {/* Date Filter */} -
- -
- -
- +
+
+ {/* Date Filter */} +
+ +
+ +
+ +
+ + {/* Area Filter */} + { + formik.setFieldValue( + 'area_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Supplier Filter */} + { + formik.setFieldValue( + 'supplier_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Filter */} + { + formik.setFieldValue( + 'product_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingProducts} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Category Filter */} + { + formik.setFieldValue( + 'product_category_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingProductCategories} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Filter By Type */} + { + if (!Array.isArray(val)) { + formik.setFieldValue('filter_by', val); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> + + {/* Sort By */} + { + if (!Array.isArray(val)) { + formik.setFieldValue('sort_by', val); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + />
- {/* Area Filter */} - { - setFilterArea(Array.isArray(val) ? val : val ? [val] : []); - }} - isLoading={isLoadingAreas} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Supplier Filter */} - { - setFilterSupplier(Array.isArray(val) ? val : val ? [val] : []); - }} - isLoading={isLoadingSuppliers} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Product Filter */} - { - setFilterProduct(Array.isArray(val) ? val : val ? [val] : []); - }} - isLoading={isLoadingProducts} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Product Category Filter */} - { - setFilterProductCategory( - Array.isArray(val) ? val : val ? [val] : [] - ); - }} - isLoading={isLoadingProductCategories} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Filter By Type */} - { - if (!Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - isClearable={true} - /> - - {/* Sort By */} - { - if (!Array.isArray(val)) { - setFilterSortBy(val); - } - }} - className={{ wrapper: 'w-full' }} - isClearable={true} - /> -
- - {/* Modal Footer */} -
- - -
+ {/* Modal Footer */} +
+ + +
+ ); From 166e95930b8cb892730ed2108b2b758e94efe37d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 09:35:24 +0700 Subject: [PATCH 05/82] refactor(FE): Remove unused imports and cleanup comments --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 4a070b56..9702d904 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -2,7 +2,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; -import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { useSelect } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; @@ -520,7 +520,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ tabId, hasFilters, @@ -530,7 +529,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setTabActions, ]); - // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); From 62dd1de150c7745e9f13fd7cc31f9240189a77f3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 09:45:18 +0700 Subject: [PATCH 06/82] refactor(FE): Reset form values on filter modal open and submit --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 9702d904..09a96b23 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -107,6 +107,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const handleFilterModalOpen = () => { filterModal.openModal(); + formik.resetForm({ values: formik.values }); }; // ===== FORMIK SETUP ===== @@ -122,7 +123,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { sort_by: null, }, validationSchema: PurchasesPerSupplierFilterSchema, - onSubmit: (values) => { + onSubmit: (values, { resetForm }) => { setFilterParams({ start_date: values.start_date?.toString() || undefined, end_date: values.end_date?.toString() || undefined, @@ -143,6 +144,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { filterModal.closeModal(); setIsSubmitted(true); setCurrentPage(1); + resetForm({ values }); }, onReset: () => { setFilterParams({}); From 28dabcbeb6f40d61e2f9b1c336bdd0bf6f6937a9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 09:52:21 +0700 Subject: [PATCH 07/82] refactor(FE): Refactor filter parameter keys to singular form --- .../tab/PurchasesPerSupplierTab.tsx | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 09a96b23..794c45d6 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -40,10 +40,10 @@ interface PurchasesPerSupplierTabProps { } interface FilterParams { - area_ids?: string; - supplier_ids?: string; - product_ids?: string; - product_category_ids?: string; + area_id?: string; + supplier_id?: string; + product_id?: string; + product_category_id?: string; start_date?: string; end_date?: string; sort_by?: string; @@ -107,7 +107,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const handleFilterModalOpen = () => { filterModal.openModal(); - formik.resetForm({ values: formik.values }); + formik.validateForm(); }; // ===== FORMIK SETUP ===== @@ -123,19 +123,19 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { sort_by: null, }, validationSchema: PurchasesPerSupplierFilterSchema, - onSubmit: (values, { resetForm }) => { + onSubmit: (values, { setSubmitting }) => { setFilterParams({ start_date: values.start_date?.toString() || undefined, end_date: values.end_date?.toString() || undefined, - area_ids: + area_id: values.area_ids?.map((v) => String(v.value)).join(',') || undefined, - supplier_ids: + supplier_id: values.supplier_ids?.map((v) => String(v.value)).join(',') || undefined, - product_ids: + product_id: values.product_ids?.map((v) => String(v.value)).join(',') || undefined, - product_category_ids: + product_category_id: values.product_category_ids?.map((v) => String(v.value)).join(',') || undefined, filter_by: values.filter_by?.value?.toString() || undefined, @@ -144,7 +144,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { filterModal.closeModal(); setIsSubmitted(true); setCurrentPage(1); - resetForm({ values }); + setSubmitting(false); }, onReset: () => { setFilterParams({}); @@ -228,19 +228,19 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { count += 1; } - if (filterParams.area_ids) { + if (filterParams.area_id) { count += 1; } - if (filterParams.supplier_ids) { + if (filterParams.supplier_id) { count += 1; } - if (filterParams.product_ids) { + if (filterParams.product_id) { count += 1; } - if (filterParams.product_category_ids) { + if (filterParams.product_category_id) { count += 1; } @@ -262,10 +262,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { isSubmitted ? () => { const params = { - area_ids: filterParams.area_ids, - supplier_ids: filterParams.supplier_ids, - product_ids: filterParams.product_ids, - product_category_ids: filterParams.product_category_ids, + area_id: filterParams.area_id, + supplier_id: filterParams.supplier_id, + product_id: filterParams.product_id, + product_category_id: filterParams.product_category_id, start_date: filterParams.start_date, end_date: filterParams.end_date, sort_by: filterParams.sort_by, @@ -279,10 +279,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { : null, ([, params]) => LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_ids, - params.supplier_ids, - params.product_ids, - params.product_category_ids, + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, params.filter_by === 'received_date' ? params.start_date : undefined, params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, @@ -313,10 +313,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { LogisticPurchasePerSupplierReport[] | null > => { const params = { - area_ids: filterParams.area_ids, - supplier_ids: filterParams.supplier_ids, - product_ids: filterParams.product_ids, - product_category_ids: filterParams.product_category_ids, + area_id: filterParams.area_id, + supplier_id: filterParams.supplier_id, + product_id: filterParams.product_id, + product_category_id: filterParams.product_category_id, start_date: filterParams.start_date, end_date: filterParams.end_date, sort_by: filterParams.sort_by, @@ -326,10 +326,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { }; const response = await LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_ids, - params.supplier_ids, - params.product_ids, - params.product_category_ids, + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, params.filter_by === 'received_date' ? params.start_date : undefined, params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, @@ -383,37 +383,37 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return; } - const areaName = filterParams.area_ids + const areaName = filterParams.area_id ? areaOptions .filter((opt) => - filterParams.area_ids?.split(',').includes(String(opt.value)) + filterParams.area_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Area' : 'Semua Area'; - const supplierName = filterParams.supplier_ids + const supplierName = filterParams.supplier_id ? supplierOptions .filter((opt) => - filterParams.supplier_ids?.split(',').includes(String(opt.value)) + filterParams.supplier_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Supplier' : 'Semua Supplier'; - const productName = filterParams.product_ids + const productName = filterParams.product_id ? productOptions .filter((opt) => - filterParams.product_ids?.split(',').includes(String(opt.value)) + filterParams.product_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Produk' : 'Semua Produk'; - const productCategoryName = filterParams.product_category_ids + const productCategoryName = filterParams.product_category_id ? productCategoryOptions .filter((opt) => - filterParams.product_category_ids + filterParams.product_category_id ?.split(',') .includes(String(opt.value)) ) From fd78ca6ac1aa410f0789e0453dd7f5f82f27e263 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:28:36 +0700 Subject: [PATCH 08/82] refactor(FE): Refactor CustomerPaymentTab to use Formik for filter management --- .../finance/filter/CustomerPaymentFilter.ts | 31 ++ .../report/finance/tab/CustomerPaymentTab.tsx | 419 ++++++++---------- 2 files changed, 211 insertions(+), 239 deletions(-) create mode 100644 src/components/pages/report/finance/filter/CustomerPaymentFilter.ts diff --git a/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts new file mode 100644 index 00000000..60359038 --- /dev/null +++ b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts @@ -0,0 +1,31 @@ +import * as yup from 'yup'; + +export type CustomerPaymentFilterType = { + start_date: string | null; + end_date: string | null; + customer_ids: string | null; + filter_by: string | null; +}; + +export const CustomerPaymentFilterSchema = yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + customer_ids: yup.string().nullable(), + filter_by: yup.string().nullable(), +}); + +export type CustomerPaymentFilterValues = yup.InferType< + typeof CustomerPaymentFilterSchema +>; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4e0e3f25..723a1ebf 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -9,7 +9,6 @@ import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; -// import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; @@ -22,18 +21,30 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import Modal from '@/components/Modal'; -import { useModal } from '@/components/Modal'; +import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; +import { + CustomerPaymentFilterSchema, + CustomerPaymentFilterType, +} from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; +import { OptionType } from '@/components/table/TableRowSizeSelector'; interface CustomerPaymentTabProps { tabId: string; } +interface FilterParams { + customer_ids?: string; + start_date?: string; + end_date?: string; + filter_by?: string; +} + const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -46,31 +57,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== FILTER STATE ===== - const [appliedFilterCustomer, setAppliedFilterCustomer] = useState< - typeof customerOptions - >([]); - // TODO: Uncomment when BE is ready - // const [appliedFilterSales, setAppliedFilterSales] = useState< - // typeof salesOptions - // >([]); - const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); - const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [filterParams, setFilterParams] = useState({}); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const [filterCustomer, setFilterCustomer] = useState( - [] - ); - // TODO: Uncomment when BE is ready - // const [filterSales, setFilterSales] = useState([]); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); - const filterModal = useModal(); const dataTypeOptions = useMemo( @@ -81,10 +71,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { [] ); - const [filterByType, setFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const { options: customerOptions, setInputValue: setCustomerInputValue, @@ -92,14 +78,43 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); - // TODO: Uncomment when BE is ready - // const { - // options: salesOptions, - // setInputValue: setSalesInputValue, - // isLoadingOptions: isLoadingSales, - // loadMore: loadMoreSales, - // hasMore: hasMoreSales, - // } = useSelect(UserApi.basePath, 'id', 'name', 'search'); + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + start_date: null, + end_date: null, + customer_ids: null, + filter_by: null, + }, + validationSchema: CustomerPaymentFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + customer_ids: values.customer_ids || undefined, + filter_by: values.filter_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setCurrentPage(1); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setCurrentPage(1); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); const getPaymentStatusColor = (notes: string) => { const normalizedValue = notes.toLowerCase(); @@ -137,63 +152,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { .join(' '); }; - // ===== FILTER HANDLERS ===== - const handleFilterModalOpen = useCallback(() => { - setFilterCustomer(appliedFilterCustomer); - // setFilterSales(appliedFilterSales); - setFilterByType(appliedFilterByType); - setFilterStartDate(appliedFilterStartDate); - setFilterEndDate(appliedFilterEndDate); - filterModal.openModal(); - }, [ - filterModal, - appliedFilterCustomer, - appliedFilterByType, - appliedFilterStartDate, - appliedFilterEndDate, - ]); - - const handleResetFilters = useCallback(() => { - setIsSubmitted(false); - setFilterCustomer([]); - setFilterByType(null); - setFilterStartDate(''); - setFilterEndDate(''); - setAppliedFilterCustomer([]); - setAppliedFilterByType(null); - setAppliedFilterStartDate(''); - setAppliedFilterEndDate(''); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }, [dateErrorShown]); - - const handleApplyFilters = useCallback(() => { - setAppliedFilterCustomer(filterCustomer); - setAppliedFilterByType(filterByType); - setAppliedFilterStartDate(filterStartDate); - setAppliedFilterEndDate(filterEndDate); - setIsSubmitted(true); - setCurrentPage(1); - filterModal.closeModal(); - }, [ - filterModal, - filterCustomer, - filterByType, - filterStartDate, - filterEndDate, - ]); - + // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterStartDate(value); + formik.setFieldValue('start_date', value || null); - if (value && filterEndDate) { + if (value && formik.values.end_date) { const startDate = new Date(value); - const endDateObj = new Date(filterEndDate); + const endDateObj = new Date(formik.values.end_date); if (endDateObj < startDate) { setHasDateError(true); @@ -214,16 +181,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [filterEndDate, dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterEndDate(value); + formik.setFieldValue('end_date', value || null); - if (value && filterStartDate) { - const startDateObj = new Date(filterStartDate); + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); const endDate = new Date(value); if (endDate < startDateObj) { @@ -244,41 +211,46 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [filterStartDate, dateErrorShown] + [formik, dateErrorShown] ); + // ===== FILTER HELPERS ===== + const customerIdsValue = useMemo(() => { + if (!formik.values.customer_ids) return []; + return customerOptions.filter((opt) => + formik.values.customer_ids?.split(',').includes(String(opt.value)) + ); + }, [formik.values.customer_ids, customerOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || + null + ); + }, [formik.values.filter_by]); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; // Date filter (start_date + end_date = 1 filter) - if (appliedFilterStartDate || appliedFilterEndDate) { + if (filterParams.start_date || filterParams.end_date) { count += 1; } // Customer filter - if (appliedFilterCustomer.length > 0) { + if (filterParams.customer_ids) { count += 1; } // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) - if (appliedFilterByType) { + if (filterParams.filter_by) { count += 1; } - // TODO: Uncomment when BE is ready - // // Sales filter - // if (appliedFilterSales.length > 0) { - // count += 1; - // } - return count; - }, [ - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterCustomer, - appliedFilterByType, - ]); + }, [filterParams]); const hasFilters = activeFiltersCount > 0; @@ -287,21 +259,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { isSubmitted ? () => { const params = { - customer_ids: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((v) => String(v.value)).join(',') - : undefined, - // TODO: Uncomment when BE is ready - // sales_id: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((v) => String(v.value)).join(',') - // : undefined, - filter_by: appliedFilterByType?.value as + customer_ids: filterParams.customer_ids, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + start_date: filterParams.start_date, + end_date: filterParams.end_date, page: currentPage, limit: pageSize, }; @@ -333,21 +297,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { CustomerPaymentReport[] | null > => { const params = { - customer_ids: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((v) => String(v.value)).join(',') - : undefined, - // TODO: Uncomment when BE is ready - // sales_id: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((v) => String(v.value)).join(',') - // : undefined, - filter_by: appliedFilterByType?.value as + customer_ids: filterParams.customer_ids, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + start_date: filterParams.start_date, + end_date: filterParams.end_date, limit: 100, page: 1, }; @@ -364,13 +320,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [ - appliedFilterCustomer, - // appliedFilterSales, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, - ]); + }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -410,21 +360,22 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return; } + const customerName = filterParams.customer_ids + ? customerOptions + .filter((opt) => + filterParams.customer_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Customer' + : 'Semua Customer'; + await generateCustomerPaymentPDF({ data: allDataForExport, params: { - customer_name: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((c) => c.label).join(', ') - : undefined, - // TODO: Uncomment when BE is ready - // sales: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((s) => s.label).join(', ') - // : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - filter_by: appliedFilterByType?.value as + customer_name: customerName, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, @@ -436,7 +387,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [customerPaymentExport]); + }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== const setTabActions = useFinanceTabStore((state) => state.setTabActions); @@ -517,7 +468,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setTabActions, ]); - // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); @@ -931,95 +881,86 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
-
-
- -
- -
+
+
+
+ +
+ +
- + +
+ + { + formik.setFieldValue( + 'customer_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v: OptionType) => String(v.value)).join(',') + : null + ); + }} + onInputChange={setCustomerInputValue} + isLoading={isLoadingCustomers} + isClearable + onMenuScrollToBottom={loadMoreCustomers} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('filter_by', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + />
- { - setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setCustomerInputValue} - isLoading={isLoadingCustomers} - isClearable - onMenuScrollToBottom={loadMoreCustomers} - className={{ wrapper: 'w-full' }} - /> - - {/* TODO: Uncomment when BE is ready */} - {/*
- { - setFilterSales(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setSalesInputValue} - isLoading={isLoadingSales} - isClearable - onMenuScrollToBottom={loadMoreSales} - className={{ wrapper: 'w-full' }} - /> -
*/} - - { - if (val && !Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - /> - - {/* Action Buttons */} -
-
- - -
+ {/* Modal Footer */} +
+ + +
+ ); From e23b53d7979d6eb64bab5afd6c98c113d041b548 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:46:06 +0700 Subject: [PATCH 09/82] refactor(FE): Refactor CustomerPaymentTab to use StatusBadge component --- .../report/finance/tab/CustomerPaymentTab.tsx | 60 ++++++------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 723a1ebf..a0fb63ae 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { useSelect } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; @@ -11,7 +11,13 @@ import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; +import { + formatCurrency, + formatDate, + formatNumber, + formatTitleCase, + cn, +} from '@/lib/helper'; import { CustomerPaymentReport, CustomerPaymentSummary, @@ -33,6 +39,7 @@ import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/ex import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; +import { Color } from '@/types/theme'; interface CustomerPaymentTabProps { tabId: string; @@ -116,40 +123,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, }); - const getPaymentStatusColor = (notes: string) => { + const getPaymentStatusBadgeColor = (notes: string): Color => { const normalizedValue = notes.toLowerCase(); if (normalizedValue === 'lunas') { - return 'bg-info/10 text-black border-info'; + return 'primary'; } if (normalizedValue.includes('belum')) { - return 'bg-warning/10 text-black border-warning'; + return 'warning'; } - return 'bg-gray-100 text-black border-gray-300'; - }; - - const getPaymentStatusIndicatorColor = (notes: string) => { - const normalizedValue = notes.toLowerCase(); - - if (normalizedValue === 'lunas') { - return 'bg-info'; - } - - if (normalizedValue.includes('belum')) { - return 'bg-warning'; - } - - return 'bg-gray-400'; - }; - - const getPaymentStatusText = (notes: string) => { - return notes - .toLowerCase() - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); + return 'neutral'; }; // ===== DATE CHANGE HANDLERS ===== @@ -181,7 +166,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [formik, dateErrorShown] + [dateErrorShown] ); const handleEndDateChange = useCallback( @@ -211,7 +196,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [formik, dateErrorShown] + [dateErrorShown] ); // ===== FILTER HELPERS ===== @@ -685,17 +670,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } return ( - - {getPaymentStatusText(value)} - + ); }, }, From 322b519def4db1b92a3aee81863db64092f24ba8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:55:40 +0700 Subject: [PATCH 10/82] refactor(FE): Refactor filter schema and form handling for PurchasesPerSupplier --- .../filter/PurchasesPerSupplierFilter.ts | 110 ++++---------- .../tab/PurchasesPerSupplierTab.tsx | 137 ++++++++++-------- 2 files changed, 106 insertions(+), 141 deletions(-) diff --git a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts index 70a01615..b3d9943b 100644 --- a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts +++ b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts @@ -1,88 +1,38 @@ -import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; export type PurchasesPerSupplierFilterType = { - start_date: string | null | undefined; - end_date: string | null | undefined; - area_ids: OptionType[] | null | undefined; - supplier_ids: OptionType[] | null | undefined; - product_ids: OptionType[] | null | undefined; - product_category_ids: OptionType[] | null | undefined; - filter_by: OptionType | null | undefined; - sort_by: OptionType | null | undefined; + start_date: string | null; + end_date: string | null; + area_ids: string | null; + supplier_ids: string | null; + product_ids: string | null; + product_category_ids: string | null; + filter_by: string | null; + sort_by: string | null; }; -export const PurchasesPerSupplierFilterSchema: yup.ObjectSchema = - yup.object({ - start_date: yup.string().optional().nullable(), - end_date: yup - .string() - .optional() - .nullable() - .test( - 'is-greater-than-start', - 'Tanggal akhir tidak boleh masa lampau', - function (value) { - const { start_date } = this.parent; - if (!start_date || !value) return true; - return new Date(value) >= new Date(start_date); - } - ), - area_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - supplier_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - product_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - product_category_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - filter_by: yup - .object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - .optional() - .nullable(), - sort_by: yup - .object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - .optional() - .nullable(), - }); +export const PurchasesPerSupplierFilterSchema = yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + area_ids: yup.string().nullable(), + supplier_ids: yup.string().nullable(), + product_ids: yup.string().nullable(), + product_category_ids: yup.string().nullable(), + filter_by: yup.string().nullable(), + sort_by: yup.string().nullable(), +}); export type PurchasesPerSupplierFilterValues = yup.InferType< typeof PurchasesPerSupplierFilterSchema diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 794c45d6..23fb067e 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -125,21 +125,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { validationSchema: PurchasesPerSupplierFilterSchema, onSubmit: (values, { setSubmitting }) => { setFilterParams({ - start_date: values.start_date?.toString() || undefined, - end_date: values.end_date?.toString() || undefined, - area_id: - values.area_ids?.map((v) => String(v.value)).join(',') || undefined, - supplier_id: - values.supplier_ids?.map((v) => String(v.value)).join(',') || - undefined, - product_id: - values.product_ids?.map((v) => String(v.value)).join(',') || - undefined, - product_category_id: - values.product_category_ids?.map((v) => String(v.value)).join(',') || - undefined, - filter_by: values.filter_by?.value?.toString() || undefined, - sort_by: values.sort_by?.value?.toString() || undefined, + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + area_id: values.area_ids || undefined, + supplier_id: values.supplier_ids || undefined, + product_id: values.product_ids || undefined, + product_category_id: values.product_category_ids || undefined, + filter_by: values.filter_by || undefined, + sort_by: values.sort_by || undefined, }); filterModal.closeModal(); setIsSubmitted(true); @@ -220,6 +213,48 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { [formik, dateErrorShown] ); + // ===== DERIVED VALUES ===== + const areaIdsValue = useMemo(() => { + if (!formik.values.area_ids) return []; + const ids = formik.values.area_ids.split(','); + return areaOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.area_ids, areaOptions]); + + const supplierIdsValue = useMemo(() => { + if (!formik.values.supplier_ids) return []; + const ids = formik.values.supplier_ids.split(','); + return supplierOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.supplier_ids, supplierOptions]); + + const productIdsValue = useMemo(() => { + if (!formik.values.product_ids) return []; + const ids = formik.values.product_ids.split(','); + return productOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.product_ids, productOptions]); + + const productCategoryIdsValue = useMemo(() => { + if (!formik.values.product_category_ids) return []; + const ids = formik.values.product_category_ids.split(','); + return productCategoryOptions.filter((opt) => + ids.includes(String(opt.value)) + ); + }, [formik.values.product_category_ids, productCategoryOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || + null + ); + }, [formik.values.filter_by, dataTypeOptions]); + + const sortByValue = useMemo(() => { + if (!formik.values.sort_by) return null; + return ( + sortByOptions.find((opt) => opt.value === formik.values.sort_by) || null + ); + }, [formik.values.sort_by, sortByOptions]); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; @@ -875,17 +910,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Area' placeholder='Pilih Area' options={areaOptions} - value={ - (formik.values.area_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={areaIdsValue} onChange={(val) => { formik.setFieldValue( 'area_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingAreas} @@ -898,17 +929,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Supplier' placeholder='Pilih Supplier' options={supplierOptions} - value={ - (formik.values.supplier_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={supplierIdsValue} onChange={(val) => { formik.setFieldValue( 'supplier_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingSuppliers} @@ -921,17 +948,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Produk' placeholder='Pilih Produk' options={productOptions} - value={ - (formik.values.product_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={productIdsValue} onChange={(val) => { formik.setFieldValue( 'product_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingProducts} @@ -944,17 +967,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Kategori Produk' placeholder='Pilih Kategori Produk' options={productCategoryOptions} - value={ - (formik.values.product_category_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={productCategoryIdsValue} onChange={(val) => { formik.setFieldValue( 'product_category_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingProductCategories} @@ -967,15 +986,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Filter Berdasarkan' placeholder='Pilih Filter Berdasarkan' options={dataTypeOptions} - value={ - (formik.values.filter_by as - | { value: string; label: string } - | null - | undefined) || null - } + value={filterByValue} onChange={(val) => { if (!Array.isArray(val)) { - formik.setFieldValue('filter_by', val); + formik.setFieldValue( + 'filter_by', + val?.value?.toString() || null + ); } }} className={{ wrapper: 'w-full' }} @@ -987,15 +1004,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Urutkan Berdasarkan' placeholder='Pilih Urutkan Berdasarkan' options={sortByOptions} - value={ - (formik.values.sort_by as - | { value: string; label: string } - | null - | undefined) || null - } + value={sortByValue} onChange={(val) => { if (!Array.isArray(val)) { - formik.setFieldValue('sort_by', val); + formik.setFieldValue( + 'sort_by', + val?.value?.toString() || null + ); } }} className={{ wrapper: 'w-full' }} From ee53ea61ccbee946516d9942b458c1cfcf71bee4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:57:22 +0700 Subject: [PATCH 11/82] refactor(FE): Fix missing dependency in useCallback hooks --- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index a0fb63ae..3680a41c 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -166,7 +166,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( @@ -196,7 +196,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [dateErrorShown] + [formik, dateErrorShown] ); // ===== FILTER HELPERS ===== From 6d2855d1173c373fdfe3411ff4b3d2d150580fd8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:15:42 +0700 Subject: [PATCH 12/82] refactor(FE): Add zustand store for marketing tab actions --- .../marketing-tab/marketing-tab.store.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/stores/marketing-tab/marketing-tab.store.ts diff --git a/src/stores/marketing-tab/marketing-tab.store.ts b/src/stores/marketing-tab/marketing-tab.store.ts new file mode 100644 index 00000000..153bbb8d --- /dev/null +++ b/src/stores/marketing-tab/marketing-tab.store.ts @@ -0,0 +1,51 @@ +'use client'; + +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export type MarketingTabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useMarketingTabStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set( + (state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + }), + false, + 'setTabActions' + ), + + clearTabActions: (tabId) => + set( + (state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }, + false, + 'clearTabActions' + ), + + clearAllTabActions: () => + set({ tabActions: {} }, false, 'clearAllTabActions'), + }), + { + name: 'MarketingTabStore', + } + ) +); From 43d26b4833d16d99c857c54e33735e4410dde72c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:16:26 +0700 Subject: [PATCH 13/82] refactor(FE): Refactor marketing report components and add HPP filter --- src/app/report/marketing/page.tsx | 6 +- .../marketing/MarketingReportContent.tsx | 59 +- .../marketing/filter/HppPerKandangFilter.ts | 40 + .../skeleton/HppPerKandangSkeleton.tsx | 37 + .../report/marketing/tab/HppPerKandangTab.tsx | 919 +++++++++++------- 5 files changed, 658 insertions(+), 403 deletions(-) create mode 100644 src/components/pages/report/marketing/filter/HppPerKandangFilter.ts create mode 100644 src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 87ed7a1a..57035844 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,11 +1,7 @@ import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; const MarketingReportPage = () => { - return ( -
- -
- ); + return ; }; export default MarketingReportPage; diff --git a/src/components/pages/report/marketing/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingReportContent.tsx index e38a39d4..9277311f 100644 --- a/src/components/pages/report/marketing/MarketingReportContent.tsx +++ b/src/components/pages/report/marketing/MarketingReportContent.tsx @@ -1,47 +1,42 @@ 'use client'; -import { JSX, useState } from 'react'; - +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; - -type MarketingReportTabType = - | 'daily' - | 'transaction' - | 'hpp-comparison' - | 'daily-hpp'; - -const marketingReportTabs: { - id: MarketingReportTabType; - label: string; - content: JSX.Element; -}[] = [ - { - id: 'daily', - label: 'Penjualan Harian', - content: , - }, - { - id: 'daily-hpp', - label: 'HPP Harian Kandang', - content: , - }, -]; +import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; const MarketingReportContent = () => { - const [activeTab, setActiveTab] = useState('daily'); + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useMarketingTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Penjualan Harian', + content: , + }, + { + id: '2', + label: 'HPP Harian Kandang', + content: , + }, + ]; return ( -
+
); diff --git a/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts new file mode 100644 index 00000000..57d2dcd2 --- /dev/null +++ b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts @@ -0,0 +1,40 @@ +import * as yup from 'yup'; + +export type HppPerKandangFilterType = { + area_id: string | null; + location_id: string | null; + kandang_id: string | null; + weight_min: string | null; + weight_max: string | null; + period: string | null; + sort_by: string | null; + show_unrecorded: boolean | null; +}; + +export const HppPerKandangFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + kandang_id: yup.string().nullable(), + weight_min: yup.string().nullable(), + weight_max: yup + .string() + .nullable() + .test( + 'is-greater-than-min', + 'Rentang bobot max tidak boleh lebih kecil dari min', + function (value) { + const { weight_min } = this.parent; + if (!weight_min || !value) return true; + const weightMinNum = parseFloat(weight_min) || 0; + const weightMaxNum = parseFloat(value) || 0; + return weightMaxNum >= weightMinNum; + } + ), + period: yup.string().required('Periode wajib diisi'), + sort_by: yup.string().nullable(), + show_unrecorded: yup.boolean().nullable(), +}); + +export type HppPerKandangFilterValues = yup.InferType< + typeof HppPerKandangFilterSchema +>; diff --git a/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx b/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx new file mode 100644 index 00000000..42a6cf56 --- /dev/null +++ b/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppPerKandangSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default HppPerKandangSkeleton; diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index c0371abf..514edcb9 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -1,11 +1,6 @@ import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; import useSWR from 'swr'; -import Card from '@/components/Card'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; +import { useSelect } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; import NumberInput from '@/components/input/NumberInput'; import { AreaApi } from '@/services/api/master-data'; @@ -21,7 +16,6 @@ import { HppPerKandangPerWeightRange, } from '@/types/api/report/hpp-per-kandang'; import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; @@ -30,8 +24,35 @@ import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/ex import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; +import { useFormik } from 'formik'; +import { + HppPerKandangFilterSchema, + HppPerKandangFilterType, +} from '@/components/pages/report/marketing/filter/HppPerKandangFilter'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import Modal, { useModal } from '@/components/Modal'; +import { cn } from '@/lib/helper'; +import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; +import { useEffect as useEffectHook } from 'react'; -const HppPerKandangTab = () => { +interface HppPerKandangTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + kandang_id?: string; + weight_min?: string; + weight_max?: string; + period?: string; + sort_by?: string; + show_unrecorded?: boolean; +} + +const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -42,190 +63,229 @@ const HppPerKandangTab = () => { // ===== VALIDATION STATE ===== const [weightMaxError, setWeightMaxError] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); - // ===== TABLE FILTER STATE ===== - const { state: tableFilterState, updateFilter } = useTableFilter({ - initial: { - area_id: [] as string[], - location_id: [] as string[], - kandang_id: [] as string[], - weight_min: '', - weight_max: '', - period: '', - sort_by: '', - show_unrecorded: false, - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); + // ===== FILTER STATE ===== + const [filterParams, setFilterParams] = useState({}); - const { - setInputValue: setAreaInputValue, - options: areaOptions, - isLoadingOptions: isLoadingAreas, - loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + const filterModal = useModal(); - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocations, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangs, - loadMore: loadMoreKandangs, - } = useSelect( - ProjectFlockKandangApi.basePath, + // ===== OPTIONS ===== + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, 'id', - 'name_with_period', + 'name', 'search' ); - const showUnrecordedOptions: OptionType[] = [ - { value: 'false', label: 'Sembunyikan' }, - { value: 'true', label: 'Tampilkan' }, - ]; + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); - const areaChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'area_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] + const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect( + ProjectFlockKandangApi.basePath, + 'id', + 'name_with_period', + 'search' + ); + + const showUnrecordedOptions = useMemo( + () => [ + { value: 'false', label: 'Sembunyikan' }, + { value: 'true', label: 'Tampilkan' }, + ], + [] ); - const locationChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'location_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + kandang_id: null, + weight_min: null, + weight_max: null, + period: null, + sort_by: null, + show_unrecorded: null, }, - [updateFilter] - ); - - const kandangChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'kandang_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); + validationSchema: HppPerKandangFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + area_id: values.area_id || undefined, + location_id: values.location_id || undefined, + kandang_id: values.kandang_id || undefined, + weight_min: values.weight_min || undefined, + weight_max: values.weight_max || undefined, + period: values.period || undefined, + sort_by: values.sort_by || undefined, + show_unrecorded: + values.show_unrecorded !== null ? values.show_unrecorded : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setSubmitting(false); }, - [updateFilter] - ); - - const weightMinChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); + onReset: () => { + setFilterParams({}); setIsSubmitted(false); + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); - if (weightMaxError) { + // ===== WEIGHT CHANGE HANDLERS ===== + const handleWeightMinChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('weight_min', value || null); + + if (value && formik.values.weight_max) { + const weightMin = parseFloat(value) || 0; + const weightMax = parseFloat(formik.values.weight_max) || 0; + + if (weightMax < weightMin) { + setWeightMaxError( + 'Rentang bobot max tidak boleh lebih kecil dari min' + ); + if (!dateErrorShown) { + toast.error('Rentang bobot max tidak boleh lebih kecil dari min', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { setWeightMaxError(''); } }, - [updateFilter, weightMaxError] + [formik, dateErrorShown] ); - const weightMaxChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - const weightMax = val ? parseFloat(val) || 0 : 0; - const weightMin = tableFilterState.weight_min - ? parseFloat(tableFilterState.weight_min) - : 0; + const handleWeightMaxChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('weight_max', value || null); - if (weightMax < weightMin) { - setWeightMaxError('Rentang bobot max tidak boleh lebih kecil dari min'); - toast.error('Rentang bobot max tidak boleh lebih kecil dari min'); - return; + if (value && formik.values.weight_min) { + const weightMin = parseFloat(formik.values.weight_min) || 0; + const weightMax = parseFloat(value) || 0; + + if (weightMax < weightMin) { + setWeightMaxError( + 'Rentang bobot max tidak boleh lebih kecil dari min' + ); + if (!dateErrorShown) { + toast.error('Rentang bobot max tidak boleh lebih kecil dari min', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } } setWeightMaxError(''); - updateFilter('weight_max', val ? String(weightMax) : ''); - setIsSubmitted(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }, - [updateFilter, tableFilterState.weight_min] + [formik, dateErrorShown] ); - const periodChangeHandler = useCallback>( - (e) => { - const val = e.target.value; - updateFilter('period', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== DERIVED VALUES ===== + const areaIdsValue = useMemo(() => { + if (!formik.values.area_id) return []; + const ids = formik.values.area_id.split(','); + return areaOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.area_id, areaOptions]); - const showUnrecordedChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('show_unrecorded', newVal?.value === 'true'); - setIsSubmitted(false); - }, - [updateFilter] - ); + const locationIdsValue = useMemo(() => { + if (!formik.values.location_id) return []; + const ids = formik.values.location_id.split(','); + return locationOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.location_id, locationOptions]); - const resetFilters = useCallback(() => { - updateFilter('area_id', []); - updateFilter('location_id', []); - updateFilter('kandang_id', []); - updateFilter('weight_min', ''); - updateFilter('weight_max', ''); - updateFilter('period', ''); - updateFilter('sort_by', ''); - updateFilter('show_unrecorded', false); - setIsSubmitted(false); - }, [updateFilter]); + const kandangIdsValue = useMemo(() => { + if (!formik.values.kandang_id) return []; + const ids = formik.values.kandang_id.split(','); + return kandangOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.kandang_id, kandangOptions]); - const handleSubmit = useCallback(() => { - if (!tableFilterState.period) { - toast.error('Periode wajib diisi'); - return; + const showUnrecordedValue = useMemo(() => { + if (formik.values.show_unrecorded === null) return null; + return ( + showUnrecordedOptions.find( + (opt) => opt.value === String(formik.values.show_unrecorded) + ) || null + ); + }, [formik.values.show_unrecorded, showUnrecordedOptions]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.period) { + count += 1; } - setIsSubmitted(true); - }, [tableFilterState.period]); + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.location_id) { + count += 1; + } + + if (filterParams.kandang_id) { + count += 1; + } + + if (filterParams.weight_min || filterParams.weight_max) { + count += 1; + } + + if (filterParams.show_unrecorded !== undefined) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( isSubmitted ? () => { const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - location_id: - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id.join(',') - : undefined, - kandang_id: - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id.join(',') - : undefined, - weight_min: tableFilterState.weight_min || undefined, - weight_max: tableFilterState.weight_max || undefined, - period: tableFilterState.period || undefined, - sort_by: tableFilterState.sort_by || undefined, - show_unrecorded: tableFilterState.show_unrecorded, + area_id: filterParams.area_id, + location_id: filterParams.location_id, + kandang_id: filterParams.kandang_id, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + period: filterParams.period, + sort_by: filterParams.sort_by, + show_unrecorded: filterParams.show_unrecorded, }; return ['hpp-per-kandang-report', params]; @@ -275,23 +335,14 @@ const HppPerKandangTab = () => { const hppPerKandangExport = useCallback(async (): Promise => { const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - location_id: - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id.join(',') - : undefined, - kandang_id: - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id.join(',') - : undefined, - weight_min: tableFilterState.weight_min || undefined, - weight_max: tableFilterState.weight_max || undefined, - period: tableFilterState.period || undefined, - sort_by: tableFilterState.sort_by || undefined, - show_unrecorded: tableFilterState.show_unrecorded, + area_id: filterParams.area_id, + location_id: filterParams.location_id, + kandang_id: filterParams.kandang_id, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + period: filterParams.period, + sort_by: filterParams.sort_by, + show_unrecorded: filterParams.show_unrecorded, limit: 10000, page: 1, }; @@ -308,7 +359,7 @@ const HppPerKandangTab = () => { ); return isResponseSuccess(response) ? response.data : null; - }, [tableFilterState]); + }, [filterParams]); // ===== TABLE COLUMNS DEFINITION ===== const allFeedSuppliers = useMemo(() => { @@ -373,38 +424,32 @@ const HppPerKandangTab = () => { return; } - const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' - : 'Semua Area'; + const areaName = filterParams.area_id + ? areaOptions + .filter((opt) => + filterParams.area_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Area' + : 'Semua Area'; - const locationName = - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id - .map( - (id) => - locationOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Lokasi' - : 'Semua Lokasi'; + const locationName = filterParams.location_id + ? locationOptions + .filter((opt) => + filterParams.location_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Lokasi' + : 'Semua Lokasi'; - const kandangName = - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id - .map( - (id) => - kandangOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Kandang' - : 'Semua Kandang'; + const kandangName = filterParams.kandang_id + ? kandangOptions + .filter((opt) => + filterParams.kandang_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Kandang' + : 'Semua Kandang'; await generateHppPerKandangPDF( { @@ -413,11 +458,12 @@ const HppPerKandangTab = () => { area_name: areaName, location_name: locationName, kandang_name: kandangName, - period: tableFilterState.period, - weight_min: tableFilterState.weight_min, - weight_max: tableFilterState.weight_max, - show_unrecorded: tableFilterState.show_unrecorded.toString(), - sort_by: tableFilterState.sort_by, + period: filterParams.period, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + show_unrecorded: + filterParams.show_unrecorded?.toString() || 'false', + sort_by: filterParams.sort_by, }, }, allFeedSuppliers, @@ -432,7 +478,7 @@ const HppPerKandangTab = () => { } }, [ hppPerKandangExport, - tableFilterState, + filterParams, areaOptions, locationOptions, kandangOptions, @@ -440,6 +486,91 @@ const HppPerKandangTab = () => { allDocSuppliers, ]); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useMarketingTabStore((state) => state.setTabActions); + const clearTabActions = useMarketingTabStore( + (state) => state.clearTabActions + ); + + useEffectHook(() => { + setTabActions( + tabId, +
+ + + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { @@ -682,161 +813,50 @@ const HppPerKandangTab = () => { ); return ( -
- HPP Harian Kandang (${period})` - : 'Laporan > HPP Harian Kandang' - } - className={{ wrapper: 'w-full', body: 'p-1!' }} - > -
- - (tableFilterState.area_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={areaChangeHandler} - onInputChange={setAreaInputValue} - onMenuScrollToBottom={loadMoreAreas} - isLoading={isLoadingAreas} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> - - (tableFilterState.location_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={locationChangeHandler} - onInputChange={setLocationInputValue} - onMenuScrollToBottom={loadMoreLocations} - isLoading={isLoadingLocations} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> - - (tableFilterState.kandang_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={kandangChangeHandler} - onInputChange={setKandangInputValue} - onMenuScrollToBottom={loadMoreKandangs} - isLoading={isLoadingKandangs} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> -
- -
-
- - -
- - opt.value === 'true') || - null - : showUnrecordedOptions.find((opt) => opt.value === 'false') || - null - } - onChange={showUnrecordedChangeHandler} - /> -
- -
- - - - Export - - - } - align='end' - > - - - - - -
- -
- + <> +
{!isSubmitted ? ( -
- Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -
+ + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? ( -
- -
+ + } + title='Memuat Data HPP Per Kandang' + subtitle='Silakan tunggu sebentar...' + /> ) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
+ + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : (
{ renderFooter={data.length > 0} renderCustomRow={renderCustomRow} className={{ - containerClassName: 'w-full mt-6', - tableWrapperClassName: 'overflow-x-auto mt-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -862,8 +882,175 @@ const HppPerKandangTab = () => { }} /> )} - - + + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ {/* Period Filter */} +
+ { + formik.setFieldValue('period', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + required={true} + isError={!!formik.errors.period && formik.touched.period} + /> + {formik.errors.period && formik.touched.period && ( +
+ {formik.errors.period} +
+ )} +
+ + {/* Area Filter */} + { + formik.setFieldValue( + 'area_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Location Filter */} + { + formik.setFieldValue( + 'location_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingLocations} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Kandang Filter */} + { + formik.setFieldValue( + 'kandang_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingKandangs} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Weight Range Filter */} +
+ +
+ +
+ +
+
+ + {/* Show Unrecorded Filter */} + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'show_unrecorded', + val?.value === 'true' || null + ); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ ); }; From 5e4619fac7cdee9f516509e28c23c69456d3d2a3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:38:46 +0700 Subject: [PATCH 14/82] feat(FE): Add DailyMarketingReportFilter and DailyMarketingReportSkeleton components --- .../filter/DailyMarketingReportFilter.ts | 42 +++++++++++++++++++ .../skeleton/DailyMarketingReportSkeleton.tsx | 37 ++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts create mode 100644 src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx diff --git a/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts b/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts new file mode 100644 index 00000000..85c765a9 --- /dev/null +++ b/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts @@ -0,0 +1,42 @@ +import * as yup from 'yup'; + +export type DailyMarketingReportFilterType = { + search: string | null; + area_id: string | null; + location_id: string | null; + warehouse_id: string | null; + customer_id: string | null; + start_date: string | null; + end_date: string | null; + marketing_type: string | null; + filter_by: string | null; + sort_by: string | null; +}; + +export const DailyMarketingReportFilterSchema = yup.object({ + search: yup.string().nullable(), + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + warehouse_id: yup.string().nullable(), + customer_id: yup.string().nullable(), + start_date: yup.string().nullable(), + end_date: yup + .string() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + marketing_type: yup.string().nullable(), + filter_by: yup.string().nullable(), + sort_by: yup.string().nullable(), +}); + +export type DailyMarketingReportFilterValues = yup.InferType< + typeof DailyMarketingReportFilterSchema +>; diff --git a/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx b/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx new file mode 100644 index 00000000..ad68b8f6 --- /dev/null +++ b/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { DailyMarketingRow } from '@/types/api/report/marketing.d'; +import { ColumnDef } from '@tanstack/react-table'; + +const DailyMarketingReportSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default DailyMarketingReportSkeleton; From 4b6a8b27731123e4a8c1c07b51d26552b68e41fd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:45:50 +0700 Subject: [PATCH 15/82] refactor(FE): Refactor file names for consistency in marketing report components --- .../pages/report/marketing/MarketingReportContent.tsx | 2 +- .../pages/report/marketing/export/DailyMarketingExportXLSX.tsx | 0 .../{DailyMarketingReportPDF.tsx => DailyMarketingPDF.tsx} | 0 .../{DailyMarketingReportFilter.ts => DailyMarketingFilter.ts} | 0 ...lyMarketingReportSkeleton.tsx => DailyMarketingSkeleton.tsx} | 0 .../{DailyMarketingReportContent.tsx => DailyMarketingTab.tsx} | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx rename src/components/pages/report/marketing/export/{DailyMarketingReportPDF.tsx => DailyMarketingPDF.tsx} (100%) rename src/components/pages/report/marketing/filter/{DailyMarketingReportFilter.ts => DailyMarketingFilter.ts} (100%) rename src/components/pages/report/marketing/skeleton/{DailyMarketingReportSkeleton.tsx => DailyMarketingSkeleton.tsx} (100%) rename src/components/pages/report/marketing/tab/{DailyMarketingReportContent.tsx => DailyMarketingTab.tsx} (99%) diff --git a/src/components/pages/report/marketing/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingReportContent.tsx index 9277311f..8a814689 100644 --- a/src/components/pages/report/marketing/MarketingReportContent.tsx +++ b/src/components/pages/report/marketing/MarketingReportContent.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; -import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent'; +import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingPDF.tsx similarity index 100% rename from src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx rename to src/components/pages/report/marketing/export/DailyMarketingPDF.tsx diff --git a/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts b/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts similarity index 100% rename from src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts rename to src/components/pages/report/marketing/filter/DailyMarketingFilter.ts diff --git a/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx b/src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx similarity index 100% rename from src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx rename to src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx diff --git a/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx similarity index 99% rename from src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx rename to src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index ca5ec12f..9f1d6d42 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -16,7 +16,7 @@ import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingPDF'; import { Area } from '@/types/api/master-data/area'; import { From 325fb373a8ba6a3f7ff5720a4b1e0c1c265e528c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:19:16 +0700 Subject: [PATCH 16/82] refactor(FE): Rename components for clarity --- src/app/report/marketing/page.tsx | 2 +- .../marketing/{MarketingReportContent.tsx => MarketingTabs.tsx} | 0 .../{DailyMarketingPDF.tsx => DailyMarketingExportPDF.tsx} | 0 src/components/pages/report/marketing/tab/DailyMarketingTab.tsx | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/components/pages/report/marketing/{MarketingReportContent.tsx => MarketingTabs.tsx} (100%) rename src/components/pages/report/marketing/export/{DailyMarketingPDF.tsx => DailyMarketingExportPDF.tsx} (100%) diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 57035844..cb79f109 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,4 +1,4 @@ -import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; +import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs'; const MarketingReportPage = () => { return ; diff --git a/src/components/pages/report/marketing/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx similarity index 100% rename from src/components/pages/report/marketing/MarketingReportContent.tsx rename to src/components/pages/report/marketing/MarketingTabs.tsx diff --git a/src/components/pages/report/marketing/export/DailyMarketingPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx similarity index 100% rename from src/components/pages/report/marketing/export/DailyMarketingPDF.tsx rename to src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 9f1d6d42..cedf979f 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -16,7 +16,7 @@ import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingPDF'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; import { Area } from '@/types/api/master-data/area'; import { From dbcf46912334db250a9baab4ca86c70f3b0f7021 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:44:22 +0700 Subject: [PATCH 17/82] refactor(FE): Remove DailyMarketingsTable component and refactor related files --- .../report/marketing/DailyMarketingsTable.tsx | 289 ---- .../pages/report/marketing/MarketingTabs.tsx | 2 +- .../marketing/tab/DailyMarketingTab.tsx | 1311 +++++++++++------ 3 files changed, 887 insertions(+), 715 deletions(-) delete mode 100644 src/components/pages/report/marketing/DailyMarketingsTable.tsx diff --git a/src/components/pages/report/marketing/DailyMarketingsTable.tsx b/src/components/pages/report/marketing/DailyMarketingsTable.tsx deleted file mode 100644 index 4904ef16..00000000 --- a/src/components/pages/report/marketing/DailyMarketingsTable.tsx +++ /dev/null @@ -1,289 +0,0 @@ -'use client'; - -import { ChangeEventHandler, useEffect, useState } from 'react'; -import useSWR from 'swr'; -import { ColumnDef, SortingState } from '@tanstack/react-table'; - -import { Icon } from '@iconify/react'; -import Table from '@/components/Table'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; - -import { - cn, - formatCurrency, - formatDate, - formatNumber, - formatVechicleNumber, -} from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { DailyMarketingRow } from '@/types/api/report/marketing'; -import { MarketingReportApi } from '@/services/api/report/marketing-report'; - -interface DailyMarketingsTableProps { - dailyMarketingsReportUrl: string; - onSetPage: (page: number) => void; - pageSize: number; - onSetPageSize: (pageSize: number) => void; - searchValue: string; - onSearchChange: ChangeEventHandler; - onFilterByChange: (filterBy: string) => void; - onSortByChange: (sort: 'asc' | 'desc' | '') => void; -} - -const DailyMarketingsTable = ({ - dailyMarketingsReportUrl, - onSetPage, - pageSize, - onSetPageSize, - searchValue, - onSearchChange, - onFilterByChange, - onSortByChange, -}: DailyMarketingsTableProps) => { - const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR( - dailyMarketingsReportUrl, - MarketingReportApi.getAllDailyMarketingFetcher, - { - keepPreviousData: true, - } - ); - - const [open, setOpen] = useState(true); - - const [sorting, setSorting] = useState([]); - - const dailyMarketingColumns: ColumnDef[] = [ - { - header: 'No', - cell: (props) => props.row.index + 1, - }, - { - accessorKey: 'so_date', - header: 'Tanggal Jual', - cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), - footer: 'Total', - }, - { - accessorKey: 'realization_date', - header: 'Tanggal Realisasi', - cell: (props) => - formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), - }, - { - accessorKey: 'aging_days', - header: 'Aging', - cell: (props) => `${props.row.original.aging_days} hari`, - }, - { - accessorKey: 'warehouse', - header: 'Gudang', - cell: ({ row }) => row.original.warehouse.name, - }, - { - accessorKey: 'customer', - header: 'Pelanggan', - cell: ({ row }) => row.original.customer.name, - }, - { - accessorKey: 'do_number', - header: 'No. DO', - enableSorting: false, - }, - { - accessorKey: 'sales_person', - header: 'Sales/Marketing', - cell: (props) => props.row.original.sales.name, - }, - { - accessorKey: 'vehicle_number', - header: 'No. Polisi', - cell: (props) => ( - - {formatVechicleNumber(props.row.original.vehicle_number)} - - ), - }, - { - accessorKey: 'marketing_type', - header: 'Marketing Type', - enableSorting: false, - }, - { - accessorKey: 'product', - header: 'Produk', - cell: ({ row }) => row.original.product.name, - }, - { - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => formatNumber(props.row.original.qty), - footer: () => { - const totalQty = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_qty - : 0; - - return totalQty ? formatNumber(totalQty) : '-'; - }, - }, - { - accessorKey: 'average_weight', - header: 'Bobot Rata-Rata (Kg)', - cell: (props) => formatNumber(props.row.original.average_weight_kg), - footer: () => { - const totalAverageWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.average_weight_kg - : 0; - - return totalAverageWeightKg ? formatNumber(totalAverageWeightKg) : '-'; - }, - }, - { - accessorKey: 'total_weight', - header: 'Bobot Total (Kg)', - cell: (props) => formatNumber(props.row.original.total_weight_kg), - footer: () => { - const totalWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_weight_kg - : 0; - - return totalWeightKg ? formatNumber(totalWeightKg) : '-'; - }, - }, - { - accessorKey: 'sales_price', - header: 'Harga Jual (Rp)', - cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), - footer: () => { - const totalSalesPrice = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.average_sales_price - : 0; - - return totalSalesPrice ? formatNumber(totalSalesPrice) : '-'; - }, - }, - { - accessorKey: 'hpp_price', - header: 'HPP (Rp)', - cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), - footer: () => { - const totalHppPricePerKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_hpp_price_per_kg - : 0; - - return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-'; - }, - }, - { - accessorKey: 'sales_amount', - header: 'Total (Rp)', - cell: (props) => formatCurrency(props.row.original.sales_amount), - footer: () => { - const totalSalesAmount = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_sales_amount - : 0; - - return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-'; - }, - }, - ]; - - useEffect(() => { - if (sorting.length === 1) { - onFilterByChange(sorting[0].id); - onSortByChange(sorting[0].desc ? 'desc' : 'asc'); - } else { - onFilterByChange(''); - onSortByChange(''); - } - }, [sorting]); - - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.length > 0 - : false - ); - } - }, [dailyMarketings, isResponseSuccess]); - - return ( - - -
Penjualan Harian
- - - - } - className='w-full!' - titleClassName='w-full p-0!' - > -
-
-
- -
-
- - - data={ - isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : [] - } - columns={dailyMarketingColumns} - pageSize={pageSize} - onPageSizeChange={onSetPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.meta?.total_results - : 0 - } - onPageChange={onSetPage} - isLoading={isLoadingDailyMarketings} - sorting={sorting} - setSorting={setSorting} - renderFooter={true} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(dailyMarketings) && - dailyMarketings?.data?.length === 0, - }), - }} - /> -
-
-
- ); -}; - -export default DailyMarketingsTable; diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index 8a814689..de449f9c 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -14,7 +14,7 @@ const MarketingReportContent = () => { { id: '1', label: 'Penjualan Harian', - content: , + content: , }, { id: '2', diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index cedf979f..5fcc630f 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -1,472 +1,933 @@ -'use client'; - -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useState, useMemo, useCallback } from 'react'; +import useSWR from 'swr'; +import { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { WarehouseApi } from '@/services/api/master-data'; +import { CustomerApi } from '@/services/api/master-data'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; +import { + DailyMarketingRow, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; import { pdf } from '@react-pdf/renderer'; import toast from 'react-hot-toast'; - import { Icon } from '@iconify/react'; -import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; -import DateInput from '@/components/input/DateInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; -import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; - -import { Area } from '@/types/api/master-data/area'; +import { useFormik } from 'formik'; import { - AreaApi, - CustomerApi, - LocationApi, - WarehouseApi, -} from '@/services/api/master-data'; -import { Warehouse } from '@/types/api/master-data/warehouse'; -import { Customer } from '@/types/api/master-data/customer'; -import { MarketingReportApi } from '@/services/api/report/marketing-report'; + DailyMarketingReportFilterSchema, + DailyMarketingReportFilterType, +} from '@/components/pages/report/marketing/filter/DailyMarketingFilter'; +import SelectInput from '@/components/input/SelectInput'; +import Modal, { useModal } from '@/components/Modal'; +import { cn } from '@/lib/helper'; +import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; +import { useEffect as useEffectHook } from 'react'; +import { httpClient } from '@/services/http/client'; +import { isResponseError } from '@/lib/api-helper'; import { MARKETING_DATE_FILTER_TYPE_OPTIONS, MARKETING_TYPE_OPTIONS, } from '@/config/constant'; -import { httpClient } from '@/services/http/client'; -import { BaseApiResponse } from '@/types/api/api-general'; -import { - DailyMarketingReport, - DailyMarketingReportResponse, -} from '@/types/api/report/marketing'; -import { isResponseError } from '@/lib/api-helper'; +import Badge from '@/components/Badge'; -const DailyMarketingReportContent = () => { - const { - state: tableFilterState, - updateFilter, - setPage, - setPageSize, - toQueryString: getTableFilterQueryString, - reset: resetFilter, - } = useTableFilter({ - initial: { - search: '', - area_id: '', - location_id: '', - warehouse_id: '', - customer_id: '', - start_date: '', - end_date: '', - marketing_type: '', - filter_by: '', - sort_by: '', +interface DailyMarketingTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + warehouse_id?: string; + customer_id?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + marketing_type?: string; + sort_by?: string; +} + +const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== SEARCH STATE ===== + const [searchValue, setSearchValue] = useState(''); + + // ===== FILTER STATE ===== + const [filterParams, setFilterParams] = useState({}); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { options: warehouseOptions, isLoadingOptions: isLoadingWarehouses } = + useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + + const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = + useSelect(CustomerApi.basePath, 'id', 'name', 'search'); + + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + search: null, + area_id: null, + location_id: null, + warehouse_id: null, + customer_id: null, + start_date: null, + end_date: null, + filter_by: null, + marketing_type: null, + sort_by: null, }, - paramMap: { - page: 'page', - pageSize: 'limit', - area_id: 'area_id', - location_id: 'location_id', - warehouse_id: 'warehouse_id', - customer_id: 'customer_id', - start_date: 'start_date', - end_date: 'end_date', - marketing_type: 'marketing_type', - filter_by: 'filter_by', - sort_by: 'sort_by', + validationSchema: DailyMarketingReportFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + area_id: values.area_id || undefined, + location_id: values.location_id || undefined, + warehouse_id: values.warehouse_id || undefined, + customer_id: values.customer_id || undefined, + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + filter_by: values.filter_by || undefined, + marketing_type: values.marketing_type || undefined, + sort_by: values.sort_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); }, }); - const dailyMarketingsReportUrl = `${MarketingReportApi.basePath}${getTableFilterQueryString()}`; - - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = - useState(false); - const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); - - const [selectedArea, setSelectedArea] = useState(null); - const { - setInputValue: setAreaInputValue, - options: areaOptions, - isLoadingOptions: isLoadingAreaOptions, - loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name'); - - const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedArea(val as OptionType); - updateFilter('area_id', val ? ((val as OptionType).value as string) : ''); - }; - - const [selectedLocation, setSelectedLocation] = useState( - null + // ===== SEARCH CHANGE HANDLER ===== + const searchChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, + [] ); - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name'); - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'location_id', - val ? ((val as OptionType).value as string) : '' + // ===== DERIVED VALUES ===== + 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 [selectedWarehouse, setSelectedWarehouse] = useState( - null + const locationValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const warehouseValue = useMemo(() => { + if (!formik.values.warehouse_id) return null; + return ( + warehouseOptions.find( + (opt) => String(opt.value) === formik.values.warehouse_id + ) || null + ); + }, [formik.values.warehouse_id, warehouseOptions]); + + const customerValue = useMemo(() => { + if (!formik.values.customer_id) return null; + return ( + customerOptions.find( + (opt) => String(opt.value) === formik.values.customer_id + ) || null + ); + }, [formik.values.customer_id, customerOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + MARKETING_DATE_FILTER_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.filter_by + ) || null + ); + }, [formik.values.filter_by]); + + const marketingTypeValue = useMemo(() => { + if (!formik.values.marketing_type) return null; + return ( + MARKETING_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.marketing_type + ) || null + ); + }, [formik.values.marketing_type]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.location_id) { + count += 1; + } + + if (filterParams.warehouse_id) { + count += 1; + } + + if (filterParams.customer_id) { + count += 1; + } + + if (filterParams.start_date || filterParams.end_date) { + count += 1; + } + + if (filterParams.filter_by) { + count += 1; + } + + if (filterParams.marketing_type) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: dailyMarketings, isLoading } = useSWR( + isSubmitted + ? () => { + const params = new URLSearchParams(); + + if (searchValue) params.set('search', searchValue); + if (filterParams.area_id) params.set('area_id', filterParams.area_id); + if (filterParams.location_id) + params.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + params.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + params.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + params.set('start_date', filterParams.start_date); + if (filterParams.end_date) + params.set('end_date', filterParams.end_date); + if (filterParams.filter_by) + params.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + params.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); + + return ['daily-marketing-report', params.toString()]; + } + : null, + ([, params]) => + MarketingReportApi.getAllDailyMarketingFetcher( + `${MarketingReportApi.basePath}?${params}` + ) ); - const { - setInputValue: setWarehouseInputValue, - options: warehouseOptions, - isLoadingOptions: isLoadingWarehouseOptions, - loadMore: loadMoreWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name'); - const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedWarehouse(val as OptionType); - updateFilter( - 'warehouse_id', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const [selectedCustomer, setSelectedCustomer] = useState( - null + const data: DailyMarketingRow[] = useMemo( + () => + isResponseSuccess(dailyMarketings) + ? (dailyMarketings?.data as DailyMarketingRow[]) || [] + : [], + [dailyMarketings] ); - const { - setInputValue: setCustomerInputValue, - options: customerOptions, - isLoadingOptions: isLoadingCustomerOptions, - loadMore: loadMoreCustomers, - } = useSelect(CustomerApi.basePath, 'id', 'name'); - const customerChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedCustomer(val as OptionType); - updateFilter( - 'customer_id', - val ? ((val as OptionType).value as string) : '' - ); - }; + const summaryTotal = useMemo( + () => + isResponseSuccess(dailyMarketings) && dailyMarketings?.total + ? dailyMarketings.total + : undefined, + [dailyMarketings] + ); - const startDateChangeHandler = (e: React.ChangeEvent) => { - updateFilter('start_date', e.target.value ? e.target.value : ''); - }; - - const endDateChangeHandler = (e: React.ChangeEvent) => { - updateFilter('end_date', e.target.value ? e.target.value : ''); - }; - - const [selectedMarketingDateFilterType, setSelectedMarketingDateFilterType] = - useState(null); - const marketingDateFilterTypeChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedMarketingDateFilterType(val as OptionType); - updateFilter('filter_by', val ? ((val as OptionType).value as string) : ''); - }; - - const [selectedMarketingType, setSelectedMarketingType] = - useState(null); - const marketingTypeChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedMarketingType(val as OptionType); - updateFilter( - 'marketing_type', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - - const filterByChangeHandler = (filterBy: string) => { - updateFilter('filter_by', filterBy); - }; - - const sortByChangeHandler = (sort: 'asc' | 'desc' | '') => { - updateFilter('sort_by', sort); - }; - - const exportToExcelHandler = async () => { - setIsLoadingExportingToExcel(true); - - await MarketingReportApi.exportDailyMarketingToExcel( - getTableFilterQueryString() - ); - - setIsLoadingExportingToExcel(false); - }; - - const exportToPdfHandler = async () => { - setIsLoadingExportingToPdf(true); - - const params = new URLSearchParams(getTableFilterQueryString()); + // ===== EXPORT DATA FETCHER ===== + const dailyMarketingsExport = useCallback(async (): Promise< + DailyMarketingRow[] | null + > => { + const params = new URLSearchParams(); + if (searchValue) params.set('search', searchValue); + if (filterParams.area_id) params.set('area_id', filterParams.area_id); + if (filterParams.location_id) + params.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + params.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + params.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + params.set('start_date', filterParams.start_date); + if (filterParams.end_date) params.set('end_date', filterParams.end_date); + if (filterParams.filter_by) params.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + params.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); params.set('limit', '9999999'); const queryString = `?${params.toString()}`; try { - const dailyMarketingsReport = - await httpClient( - `${MarketingReportApi.basePath}${queryString}` - ); + const response = await httpClient( + `${MarketingReportApi.basePath}${queryString}` + ); - if (isResponseError(dailyMarketingsReport)) { - toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + if (isResponseError(response)) { + return null; + } + + return response.data || []; + } catch { + return null; + } + }, [filterParams, searchValue]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const queryString = new URLSearchParams(); + + if (searchValue) queryString.set('search', searchValue); + if (filterParams.area_id) + queryString.set('area_id', filterParams.area_id); + if (filterParams.location_id) + queryString.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + queryString.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + queryString.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + queryString.set('start_date', filterParams.start_date); + if (filterParams.end_date) + queryString.set('end_date', filterParams.end_date); + if (filterParams.filter_by) + queryString.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + queryString.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) + queryString.set('sort_by', filterParams.sort_by); + + await MarketingReportApi.exportDailyMarketingToExcel( + `?${queryString.toString()}` + ); + + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [filterParams, searchValue]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await dailyMarketingsExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); return; } - const openPdf = async () => { - const dailyMarketingReportPdfBlob = await pdf( - - ).toBlob(); + const dailyMarketingReportPdfBlob = await pdf( + + ).toBlob(); - const dailyMarketingReportPdfUrl = URL.createObjectURL( - dailyMarketingReportPdfBlob - ); - window.open(dailyMarketingReportPdfUrl, '_blank'); - }; + const dailyMarketingReportPdfUrl = URL.createObjectURL( + dailyMarketingReportPdfBlob + ); + window.open(dailyMarketingReportPdfUrl, '_blank'); - const downloadPdf = async () => { - const blob = await pdf( - - ).toBlob(); - const url = URL.createObjectURL(blob); - - const link = document.createElement('a'); - link.href = url; - link.download = 'laporan-penjualan-harian.pdf'; - link.click(); - - URL.revokeObjectURL(url); - }; - - await openPdf(); - } catch (error) { - toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + toast.success('PDF berhasil dibuat.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); } + }, [dailyMarketingsExport, summaryTotal]); - setIsLoadingExportingToPdf(false); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useMarketingTabStore((state) => state.setTabActions); + const clearTabActions = useMarketingTabStore( + (state) => state.clearTabActions + ); + + useEffectHook(() => { + setTabActions( + tabId, +
+ + } + className={{ + wrapper: 'w-full min-w-48 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + + + + +
+ + + Export + +
+ + +
+ + } + > + + + +
+ ); + }, [ + tabId, + searchValue, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + const getTableColumns = (): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
TOTAL
, + }, + { + id: 'so_date', + header: 'Tanggal Jual', + accessorKey: 'so_date', + cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), + footer: () =>
ALL
, + }, + { + id: 'realization_date', + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: (props) => + formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), + footer: () =>
-
, + }, + { + id: 'aging_days', + header: 'Aging', + accessorKey: 'aging_days', + cell: (props) => `${props.row.original.aging_days} hari`, + footer: () =>
-
, + }, + { + id: 'warehouse', + header: 'Gudang', + accessorKey: 'warehouse', + cell: ({ row }) => row.original.warehouse.name, + footer: () =>
-
, + }, + { + id: 'customer', + header: 'Pelanggan', + accessorKey: 'customer', + cell: ({ row }) => row.original.customer.name, + footer: () =>
-
, + }, + { + id: 'do_number', + header: 'No. DO', + accessorKey: 'do_number', + footer: () =>
-
, + }, + { + id: 'sales_person', + header: 'Sales/Marketing', + accessorKey: 'sales', + cell: (props) => props.row.original.sales.name, + footer: () =>
-
, + }, + { + id: 'vehicle_number', + header: 'No. Polisi', + accessorKey: 'vehicle_number', + cell: (props) => ( + + {formatVechicleNumber(props.row.original.vehicle_number)} + + ), + footer: () =>
-
, + }, + { + id: 'marketing_type', + header: 'Marketing Type', + accessorKey: 'marketing_type', + footer: () =>
-
, + }, + { + id: 'product', + header: 'Produk', + accessorKey: 'product', + cell: ({ row }) => row.original.product.name, + footer: () =>
-
, + }, + { + id: 'qty', + header: 'Kuantitas', + accessorKey: 'qty', + cell: (props) => formatNumber(props.row.original.qty), + footer: () => ( +
+ {summaryTotal?.total_qty + ? formatNumber(summaryTotal.total_qty) + : '-'} +
+ ), + }, + { + id: 'average_weight', + header: 'Bobot Rata-Rata (Kg)', + accessorKey: 'average_weight_kg', + cell: (props) => formatNumber(props.row.original.average_weight_kg), + footer: () => ( +
+ {summaryTotal?.average_weight_kg + ? formatNumber(summaryTotal.average_weight_kg) + : '-'} +
+ ), + }, + { + id: 'total_weight', + header: 'Bobot Total (Kg)', + accessorKey: 'total_weight_kg', + cell: (props) => formatNumber(props.row.original.total_weight_kg), + footer: () => ( +
+ {summaryTotal?.total_weight_kg + ? formatNumber(summaryTotal.total_weight_kg) + : '-'} +
+ ), + }, + { + id: 'sales_price', + header: 'Harga Jual (Rp)', + accessorKey: 'sales_price_per_kg', + cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), + footer: () => ( +
+ {summaryTotal?.average_sales_price + ? formatNumber(summaryTotal.average_sales_price) + : '-'} +
+ ), + }, + { + id: 'hpp_price', + header: 'HPP (Rp)', + accessorKey: 'hpp_price_per_kg', + cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + footer: () => ( +
+ {summaryTotal?.total_hpp_price_per_kg + ? formatCurrency(summaryTotal.total_hpp_price_per_kg) + : '-'} +
+ ), + }, + { + id: 'sales_amount', + header: 'Total (Rp)', + accessorKey: 'sales_amount', + cell: (props) => formatCurrency(props.row.original.sales_amount), + footer: () => ( +
+ {summaryTotal?.total_sales_amount + ? formatCurrency(summaryTotal.total_sales_amount) + : '-'} +
+ ), + }, + ]; + return tableColumns; }; - const handleReset = () => { - setSelectedArea(null); - setSelectedLocation(null); - setSelectedWarehouse(null); - setSelectedCustomer(null); - setSelectedMarketingType(null); - resetFilter(); - }; - - useEffect(() => { - if ( - tableFilterState.filter_by === 'realization_date' || - tableFilterState.filter_by === 'so_date' - ) { - setSelectedMarketingDateFilterType({ - label: - tableFilterState.filter_by === 'realization_date' - ? 'Tanggal Realisasi' - : 'Tanggal SO', - value: tableFilterState.filter_by, - }); - } else { - setSelectedMarketingDateFilterType(null); - } - }, [tableFilterState.filter_by]); - return ( -
-
-

Penjualan Harian

+ <> +
+ {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( + + } + title='Memuat Data Penjualan Harian' + subtitle='Silakan tunggu sebentar...' + /> + ) : data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( +
0} + className={{ + containerClassName: cn('p-3', { + 'w-full mb-20': data.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )} - {/* Filters */} -
-
- - - - - - - - - - - -
- -
- - - - -
- - - - - - Export{' '} - - - } - > - - - - - + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
-
+
+
+ {/* Area Filter */} + { + formik.setFieldValue( + 'area_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> - -
+ {/* Location Filter */} + { + formik.setFieldValue( + 'location_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Warehouse Filter */} + { + formik.setFieldValue( + 'warehouse_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Customer Filter */} + { + formik.setFieldValue( + 'customer_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Date Range Filter */} +
+ +
+ { + formik.setFieldValue('start_date', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isError={ + !!formik.errors.start_date && formik.touched.start_date + } + /> + {formik.errors.start_date && formik.touched.start_date && ( +
+ {formik.errors.start_date} +
+ )} + { + formik.setFieldValue('end_date', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isError={!!formik.errors.end_date && formik.touched.end_date} + /> + {formik.errors.end_date && formik.touched.end_date && ( +
+ {formik.errors.end_date} +
+ )} +
+
+ + {/* Filter By Date Type */} + { + formik.setFieldValue( + 'filter_by', + val && !Array.isArray(val) ? (val.value as string) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Marketing Type Filter */} + { + formik.setFieldValue( + 'marketing_type', + val && !Array.isArray(val) ? (val.value as string) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ + + ); }; -export default DailyMarketingReportContent; +export default DailyMarketingTab; From 510573e66f16c1f2dd3459e5343755cbeecb962b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:55:31 +0700 Subject: [PATCH 18/82] refactor(FE): Add Excel export functionality for daily marketing report --- .../export/DailyMarketingExportXLSX.tsx | 118 ++++++++++++++++++ .../marketing/tab/DailyMarketingTab.tsx | 49 ++++---- 2 files changed, 141 insertions(+), 26 deletions(-) diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx index e69de29b..8c368fbf 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx @@ -0,0 +1,118 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; +import { DailyMarketingRow, SalesSummary } from '@/types/api/report/marketing'; + +interface DailyMarketingExportExcelParams { + data: DailyMarketingRow[]; + summaryTotal?: SalesSummary; + period?: string; +} + +export const generateDailyMarketingExcel = async ( + params: DailyMarketingExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== DAILY MARKETING WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Tanggal Jual', key: 'soDate', width: 15 }, + { header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 }, + { header: 'Aging', key: 'aging', width: 10 }, + { header: 'Gudang', key: 'warehouse', width: 25 }, + { header: 'Pelanggan', key: 'customer', width: 25 }, + { header: 'No. DO', key: 'doNumber', width: 15 }, + { header: 'Sales/Marketing', key: 'sales', width: 20 }, + { header: 'No. Polisi', key: 'vehicleNumber', width: 15 }, + { header: 'Marketing Type', key: 'marketingType', width: 15 }, + { header: 'Produk', key: 'product', width: 20 }, + { header: 'Kuantitas', key: 'qty', width: 12 }, + { header: 'Bobot Rata-Rata (Kg)', key: 'averageWeight', width: 20 }, + { header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 }, + { header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 }, + { header: 'HPP (Rp)', key: 'hppPrice', width: 15 }, + { header: 'Total (Rp)', key: 'salesAmount', width: 20 }, + ]; + + const worksheet = workbook.addWorksheet('Laporan Marketing Harian'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: DailyMarketingRow, index: number) => { + worksheet.addRow({ + no: index + 1, + soDate: formatDate(item.so_date, 'DD-MMM-YYYY'), + realizationDate: formatDate(item.realization_date, 'DD-MMM-YYYY'), + aging: `${item.aging_days} hari`, + warehouse: item.warehouse?.name || '', + customer: item.customer?.name || '', + doNumber: item.do_number || '', + sales: item.sales?.name || '', + vehicleNumber: formatVechicleNumber(item.vehicle_number), + marketingType: item.marketing_type || '', + product: item.product?.name || '', + qty: formatNumber(item.qty || 0), + averageWeight: formatNumber(item.average_weight_kg || 0), + totalWeight: formatNumber(item.total_weight_kg || 0), + salesPrice: formatCurrency(item.sales_price_per_kg || 0), + hppPrice: formatCurrency(item.hpp_price_per_kg || 0), + salesAmount: formatCurrency(item.sales_amount || 0), + }); + }); + + // Add TOTAL row if summary data is available + if (params.summaryTotal) { + worksheet.addRow({ + no: 'TOTAL', + soDate: 'ALL', + realizationDate: '-', + aging: '-', + warehouse: '-', + customer: '-', + doNumber: '-', + sales: '-', + vehicleNumber: '-', + marketingType: '-', + product: '-', + qty: formatNumber(params.summaryTotal.total_qty || 0), + averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0), + totalWeight: formatNumber(params.summaryTotal.total_weight_kg || 0), + salesPrice: formatNumber(params.summaryTotal.average_sales_price || 0), + hppPrice: formatCurrency(params.summaryTotal.total_hpp_price_per_kg || 0), + salesAmount: formatCurrency(params.summaryTotal.total_sales_amount || 0), + }); + } + + worksheet.columns.forEach((column) => { + if (column.width && column.width < 10) { + column.width = 10; + } + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-marketing-harian-${params.period}.xlsx` + : `laporan-marketing-harian-${currentDate}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 5fcc630f..a687fdf5 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -15,6 +15,7 @@ import { formatNumber, formatDate, formatVechicleNumber, + formatTitleCase, } from '@/lib/helper'; import { DailyMarketingRow, @@ -23,9 +24,8 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; +import { generateDailyMarketingExcel } from '@/components/pages/report/marketing/export/DailyMarketingExportXLSX'; import { pdf } from '@react-pdf/renderer'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -336,31 +336,23 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { - const queryString = new URLSearchParams(); + const allDataForExport = await dailyMarketingsExport(); - if (searchValue) queryString.set('search', searchValue); - if (filterParams.area_id) - queryString.set('area_id', filterParams.area_id); - if (filterParams.location_id) - queryString.set('location_id', filterParams.location_id); - if (filterParams.warehouse_id) - queryString.set('warehouse_id', filterParams.warehouse_id); - if (filterParams.customer_id) - queryString.set('customer_id', filterParams.customer_id); - if (filterParams.start_date) - queryString.set('start_date', filterParams.start_date); - if (filterParams.end_date) - queryString.set('end_date', filterParams.end_date); - if (filterParams.filter_by) - queryString.set('filter_by', filterParams.filter_by); - if (filterParams.marketing_type) - queryString.set('marketing_type', filterParams.marketing_type); - if (filterParams.sort_by) - queryString.set('sort_by', filterParams.sort_by); + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } - await MarketingReportApi.exportDailyMarketingToExcel( - `?${queryString.toString()}` - ); + const period = + filterParams.start_date && filterParams.end_date + ? `${formatDate(filterParams.start_date, 'DD-MMM-YYYY')}_to_${formatDate(filterParams.end_date, 'DD-MMM-YYYY')}` + : undefined; + + await generateDailyMarketingExcel({ + data: allDataForExport, + summaryTotal: summaryTotal, + period: period, + }); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -368,7 +360,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams, searchValue]); + }, [filterParams, dailyMarketingsExport, summaryTotal]); const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); @@ -588,6 +580,11 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { id: 'marketing_type', header: 'Marketing Type', accessorKey: 'marketing_type', + cell: (props) => ( + + {formatTitleCase(props.row.original.marketing_type || '-')} + + ), footer: () =>
-
, }, { From b154b478bcb477ee8b77e4ec9ea86115d7dd2798 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 14:15:56 +0700 Subject: [PATCH 19/82] refactor(FE): Refactor production result components structure --- src/app/report/production-result/page.tsx | 2 +- .../ProductionResultExportPDF.tsx} | 0 .../production-result/export/ProductionResultExportXLSX.tsx | 0 .../report/production-result/filter/ProductionResultFilter.ts | 0 .../production-result/skeleton/ProductionResultSkeleton.tsx | 0 .../ProductionResultTab.tsx} | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename src/components/pages/report/production-result/{ProductionResultReportPDF.tsx => export/ProductionResultExportPDF.tsx} (100%) create mode 100644 src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx create mode 100644 src/components/pages/report/production-result/filter/ProductionResultFilter.ts create mode 100644 src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx rename src/components/pages/report/production-result/{ProductionResultContent.tsx => tab/ProductionResultTab.tsx} (99%) diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index cdac598c..fb8f2a0c 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -1,4 +1,4 @@ -import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent'; +import ProductionResultContent from '@/components/pages/report/production-result/tab/ProductionResultTab'; const ProductionResultReportPage = () => { return ( diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx similarity index 100% rename from src/components/pages/report/production-result/ProductionResultReportPDF.tsx rename to src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx diff --git a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx similarity index 99% rename from src/components/pages/report/production-result/ProductionResultContent.tsx rename to src/components/pages/report/production-result/tab/ProductionResultTab.tsx index d79d4c94..84b21c41 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -31,7 +31,7 @@ import { ProductionResultReportApi } from '@/services/api/report/production-resu import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient } from '@/services/http/client'; import { ProductionResult } from '@/types/api/report/production-result'; -import ProductionResultReportPDF from './ProductionResultReportPDF'; +import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; const ProductionResultContent = () => { From dc4e945a35946accb5e3115640870962287ec91d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 14:50:19 +0700 Subject: [PATCH 20/82] refactor(FE): Refactor table column definitions to remove unused row parameter --- .../export/ProductionResultExportPDF.tsx | 14 ++-- .../filter/ProductionResultFilter.ts | 50 ++++++++++++ .../skeleton/ProductionResultSkeleton.tsx | 38 +++++++++ .../tab/ProductionResultTab.tsx | 79 +++++++++++++++++-- 4 files changed, 169 insertions(+), 12 deletions(-) diff --git a/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx index eabb03bf..76336569 100644 --- a/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx +++ b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx @@ -66,7 +66,7 @@ const getBwTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'woa', @@ -114,7 +114,7 @@ const getDepTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'dep_kum', @@ -141,7 +141,7 @@ const getButiranTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'butiran_utuh', @@ -196,7 +196,7 @@ const getKgTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'kg_utuh', @@ -251,7 +251,7 @@ const getPersenTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'persen_utuh', @@ -292,7 +292,7 @@ const getProduksi1TableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'hd', @@ -361,7 +361,7 @@ const getProduksi2TableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'fcr', diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index e69de29b..1c1979eb 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -0,0 +1,50 @@ +import * as yup from 'yup'; + +export type ProductionResultFilterType = { + area_id: string | null; + location_id: string | null; + project_flock_id: string | null; + kandang_id: string | null; + date_start: string | null; + date_end: string | null; + sort_by: string | null; + show_unrecorded: boolean | null; +}; + +export const ProductionResultFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + project_flock_id: yup.string().nullable(), + kandang_id: yup.string().nullable(), + date_start: yup + .string() + .nullable() + .test( + 'is-valid-date', + 'Tanggal mulai tidak valid', + (value) => !value || !isNaN(Date.parse(value)) + ), + date_end: yup + .string() + .nullable() + .test( + 'is-valid-date', + 'Tanggal akhir tidak valid', + (value) => !value || !isNaN(Date.parse(value)) + ) + .test( + 'is-after-start', + 'Tanggal akhir tidak boleh lebih awal dari tanggal mulai', + function (value) { + const { date_start } = this.parent; + if (!date_start || !value) return true; + return new Date(value) >= new Date(date_start); + } + ), + sort_by: yup.string().nullable(), + show_unrecorded: yup.boolean().nullable(), +}); + +export type ProductionResultFilterValues = yup.InferType< + typeof ProductionResultFilterSchema +>; diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx index e69de29b..c8cd63d8 100644 --- a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx +++ b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ProductionResult } from '@/types/api/report/production-result'; +import { ColumnDef } from '@tanstack/react-table'; + +const ProductionResultSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ProductionResultSkeleton; diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index 84b21c41..ac5e76fd 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState } from 'react'; +import React, { useState } from 'react'; +import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -161,11 +162,79 @@ const ProductionResultContent = () => { const exportToExcelHandler = async () => { setIsLoadingExportingToExcel(true); - await ProductionResultReportApi.exportProductionResultToExcel( - projectFlockKandangs - ); + try { + let projectFlockKandangsData: BaseProjectFlockKandang[] = []; - setIsLoadingExportingToExcel(false); + if (selectedProjectFlockKandang) { + const projectFlockKandangResponse = + await ProjectFlockKandangApi.getSingle( + selectedProjectFlockKandang?.value as number + ); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangResponse + ) + ? [projectFlockKandangResponse.data] + : []; + } else { + const projectFlockKandangsResponse = + await ProjectFlockKandangApi.getAll({ + area_id: selectedArea?.value, + project_flock_id: selectedProjectFlock?.value, + }); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsResponse + ) + ? projectFlockKandangsResponse.data + : []; + } + + const productionResultData: ProductionResult[] = []; + + for (const kandang of projectFlockKandangsData) { + const getProductionResultPath = `${ProductionResultReportApi.basePath}/${kandang.id}?page=1&limit=100`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + if (isResponseSuccess(getProductionResultRes)) { + productionResultData.push( + ...(getProductionResultRes.data?.map((result) => ({ + ...result, + project_flock: { + ...result.project_flock, + name: + selectedProjectFlock?.label || + kandang.project_flock?.name || + `Project Flock #${kandang.project_flock_id}`, + category: kandang.project_flock?.category || '', + kandang: { + ...result.project_flock?.kandang, + name: + kandang.kandang?.name || `Kandang #${kandang.kandang_id}`, + }, + }, + })) || []) + ); + } + } + + if (productionResultData.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + setIsLoadingExportingToExcel(false); + return; + } + + await generateProductionResultExcel({ + data: productionResultData, + period: '', + }); + } catch { + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } finally { + setIsLoadingExportingToExcel(false); + } }; const exportToPdfHandler = async () => { From 6595ff7a6ec7436bce4f829f7cfed37715fee4b2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:03:24 +0700 Subject: [PATCH 21/82] refactor(FE): Add function to export production results to Excel --- .../export/ProductionResultExportXLSX.tsx | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx index e69de29b..af0380c0 100644 --- a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx +++ b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx @@ -0,0 +1,135 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatNumber } from '@/lib/helper'; +import { ProductionResult } from '@/types/api/report/production-result'; + +interface ProductionResultExportExcelParams { + data: ProductionResult[]; + period?: string; +} + +export const generateProductionResultExcel = async ( + params: ProductionResultExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== PRODUCTION RESULT WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 6 }, + { header: 'Project Flock', key: 'projectFlockName', width: 25 }, + { + header: 'Category', + key: 'projectFlockCategory', + width: 18, + }, + { header: 'Kandang', key: 'kandangName', width: 18 }, + { header: 'Week of Age (WOA)', key: 'woa', width: 20 }, + { header: 'Body Weight (BW)', key: 'bw', width: 18 }, + { header: 'Body Weight (Std BW)', key: 'stdBw', width: 22 }, + { header: 'Uniformity (%)', key: 'uniformity', width: 16 }, + { header: 'Uniformity Std (%)', key: 'stdUniformity', width: 20 }, + { header: 'Depletion Cumulative', key: 'depKum', width: 22 }, + { header: 'Depletion Standard', key: 'depStd', width: 20 }, + { header: 'Telur Utuh', key: 'butiranUtuh', width: 14 }, + { header: 'Telur Putih', key: 'butiranPutih', width: 14 }, + { header: 'Telur Retak', key: 'butiranRetak', width: 14 }, + { header: 'Telur Pecah', key: 'butiranPecah', width: 14 }, + { header: 'Jumlah Telur', key: 'butiranJumlah', width: 16 }, + { header: 'Total Telur', key: 'totalButir', width: 14 }, + { header: 'Utuh (Kg)', key: 'kgUtuh', width: 12 }, + { header: 'Putih (Kg)', key: 'kgPutih', width: 12 }, + { header: 'Retak (Kg)', key: 'kgRetak', width: 12 }, + { header: 'Pecah (Kg)', key: 'kgPecah', width: 12 }, + { header: 'Jumlah (Kg)', key: 'kgJumlah', width: 14 }, + { header: 'Total Weight (Kg)', key: 'totalKg', width: 20 }, + { header: 'Utuh (%)', key: 'persenUtuh', width: 12 }, + { header: 'Putih (%)', key: 'persenPutih', width: 12 }, + { header: 'Retak (%)', key: 'persenRetak', width: 12 }, + { header: 'Pecah (%)', key: 'persenPecah', width: 12 }, + { header: 'Hen Day (HD)', key: 'hd', width: 15 }, + { header: 'Hen Day Std (HD Std)', key: 'hdStd', width: 22 }, + { header: 'Feed Intake (FI)', key: 'fi', width: 18 }, + { header: 'Feed Intake Std (FI Std)', key: 'fiStd', width: 25 }, + { header: 'Egg Mass (EM)', key: 'em', width: 16 }, + { header: 'Egg Mass Std (EM Std)', key: 'emStd', width: 23 }, + { header: 'Egg Weight (EW)', key: 'ew', width: 18 }, + { header: 'Egg Weight Std (EW Std)', key: 'ewStd', width: 25 }, + { header: 'Feed Conversion Ratio (FCR)', key: 'fcr', width: 30 }, + { + header: 'Feed Conversion Ratio Std (FCR Std)', + key: 'fcrStd', + width: 35, + }, + { header: 'Hen House (HH)', key: 'hh', width: 18 }, + { header: 'Hen House Std (HH Std)', key: 'hhStd', width: 25 }, + ]; + + const worksheet = workbook.addWorksheet('Production Result'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: ProductionResult, index: number) => { + worksheet.addRow({ + no: index + 1, + projectFlockName: item.project_flock?.name || '', + projectFlockCategory: item.project_flock?.category || '', + kandangName: item.project_flock?.kandang?.name || '', + woa: formatNumber(item.woa || 0), + bw: formatNumber(item.bw || 0), + stdBw: formatNumber(item.std_bw || 0), + uniformity: formatNumber(item.uniformity || 0), + stdUniformity: item.std_uniformity || '', + depKum: formatNumber(item.dep_kum || 0), + depStd: formatNumber(item.dep_std || 0), + butiranUtuh: formatNumber(item.butiran_utuh || 0), + butiranPutih: formatNumber(item.butiran_putih || 0), + butiranRetak: formatNumber(item.butiran_retak || 0), + butiranPecah: formatNumber(item.butiran_pecah || 0), + butiranJumlah: formatNumber(item.butiran_jumlah || 0), + totalButir: formatNumber(item.total_butir || 0), + kgUtuh: formatNumber(item.kg_utuh || 0), + kgPutih: formatNumber(item.kg_putih || 0), + kgRetak: formatNumber(item.kg_retak || 0), + kgPecah: formatNumber(item.kg_pecah || 0), + kgJumlah: formatNumber(item.kg_jumlah || 0), + totalKg: formatNumber(item.total_kg || 0), + persenUtuh: formatNumber(item.persen_utuh || 0), + persenPutih: formatNumber(item.persen_putih || 0), + persenRetak: formatNumber(item.persen_retak || 0), + persenPecah: formatNumber(item.persen_pecah || 0), + hd: formatNumber(item.hd || 0), + hdStd: formatNumber(item.hd_std || 0), + fi: formatNumber(item.fi || 0), + fiStd: formatNumber(item.fi_std || 0), + em: formatNumber(item.em || 0), + emStd: formatNumber(item.em_std || 0), + ew: formatNumber(item.ew || 0), + ewStd: formatNumber(item.ew_std || 0), + fcr: formatNumber(item.fcr || 0), + fcrStd: formatNumber(item.fcr_std || 0), + hh: formatNumber(item.hh || 0), + hhStd: formatNumber(item.hh_std || 0), + }); + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-hasil-produksi-${params.period}.xlsx` + : `laporan-hasil-produksi-${currentDate}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; From 5d92e6774ec4d9a9396fe89f1193b926dc7aec94 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:14:00 +0700 Subject: [PATCH 22/82] feat(FE): Add stock field to StockLog type --- src/types/api/inventory/product.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index f75e4060..134b982a 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -38,6 +38,7 @@ export type StockLog = { id: number; increase: number; decrease: number; + stock: number; loggable_type: string; loggable_id: number; notes: string; From 45ac8348fe84d949d63c4d0976a0a7a5d2cac7d1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:23:17 +0700 Subject: [PATCH 23/82] feat(FE): Add "Stock Akhir" column to StockLogTable --- .../pages/inventory/product/detail/StockLogTable.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx index 96d3dda6..0a305659 100644 --- a/src/components/pages/inventory/product/detail/StockLogTable.tsx +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -35,6 +35,13 @@ const StockLogTable = ({ header: 'Gudang', accessorKey: 'warehouse_name', }, + { + header: 'Stock Akhir', + accessorKey: 'stock', + cell: (props) => { + return formatNumber(props.row.original.stock); + }, + }, { header: 'Peningkatan', accessorKey: 'increase', From cb171118ee2eab5d7d873994d4a9716758564337 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:32:11 +0700 Subject: [PATCH 24/82] refactor(FE): Restrict row selection to specific approval criteria --- .../project-flock/ProjectFlockTable.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 4085bc56..5521fdbf 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -384,7 +384,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { id: 'select', header: ({ table }) => { const allRows = table.getRowModel().rows; - const selectableRows = allRows; + const selectableRows = allRows.filter((row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }); const allSelected = selectableRows.every((row) => row.getIsSelected()) && @@ -398,6 +404,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); }; + const hasNoSelectableRows = selectableRows.length === 0; + return (
void }) => { checked={allSelected} indeterminate={someSelected} onChange={toggleSelectableRows} + disabled={hasNoSelectableRows} />
); @@ -845,6 +854,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setSorting={setSorting} rowSelection={rowSelection} setRowSelection={setRowSelection} + enableRowSelection={(row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }} withCheckbox className={{ containerClassName: cn('p-3', { From 6aae18df54524d3aee0933836dc818feb7926c7c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:49:26 +0700 Subject: [PATCH 25/82] refactor(FE): Update import paths for finance and marketing tab stores --- .../pages/production/project-flock/ProjectFlockTable.tsx | 2 +- .../pages/production/project-flock/form/ProjectFlockForm.tsx | 2 +- src/components/pages/production/uniformity/UniformityTable.tsx | 2 +- .../pages/production/uniformity/form/UniformityForm.tsx | 2 +- .../pages/production/uniformity/form/UniformityPreviewForm.tsx | 2 +- .../pages/production/uniformity/form/UniformityResultForm.tsx | 2 +- src/components/pages/report/finance/FinanceTabs.tsx | 2 +- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 2 +- src/components/pages/report/finance/tab/DebtSupplierTab.tsx | 2 +- .../pages/report/logistic-stock/LogisticStockTabs.tsx | 2 +- .../pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 2 +- src/components/pages/report/marketing/MarketingTabs.tsx | 2 +- src/components/pages/report/marketing/tab/DailyMarketingTab.tsx | 2 +- src/components/pages/report/marketing/tab/HppPerKandangTab.tsx | 2 +- .../{ => production}/project-flock/project-flock.store.ts | 2 +- .../project-flock/slices/project-flock.slice.ts | 0 .../{ => production}/uniformity/slices/uniformity.slice.ts | 0 src/stores/{ => production}/uniformity/uniformity.store.ts | 2 +- src/stores/{ => report}/finance-tab/finance-tab.store.ts | 0 .../{ => report}/logistic-stock-tab/logistic-stock-tab.store.ts | 0 src/stores/{ => report}/marketing-tab/marketing-tab.store.ts | 0 21 files changed, 16 insertions(+), 16 deletions(-) rename src/stores/{ => production}/project-flock/project-flock.store.ts (79%) rename src/stores/{ => production}/project-flock/slices/project-flock.slice.ts (100%) rename src/stores/{ => production}/uniformity/slices/uniformity.slice.ts (100%) rename src/stores/{ => production}/uniformity/uniformity.store.ts (80%) rename src/stores/{ => report}/finance-tab/finance-tab.store.ts (100%) rename src/stores/{ => report}/logistic-stock-tab/logistic-stock-tab.store.ts (100%) rename src/stores/{ => report}/marketing-tab/marketing-tab.store.ts (100%) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 5521fdbf..e8280fa8 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -34,7 +34,7 @@ import StatusBadge from '@/components/helper/StatusBadge'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; -import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store'; +import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; const RowOptionsMenu = ({ diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index d56550a6..57cde405 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -52,7 +52,7 @@ import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import StatusBadge from '@/components/helper/StatusBadge'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; -import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store'; +import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; interface ProjectFlockFormProps { formType?: 'add' | 'edit' | 'detail'; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 39112b47..3473967e 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -26,7 +26,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import UniformityTableSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import FloatingActionsButton from '@/components/FloatingActionsButton'; import Modal from '@/components/Modal'; import SelectInput, { diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 8ab62d85..33b649c4 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; diff --git a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx index 3cc120fd..3ca24952 100644 --- a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx @@ -7,7 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { diff --git a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx index eaf51103..108cb4f8 100644 --- a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -7,7 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { useRouter } from 'next/navigation'; diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index ffb0d3f1..84d610a5 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 3680a41c..03b41e10 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -36,7 +36,7 @@ import { } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 5e7781bf..bd71f02a 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -33,7 +33,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index f06e63dc..6996db55 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; -import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; +import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; const LogisticStockTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 23fb067e..f9251f4a 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -32,7 +32,7 @@ import { } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; +import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index de449f9c..d07c7d1b 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; -import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; const MarketingReportContent = () => { const [activeTabId, setActiveTabId] = useState('1'); diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index a687fdf5..9c03de7a 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -37,7 +37,7 @@ import { import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; import { httpClient } from '@/services/http/client'; diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 514edcb9..0650c19a 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -33,7 +33,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; diff --git a/src/stores/project-flock/project-flock.store.ts b/src/stores/production/project-flock/project-flock.store.ts similarity index 79% rename from src/stores/project-flock/project-flock.store.ts rename to src/stores/production/project-flock/project-flock.store.ts index 61efcb97..97402132 100644 --- a/src/stores/project-flock/project-flock.store.ts +++ b/src/stores/production/project-flock/project-flock.store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createProjectFlockSlice } from '@/stores/project-flock/slices/project-flock.slice'; +import { createProjectFlockSlice } from '@/stores/production/project-flock/slices/project-flock.slice'; import { ProjectFlockSlice } from '@/types/stores'; export type ProjectFlockStore = ProjectFlockSlice; diff --git a/src/stores/project-flock/slices/project-flock.slice.ts b/src/stores/production/project-flock/slices/project-flock.slice.ts similarity index 100% rename from src/stores/project-flock/slices/project-flock.slice.ts rename to src/stores/production/project-flock/slices/project-flock.slice.ts diff --git a/src/stores/uniformity/slices/uniformity.slice.ts b/src/stores/production/uniformity/slices/uniformity.slice.ts similarity index 100% rename from src/stores/uniformity/slices/uniformity.slice.ts rename to src/stores/production/uniformity/slices/uniformity.slice.ts diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/production/uniformity/uniformity.store.ts similarity index 80% rename from src/stores/uniformity/uniformity.store.ts rename to src/stores/production/uniformity/uniformity.store.ts index c8d759d6..740d10b6 100644 --- a/src/stores/uniformity/uniformity.store.ts +++ b/src/stores/production/uniformity/uniformity.store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createUniformitySlice } from '@/stores/uniformity/slices/uniformity.slice'; +import { createUniformitySlice } from '@/stores/production/uniformity/slices/uniformity.slice'; import { UniformitySlice } from '@/types/stores'; export type UniformityStore = UniformitySlice; diff --git a/src/stores/finance-tab/finance-tab.store.ts b/src/stores/report/finance-tab/finance-tab.store.ts similarity index 100% rename from src/stores/finance-tab/finance-tab.store.ts rename to src/stores/report/finance-tab/finance-tab.store.ts diff --git a/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts b/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts similarity index 100% rename from src/stores/logistic-stock-tab/logistic-stock-tab.store.ts rename to src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts diff --git a/src/stores/marketing-tab/marketing-tab.store.ts b/src/stores/report/marketing-tab/marketing-tab.store.ts similarity index 100% rename from src/stores/marketing-tab/marketing-tab.store.ts rename to src/stores/report/marketing-tab/marketing-tab.store.ts From dbb523c7106560d57f86658a70184f4d0608a719 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 16:16:40 +0700 Subject: [PATCH 26/82] refactor(FE): Refactor tab store to use a unified ReportTabStore --- .../pages/report/finance/FinanceTabs.tsx | 4 +- .../report/finance/tab/CustomerPaymentTab.tsx | 6 +-- .../report/finance/tab/DebtSupplierTab.tsx | 6 +-- .../logistic-stock/LogisticStockTabs.tsx | 4 +- .../tab/PurchasesPerSupplierTab.tsx | 10 ++-- .../pages/report/marketing/MarketingTabs.tsx | 4 +- .../marketing/tab/DailyMarketingTab.tsx | 8 ++- .../report/marketing/tab/HppPerKandangTab.tsx | 8 ++- .../report/finance-tab/finance-tab.store.ts | 51 ------------------- .../logistic-stock-tab.store.ts | 51 ------------------- .../marketing-tab/marketing-tab.store.ts | 51 ------------------- src/stores/report/report-tab.store.ts | 18 +++++++ src/stores/report/slices/report-tab.slice.ts | 37 ++++++++++++++ 13 files changed, 76 insertions(+), 182 deletions(-) delete mode 100644 src/stores/report/finance-tab/finance-tab.store.ts delete mode 100644 src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts delete mode 100644 src/stores/report/marketing-tab/marketing-tab.store.ts create mode 100644 src/stores/report/report-tab.store.ts create mode 100644 src/stores/report/slices/report-tab.slice.ts diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index 84d610a5..de924f62 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useFinanceTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 03b41e10..1443fa0b 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -36,7 +36,7 @@ import { } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; @@ -375,8 +375,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useFinanceTabStore((state) => state.setTabActions); - const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index bd71f02a..44677313 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -33,7 +33,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; @@ -271,8 +271,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, [debtSupplierExport]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useFinanceTabStore((state) => state.setTabActions); - const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 6996db55..1e3f4109 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -3,11 +3,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; -import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const LogisticStockTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useLogisticStockTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index f9251f4a..5e2c4e27 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -32,7 +32,7 @@ import { } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { @@ -486,12 +486,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useLogisticStockTabStore( - (state) => state.setTabActions - ); - const clearTabActions = useLogisticStockTabStore( - (state) => state.clearTabActions - ); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index d07c7d1b..8a02a0c2 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; -import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const MarketingReportContent = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useMarketingTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 9c03de7a..df06c83c 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -37,7 +37,7 @@ import { import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; import { httpClient } from '@/services/http/client'; @@ -390,10 +390,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }, [dailyMarketingsExport, summaryTotal]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useMarketingTabStore((state) => state.setTabActions); - const clearTabActions = useMarketingTabStore( - (state) => state.clearTabActions - ); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 0650c19a..d3d4bfac 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -33,7 +33,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; @@ -487,10 +487,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useMarketingTabStore((state) => state.setTabActions); - const clearTabActions = useMarketingTabStore( - (state) => state.clearTabActions - ); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/stores/report/finance-tab/finance-tab.store.ts b/src/stores/report/finance-tab/finance-tab.store.ts deleted file mode 100644 index 9b5cf096..00000000 --- a/src/stores/report/finance-tab/finance-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type FinanceTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useFinanceTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'FinanceTabStore', - } - ) -); diff --git a/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts b/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts deleted file mode 100644 index f9e142b1..00000000 --- a/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type LogisticStockTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useLogisticStockTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'LogisticStockTabStore', - } - ) -); diff --git a/src/stores/report/marketing-tab/marketing-tab.store.ts b/src/stores/report/marketing-tab/marketing-tab.store.ts deleted file mode 100644 index 153bbb8d..00000000 --- a/src/stores/report/marketing-tab/marketing-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type MarketingTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useMarketingTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'MarketingTabStore', - } - ) -); diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts new file mode 100644 index 00000000..76a2c684 --- /dev/null +++ b/src/stores/report/report-tab.store.ts @@ -0,0 +1,18 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createReportTabSlice, ReportTabSlice } from '@/stores/report/slices/report-tab.slice'; + +export type ReportTabStore = ReportTabSlice; + +export const useReportTabStore = create()( + devtools( + (...args) => ({ + ...createReportTabSlice(...args), + }), + { + name: 'ReportTabStore', + } + ) +); diff --git a/src/stores/report/slices/report-tab.slice.ts b/src/stores/report/slices/report-tab.slice.ts new file mode 100644 index 00000000..6582eaed --- /dev/null +++ b/src/stores/report/slices/report-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ReportTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createReportTabSlice: StateCreator< + ReportTabSlice, + [], + [], + ReportTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); From 211622c7b06fa5d79c3a2d874ecba619c45f5d84 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 16:19:40 +0700 Subject: [PATCH 27/82] refactor(FE): Format import statements in report-tab.store.ts --- src/stores/report/report-tab.store.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts index 76a2c684..aad47d17 100644 --- a/src/stores/report/report-tab.store.ts +++ b/src/stores/report/report-tab.store.ts @@ -2,7 +2,10 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createReportTabSlice, ReportTabSlice } from '@/stores/report/slices/report-tab.slice'; +import { + createReportTabSlice, + ReportTabSlice, +} from '@/stores/report/slices/report-tab.slice'; export type ReportTabStore = ReportTabSlice; From 5c00893ea3fbf4db448fe34b8d2812e1a0f471e3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:24:42 +0700 Subject: [PATCH 28/82] refactor(FE): Refactor production result components and improve UI --- src/app/report/production-result/page.tsx | 4 +- ...oductionResultProjectFlockKandangTable.tsx | 137 +-- .../ProductionResultTabs.tsx | 39 + .../filter/ProductionResultFilter.ts | 41 +- .../skeleton/ProductionResultSkeleton.tsx | 15 +- .../tab/ProductionResultTab.tsx | 919 +++++++++++------- 6 files changed, 682 insertions(+), 473 deletions(-) create mode 100644 src/components/pages/report/production-result/ProductionResultTabs.tsx diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index fb8f2a0c..4c9ea02b 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -1,9 +1,9 @@ -import ProductionResultContent from '@/components/pages/report/production-result/tab/ProductionResultTab'; +import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs'; const ProductionResultReportPage = () => { return (
- +
); }; diff --git a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx index e1dfd515..ded97d02 100644 --- a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx +++ b/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx @@ -4,12 +4,10 @@ import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { ProductionResult } from '@/types/api/report/production-result'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -52,8 +50,6 @@ const ProductionResultProjectFlockKandangTable = ({ } ); - const [open, setOpen] = useState(false); - const [sorting, setSorting] = useState([]); const productionResultColumns: ColumnDef[] = [ @@ -270,93 +266,60 @@ const ProductionResultProjectFlockKandangTable = ({ } }, [sorting]); - useEffect(() => { - if (!open) { - setOpen( + return ( + 0 : false - ); - } - }, [productionResults, isResponseSuccess]); - - return ( - - -
{kandangName}
- - - + + data={ + isResponseSuccess(productionResults) ? productionResults?.data : [] } - className='w-full!' - titleClassName='w-full p-0!' - > -
- {/*
-
- -
-
*/} - - - data={ - isResponseSuccess(productionResults) - ? productionResults?.data - : [] - } - columns={productionResultColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(productionResults) - ? productionResults?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(productionResults) - ? productionResults?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingProductionResults} - sorting={sorting} - setSorting={setSorting} - renderFooter={false} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(productionResults) && - productionResults?.data?.length === 0, - }), - headerColumnClassName: - 'px-4 py-3 border-x border-base-content/10 text-base-content/50', - }} - /> -
-
+ columns={productionResultColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(productionResults) + ? productionResults?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(productionResults) + ? productionResults?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingProductionResults} + sorting={sorting} + setSorting={setSorting} + renderFooter={false} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + />
); }; diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx new file mode 100644 index 00000000..2bd7765f --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; +import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultTab'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; + +const ProductionResultTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Hasil Produksi', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default ProductionResultTabs; diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index 1c1979eb..3281bb19 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -1,48 +1,17 @@ import * as yup from 'yup'; -export type ProductionResultFilterType = { +export type ProductionResultFilterProps = { area_id: string | null; location_id: string | null; project_flock_id: string | null; kandang_id: string | null; - date_start: string | null; - date_end: string | null; - sort_by: string | null; - show_unrecorded: boolean | null; }; export const ProductionResultFilterSchema = yup.object({ - area_id: yup.string().nullable(), - location_id: yup.string().nullable(), - project_flock_id: yup.string().nullable(), - kandang_id: yup.string().nullable(), - date_start: yup - .string() - .nullable() - .test( - 'is-valid-date', - 'Tanggal mulai tidak valid', - (value) => !value || !isNaN(Date.parse(value)) - ), - date_end: yup - .string() - .nullable() - .test( - 'is-valid-date', - 'Tanggal akhir tidak valid', - (value) => !value || !isNaN(Date.parse(value)) - ) - .test( - 'is-after-start', - 'Tanggal akhir tidak boleh lebih awal dari tanggal mulai', - function (value) { - const { date_start } = this.parent; - if (!date_start || !value) return true; - return new Date(value) >= new Date(date_start); - } - ), - sort_by: yup.string().nullable(), - show_unrecorded: yup.boolean().nullable(), + area_id: yup.string().required('Area wajib dipilih'), + location_id: yup.string().required('Lokasi wajib dipilih'), + project_flock_id: yup.string().required('Project Flock wajib dipilih'), + kandang_id: yup.string().required('Kandang wajib dipilih'), }); export type ProductionResultFilterValues = yup.InferType< diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx index c8cd63d8..07d33233 100644 --- a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx +++ b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx @@ -4,13 +4,26 @@ import Table from '@/components/Table'; import { ProductionResult } from '@/types/api/report/production-result'; import { ColumnDef } from '@tanstack/react-table'; +type ProductionResultColumn = + | ColumnDef + | { + header: string; + columns: Array<{ + header: string; + accessorKey?: string; + cell?: (props: { + row: { original: ProductionResult }; + }) => React.ReactNode; + }>; + }; + const ProductionResultSkeleton = ({ columns, icon, title, subtitle, }: { - columns: ColumnDef[]; + columns: ProductionResultColumn[]; icon: React.ReactNode; title: string; subtitle: string; diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index ac5e76fd..2d7915e8 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -1,6 +1,7 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import useSWR from 'swr'; import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX'; import toast from 'react-hot-toast'; @@ -13,8 +14,9 @@ import SelectInput, { } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; -import Card from '@/components/Card'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; +import { useFormik } from 'formik'; +import { ProductionResultFilterSchema } from '@/components/pages/report/production-result/filter/ProductionResultFilter'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { AreaApi, LocationApi } from '@/services/api/master-data'; @@ -26,87 +28,248 @@ import { BaseProjectFlockKandang, ProjectFlockKandang, } from '@/types/api/production/project-flock-kandang'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import Pagination from '@/components/Pagination'; +import { isResponseSuccess } from '@/lib/api-helper'; import { ProductionResultReportApi } from '@/services/api/report/production-result'; import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient } from '@/services/http/client'; +import { ColumnDef } from '@tanstack/react-table'; import { ProductionResult } from '@/types/api/report/production-result'; import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import Modal, { useModal } from '@/components/Modal'; +import { cn, formatNumber } from '@/lib/helper'; +import Pagination from '@/components/Pagination'; +import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton'; -const ProductionResultContent = () => { - const [projectFlockKandangs, setProjectFlockKandangs] = useState< - ProjectFlockKandang[] | null - >(null); - const [projectFlockKandangMetadata, setProjectFlockKandangMetadata] = - useState< - | { - page: number; - limit: number; - total_pages: number; - total_results: number; - } - | undefined - >(undefined); +interface ProductionResultTabProps { + tabId: string; +} +interface FilterParams { + area_id?: string; + location_id?: string; + project_flock_id?: string; + project_flock_kandang_id?: string; +} + +type ProductionResultFilterFormValues = { + area_id: OptionType | null; + location_id: OptionType | null; + project_flock_id: OptionType | null; + kandang_id: OptionType | null; +}; + +const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + const [filterParams, setFilterParams] = useState({}); + + // ===== PAGINATION STATE ===== const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const [isLoadingSearch, setIsLoadingSearch] = useState(false); + const filterModal = useModal(); - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = - useState(false); + // ===== TABLE COLUMNS ===== + const productionResultColumns: ColumnDef[] = [ + { + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'woa', + header: 'WOA', + }, + { + accessorKey: 'bw', + header: 'BW', + cell: (props) => formatNumber(props.row.original.bw), + }, + { + accessorKey: 'std_bw', + header: 'STD BW', + cell: (props) => formatNumber(props.row.original.std_bw), + }, + { + accessorKey: 'uniformity', + header: 'Uniformity', + cell: (props) => formatNumber(props.row.original.uniformity), + }, + { + accessorKey: 'std_uniformity', + header: 'STD Uniformity', + }, + { + accessorKey: 'dep_kum', + header: 'Dep Kum', + cell: (props) => formatNumber(props.row.original.dep_kum), + }, + { + accessorKey: 'dep_std', + header: 'Dep STD', + cell: (props) => formatNumber(props.row.original.dep_std), + }, + { + header: 'Butiran', + columns: [ + { + accessorKey: 'butiran_utuh', + header: 'Utuh', + cell: (props) => formatNumber(props.row.original.butiran_utuh), + }, + { + accessorKey: 'butiran_putih', + header: 'Putih', + cell: (props) => formatNumber(props.row.original.butiran_putih), + }, + { + accessorKey: 'butiran_retak', + header: 'Retak', + cell: (props) => formatNumber(props.row.original.butiran_retak), + }, + { + accessorKey: 'butiran_pecah', + header: 'Pecah', + cell: (props) => formatNumber(props.row.original.butiran_pecah), + }, + { + accessorKey: 'butiran_jumlah', + header: 'Jumlah (Butir)', + cell: (props) => formatNumber(props.row.original.butiran_jumlah), + }, + { + accessorKey: 'total_butir', + header: 'Total Butir', + cell: (props) => formatNumber(props.row.original.total_butir), + }, + ], + }, + { + header: 'Kg', + columns: [ + { + accessorKey: 'kg_utuh', + header: 'Utuh (Kg)', + cell: (props) => formatNumber(props.row.original.kg_utuh), + }, + { + accessorKey: 'kg_putih', + header: 'Putih (Kg)', + cell: (props) => formatNumber(props.row.original.kg_putih), + }, + { + accessorKey: 'kg_retak', + header: 'Retak (Kg)', + cell: (props) => formatNumber(props.row.original.kg_retak), + }, + { + accessorKey: 'kg_pecah', + header: 'Pecah (Kg)', + cell: (props) => formatNumber(props.row.original.kg_pecah), + }, + { + accessorKey: 'kg_jumlah', + header: 'Jumlah (Kg)', + cell: (props) => formatNumber(props.row.original.kg_jumlah), + }, + { + accessorKey: 'total_kg', + header: 'Total Kg', + cell: (props) => formatNumber(props.row.original.total_kg), + }, + ], + }, + { + header: '%', + columns: [ + { + accessorKey: 'persen_utuh', + header: 'Utuh', + cell: (props) => formatNumber(props.row.original.persen_utuh), + }, + { + accessorKey: 'persen_putih', + header: 'Putih', + cell: (props) => formatNumber(props.row.original.persen_putih), + }, + { + accessorKey: 'persen_retak', + header: 'Retak', + cell: (props) => formatNumber(props.row.original.persen_retak), + }, + { + accessorKey: 'persen_pecah', + header: 'Pecah', + cell: (props) => formatNumber(props.row.original.persen_pecah), + }, + ], + }, + ]; - const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); - - const [selectedArea, setSelectedArea] = useState(null); - const [selectedLocation, setSelectedLocation] = useState( - null - ); - const [selectedProjectFlock, setSelectedProjectFlock] = - useState(null); - const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] = - useState(null); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + project_flock_id: null, + kandang_id: null, + }, + validationSchema: ProductionResultFilterSchema, + onSubmit: (values) => { + setFilterParams({ + area_id: values.area_id?.value + ? String(values.area_id.value) + : undefined, + location_id: values.location_id?.value + ? String(values.location_id.value) + : undefined, + project_flock_id: values.project_flock_id?.value + ? String(values.project_flock_id.value) + : undefined, + project_flock_kandang_id: values.kandang_id?.value + ? String(values.kandang_id.value) + : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setPage(1); + }, + }); + // ===== OPTIONS ===== const { setInputValue: setAreaInputValue, options: areaOptions, - isLoadingOptions: isLoadingAreaOptions, + isLoadingOptions: isLoadingAreas, loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name'); - - const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedArea(val as OptionType); - - setSelectedLocation(null); - - setSelectedProjectFlock(null); - - setSelectedProjectFlockKandang(null); - }; + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); const { setInputValue: setLocationInputValue, options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, + isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) + : '', }); - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - - setSelectedProjectFlock(null); - - setSelectedProjectFlockKandang(null); - }; - const { setInputValue: setProjectFlockInputValue, options: projectFlockOptions, - isLoadingOptions: isLoadingProjectFlockOptions, + isLoadingOptions: isLoadingProjectFlocks, loadMore: loadMoreProjectFlocks, } = useSelect( ProjectFlockApi.basePath, @@ -114,26 +277,20 @@ const ProductionResultContent = () => { 'flock_name', 'search', { - area_id: selectedArea - ? ((selectedArea as OptionType).value as string) + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) : '', - location_id: selectedLocation - ? ((selectedLocation as OptionType).value as string) + location_id: formik.values.location_id?.value + ? String(formik.values.location_id.value) : '', category: 'LAYING', } ); - const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedProjectFlock(val as OptionType); - - setSelectedProjectFlockKandang(null); - }; - const { setInputValue: setProjectFlockKandangInputValue, options: projectFlockKandangOptions, - isLoadingOptions: isLoadingProjectFlockKandangOptions, + isLoadingOptions: isLoadingProjectFlockKandangs, loadMore: loadMoreProjectFlockKandangs, } = useSelect( ProjectFlockKandangApi.basePath, @@ -141,37 +298,103 @@ const ProductionResultContent = () => { 'kandang.name', 'search', { - area_id: selectedArea - ? ((selectedArea as OptionType).value as string) + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) : '', - location_id: selectedLocation - ? ((selectedLocation as OptionType).value as string) + location_id: formik.values.location_id?.value + ? String(formik.values.location_id.value) : '', - project_flock_id: selectedProjectFlock - ? ((selectedProjectFlock as OptionType).value as string) + project_flock_id: formik.values.project_flock_id?.value + ? String(formik.values.project_flock_id.value) : '', } ); - const projectFlockKandangChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedProjectFlockKandang(val as OptionType); - }; + // ===== FILTER HELPERS ===== + const areaValue = useMemo( + () => formik.values.area_id, + [formik.values.area_id] + ); - const exportToExcelHandler = async () => { - setIsLoadingExportingToExcel(true); + const locationValue = useMemo( + () => formik.values.location_id, + [formik.values.location_id] + ); + + const projectFlockValue = useMemo( + () => formik.values.project_flock_id, + [formik.values.project_flock_id] + ); + + const projectFlockKandangValue = useMemo( + () => formik.values.kandang_id, + [formik.values.kandang_id] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.area_id) count += 1; + if (filterParams.location_id) count += 1; + if (filterParams.project_flock_id) count += 1; + if (filterParams.project_flock_kandang_id) count += 1; + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: projectFlockKandangsData, isLoading } = useSWR< + BaseApiResponse + >( + isSubmitted + ? () => { + const params = new URLSearchParams(); + if (filterParams.area_id) + params.append('area_id', filterParams.area_id); + if (filterParams.project_flock_id) + params.append('project_flock_id', filterParams.project_flock_id); + params.append('page', String(page)); + params.append('limit', String(pageSize)); + + return [`/production/project-flock-kandangs?${params.toString()}`]; + } + : null, + ([url]: string[]) => httpClient>(url) + ); + + const projectFlockKandangs = useMemo( + () => + isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.data + : null, + [projectFlockKandangsData] + ); + + const projectFlockKandangMetadata = useMemo( + () => + isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.meta + : undefined, + [projectFlockKandangsData] + ); + + // ===== EXPORT HANDLERS ===== + const exportToExcelHandler = useCallback(async () => { + setIsExcelExportLoading(true); try { - let projectFlockKandangsData: BaseProjectFlockKandang[] = []; + let projectFlockKandangsFetch: BaseProjectFlockKandang[] = []; - if (selectedProjectFlockKandang) { + if (filterParams.project_flock_kandang_id) { const projectFlockKandangResponse = await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number + Number(filterParams.project_flock_kandang_id) ); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangResponse ) ? [projectFlockKandangResponse.data] @@ -179,11 +402,11 @@ const ProductionResultContent = () => { } else { const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, + area_id: filterParams.area_id, + project_flock_id: filterParams.project_flock_id, }); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangsResponse ) ? projectFlockKandangsResponse.data @@ -192,7 +415,7 @@ const ProductionResultContent = () => { const productionResultData: ProductionResult[] = []; - for (const kandang of projectFlockKandangsData) { + for (const kandang of projectFlockKandangsFetch) { const getProductionResultPath = `${ProductionResultReportApi.basePath}/${kandang.id}?page=1&limit=100`; const getProductionResultRes = await httpClient< BaseApiResponse @@ -205,7 +428,7 @@ const ProductionResultContent = () => { project_flock: { ...result.project_flock, name: - selectedProjectFlock?.label || + projectFlockValue?.label || kandang.project_flock?.name || `Project Flock #${kandang.project_flock_id}`, category: kandang.project_flock?.category || '', @@ -222,7 +445,7 @@ const ProductionResultContent = () => { if (productionResultData.length === 0) { toast.error('Tidak ada data untuk diexport.'); - setIsLoadingExportingToExcel(false); + setIsExcelExportLoading(false); return; } @@ -233,23 +456,23 @@ const ProductionResultContent = () => { } catch { toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); } finally { - setIsLoadingExportingToExcel(false); + setIsExcelExportLoading(false); } - }; + }, [filterParams, projectFlockValue]); - const exportToPdfHandler = async () => { - setIsLoadingExportingToPdf(true); + const exportToPdfHandler = useCallback(async () => { + setIsPdfExportLoading(true); try { - let projectFlockKandangsData: BaseProjectFlockKandang[] = []; + let projectFlockKandangsFetch: BaseProjectFlockKandang[] = []; - if (selectedProjectFlockKandang) { + if (filterParams.project_flock_kandang_id) { const projectFlockKandangResponse = await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number + Number(filterParams.project_flock_kandang_id) ); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangResponse ) ? [projectFlockKandangResponse.data] @@ -257,11 +480,11 @@ const ProductionResultContent = () => { } else { const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, + area_id: filterParams.area_id, + project_flock_id: filterParams.project_flock_id, }); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangsResponse ) ? projectFlockKandangsResponse.data @@ -272,7 +495,7 @@ const ProductionResultContent = () => { projectFlockKandang: BaseProjectFlockKandang; productionResult: ProductionResult[] | null; }[] = await Promise.all( - projectFlockKandangsData.map(async (projectFlockKandang) => { + projectFlockKandangsFetch.map(async (projectFlockKandang) => { const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`; const getProductionResultRes = await httpClient< BaseApiResponse @@ -289,7 +512,7 @@ const ProductionResultContent = () => { if (mappedProductionResults.length === 0) { toast.error('Tidak ada data untuk diexport.'); - setIsLoadingExportingToPdf(false); + setIsPdfExportLoading(false); return; } @@ -312,258 +535,141 @@ const ProductionResultContent = () => { toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); } - setIsLoadingExportingToPdf(false); - }; + setIsPdfExportLoading(false); + }, [filterParams]); - const searchHandler = async () => { - setProjectFlockKandangs(null); - setIsLoadingSearch(true); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); - try { - if (selectedProjectFlockKandang) { - const projectFlockKandangResponse = - await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number - ); + useEffect(() => { + setTabActions( + tabId, +
+ - if ( - !projectFlockKandangResponse || - isResponseError(projectFlockKandangResponse) - ) { - throw new Error(); - } + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + exportToExcelHandler, + exportToPdfHandler, + setTabActions, + ]); - setProjectFlockKandangs([projectFlockKandangResponse.data]); - setProjectFlockKandangMetadata({ - page: 1, - limit: 10, - total_pages: 1, - total_results: 1, - }); - setIsLoadingSearch(false); - return; - } - - const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, - }); - - if ( - !projectFlockKandangsResponse || - isResponseError(projectFlockKandangsResponse) - ) { - throw new Error(); - } - - setProjectFlockKandangs(projectFlockKandangsResponse.data); - setProjectFlockKandangMetadata(projectFlockKandangsResponse.meta); - setIsLoadingSearch(false); - } catch (error) { - toast.error('Gagal mencari data! Coba lagi.'); - setProjectFlockKandangs(null); - setProjectFlockKandangMetadata(undefined); - setIsLoadingSearch(false); - } - }; - - const resetHandler = () => { - setProjectFlockKandangs(null); - setSelectedArea(null); - setSelectedLocation(null); - setSelectedProjectFlock(null); - setSelectedProjectFlockKandang(null); - // resetFilter(); - }; + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); return ( -
- -
-

- Laporan Hasil Produksi -

-
- - {/* Filters */} -
-
- - - - - - - -
- -
-
- - - - - Export{' '} - - - } - > - - - - - -
-
-
-
- -
- {isLoadingSearch && ( - - )} - - {!isLoadingSearch && !projectFlockKandangs && ( -

- Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -

- )} - - {!isLoadingSearch && projectFlockKandangs?.length === 0 && ( -

- Tidak ada data kandang project flock yang dapat ditampilkan. -

- )} - - {!isLoadingSearch && projectFlockKandangs && ( - - {projectFlockKandangs.map((projectFlockKandang) => ( - +
+ {!isSubmitted ? ( + - ))} + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( +
+ +
+ ) : !projectFlockKandangs || projectFlockKandangs.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( + <> + {projectFlockKandangs.map( + (projectFlockKandang: ProjectFlockKandang) => ( + + ) + )} -
+
{ onRowChange={setPageSize} />
- + )}
-
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+ {/* Modal Body */} +
+ { + formik.setFieldValue('area_id', val); + formik.setFieldValue('location_id', null); + formik.setFieldValue('project_flock_id', null); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('location_id', val); + formik.setFieldValue('project_flock_id', null); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + isDisabled={!formik.values.area_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('project_flock_id', val); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} + isClearable + isDisabled={!formik.values.location_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('kandang_id', val); + }} + onInputChange={setProjectFlockKandangInputValue} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} + isClearable + isDisabled={!formik.values.project_flock_id} + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ ); }; From d5962f94a1ff0be63114e5d5721dd6fecdef826a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:29:37 +0700 Subject: [PATCH 29/82] refactor(FE): Refactor production result filter to use OptionType --- .../filter/ProductionResultFilter.ts | 50 +++++++++++++++++-- .../tab/ProductionResultTab.tsx | 44 +++++++++------- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index 3281bb19..6df3759e 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -1,3 +1,4 @@ +import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; export type ProductionResultFilterProps = { @@ -7,12 +8,51 @@ export type ProductionResultFilterProps = { kandang_id: string | null; }; +export type ProductionResultFilterFormType = { + area_id: OptionType | null; + location_id: OptionType | null; + project_flock_id: OptionType | null; + kandang_id: OptionType | null; +}; + export const ProductionResultFilterSchema = yup.object({ - area_id: yup.string().required('Area wajib dipilih'), - location_id: yup.string().required('Lokasi wajib dipilih'), - project_flock_id: yup.string().required('Project Flock wajib dipilih'), - kandang_id: yup.string().required('Kandang wajib dipilih'), -}); + area_id: yup + .mixed() + .required('Area wajib dipilih') + .test('is-not-empty', 'Area wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + location_id: yup + .mixed() + .required('Lokasi wajib dipilih') + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + project_flock_id: yup + .mixed() + .required('Project Flock wajib dipilih') + .test('is-not-empty', 'Project Flock wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + kandang_id: yup + .mixed() + .required('Kandang wajib dipilih') + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), +}) as yup.ObjectSchema; export type ProductionResultFilterValues = yup.InferType< typeof ProductionResultFilterSchema diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index 2d7915e8..090ca781 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -8,15 +8,15 @@ import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Dropdown from '@/components/dropdown/Dropdown'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; import { useFormik } from 'formik'; -import { ProductionResultFilterSchema } from '@/components/pages/report/production-result/filter/ProductionResultFilter'; +import { + ProductionResultFilterSchema, + type ProductionResultFilterValues, +} from '@/components/pages/report/production-result/filter/ProductionResultFilter'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { AreaApi, LocationApi } from '@/services/api/master-data'; @@ -53,13 +53,6 @@ interface FilterParams { project_flock_kandang_id?: string; } -type ProductionResultFilterFormValues = { - area_id: OptionType | null; - location_id: OptionType | null; - project_flock_id: OptionType | null; - kandang_id: OptionType | null; -}; - const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -213,7 +206,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { ]; // ===== FORMIK SETUP ===== - const formik = useFormik({ + const formik = useFormik({ initialValues: { area_id: null, location_id: null, @@ -221,6 +214,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { kandang_id: null, }, validationSchema: ProductionResultFilterSchema, + validateOnBlur: true, + validateOnChange: true, onSubmit: (values) => { setFilterParams({ area_id: values.area_id?.value @@ -723,7 +718,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { {/* Modal Body */}
{ onInputChange={setAreaInputValue} onMenuScrollToBottom={loadMoreAreas} isClearable + isError={formik.touched.area_id && Boolean(formik.errors.area_id)} + errorMessage={formik.errors.area_id} className={{ wrapper: 'w-full' }} /> { onMenuScrollToBottom={loadMoreLocations} isClearable isDisabled={!formik.values.area_id} + isError={ + formik.touched.location_id && Boolean(formik.errors.location_id) + } + errorMessage={formik.errors.location_id} className={{ wrapper: 'w-full' }} /> { onMenuScrollToBottom={loadMoreProjectFlocks} isClearable isDisabled={!formik.values.location_id} + isError={ + formik.touched.project_flock_id && + Boolean(formik.errors.project_flock_id) + } + errorMessage={formik.errors.project_flock_id} className={{ wrapper: 'w-full' }} /> { onMenuScrollToBottom={loadMoreProjectFlockKandangs} isClearable isDisabled={!formik.values.project_flock_id} + isError={ + formik.touched.kandang_id && Boolean(formik.errors.kandang_id) + } + errorMessage={formik.errors.kandang_id} className={{ wrapper: 'w-full' }} />
From 684f67593fcc60c948e7bc91015a0ea42d39d15a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:32:45 +0700 Subject: [PATCH 30/82] refactor(FE): Refactor SelectInput styles for improved readability --- src/components/input/SelectInput.tsx | 53 ++++++++++++---------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 38be09e4..ef959ea7 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -284,23 +284,22 @@ const SelectInput = (props: SelectInputProps) => { isDisabled && !readOnly, 'bg-transparent! cursor-not-allowed!': readOnly, 'cursor-pointer!': !readOnly && !isDisabled, - 'border-red-500! ring-2 ring-red-200': isError, + 'border-error!': isError, + 'ring-2 ring-error/20': isError, 'border-indigo-500 ring-2 ring-indigo-200': - isFocused && !startAdornment, + isFocused && !startAdornment && !isError, 'border-base-content/10!': !isError && !isFocused, 'rounded-l-none!': inputPrefix && !startAdornment, 'rounded-r-none!': inputSuffix && !startAdornment, }), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, + cn('text-gray-400 text-sm leading-tight', { + 'text-error!': isError, }), singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, + cn('m-0! text-gray-900 text-sm leading-tight', { + 'text-error!': isError && !readOnly, 'text-gray-900!': readOnly, }), input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), @@ -404,32 +403,26 @@ const SelectInput = (props: SelectInputProps) => { className={cn('w-full', className?.select)} classNames={{ control: ({ isFocused, isDisabled }) => - cn( - 'w-full border transition-shadow', - // Gunakan rounded-lg untuk semua kasus - 'rounded-lg!', - { - 'bg-base-100!': !isDisabled && !readOnly, - 'bg-base-200! text-gray-400 cursor-not-allowed': - isDisabled && !readOnly, - 'bg-transparent! cursor-not-allowed!': readOnly, - 'cursor-pointer!': !readOnly && !isDisabled, - 'border-red-500! ring-2 ring-red-200': isError, - 'border-indigo-500 ring-2 ring-indigo-200': - isFocused && !startAdornment, - 'border-base-content/10!': !isError && !isFocused, - } - ), + cn('w-full border transition-shadow rounded-lg!', { + 'bg-base-100!': !isDisabled && !readOnly, + 'bg-base-200! text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, + 'cursor-pointer!': !readOnly && !isDisabled, + 'border-error!': isError, + 'ring-2 ring-error/20': isError, + 'border-indigo-500 ring-2 ring-indigo-200': + isFocused && !startAdornment && !isError, + 'border-base-content/10!': !isError && !isFocused, + }), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, + cn('text-gray-400 text-sm leading-tight', { + 'text-error!': isError, }), singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, + cn('m-0! text-gray-900 text-sm leading-tight', { + 'text-error!': isError && !readOnly, 'text-gray-900!': readOnly, }), input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), From 3a676723e41afb8c92fb091c90b5553b352156d7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:42:04 +0700 Subject: [PATCH 31/82] refactor(FE): Refactor table class names in DailyMarketingTab --- .../marketing/tab/DailyMarketingTab.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index df06c83c..a336b671 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -725,10 +725,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { columns={getTableColumns()} renderFooter={data.length > 0} className={{ - containerClassName: cn('p-3', { - 'w-full mb-20': data.length === 0, - }), - headerColumnClassName: 'text-nowrap', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200 text-nowrap', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> )} From d312da4c6639beb41fb866eea56a9be29988b9f9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:53:33 +0700 Subject: [PATCH 32/82] refactor(FE): Refactor dropdown and export button components --- .../report/finance/tab/CustomerPaymentTab.tsx | 77 +++++++++++-------- .../report/finance/tab/DebtSupplierTab.tsx | 69 ++++++++++------- .../tab/PurchasesPerSupplierTab.tsx | 77 +++++++++++-------- .../report/marketing/tab/HppPerKandangTab.tsx | 77 +++++++++++-------- .../tab/ProductionResultTab.tsx | 77 +++++++++++-------- 5 files changed, 216 insertions(+), 161 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1443fa0b..1c546058 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -25,8 +25,6 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; @@ -387,13 +385,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -403,42 +401,55 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 44677313..d0a27b92 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -3,8 +3,6 @@ import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -277,7 +275,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { useEffect(() => { setTabActions( tabId, -
+
{ /> - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5e2c4e27..36476956 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -3,8 +3,6 @@ import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import { useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -498,13 +496,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -514,42 +512,55 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index d3d4bfac..e106dbf4 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -18,8 +18,6 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF'; import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; @@ -499,13 +497,13 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -515,42 +513,55 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index 090ca781..d04c9e20 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -9,8 +9,6 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Dropdown from '@/components/dropdown/Dropdown'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; import { useFormik } from 'formik'; import { @@ -546,13 +544,13 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { color='none' onClick={() => filterModal.openModal()} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -562,42 +560,55 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); From ceb594a4ccd871bb9214615faf74561a9a67a7f9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:55:22 +0700 Subject: [PATCH 33/82] refactor(FE): Rename and update paths for ProductionResult components --- .../pages/report/production-result/ProductionResultTabs.tsx | 2 +- ...ResultTab.tsx => ProductionResultProjectFlockKandangTab.tsx} | 2 +- .../{ => tab}/ProductionResultProjectFlockKandangTable.tsx | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/components/pages/report/production-result/tab/{ProductionResultTab.tsx => ProductionResultProjectFlockKandangTab.tsx} (99%) rename src/components/pages/report/production-result/{ => tab}/ProductionResultProjectFlockKandangTable.tsx (100%) diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx index 2bd7765f..6f5e4410 100644 --- a/src/components/pages/report/production-result/ProductionResultTabs.tsx +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; -import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultTab'; +import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab'; import { useReportTabStore } from '@/stores/report/report-tab.store'; const ProductionResultTabs = () => { diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx similarity index 99% rename from src/components/pages/report/production-result/tab/ProductionResultTab.tsx rename to src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index d04c9e20..9ac5faf6 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -9,7 +9,7 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Dropdown from '@/components/dropdown/Dropdown'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; -import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; +import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable'; import { useFormik } from 'formik'; import { ProductionResultFilterSchema, diff --git a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx similarity index 100% rename from src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx rename to src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx From 67f2a80f237df64d4c3c51b8eb251ddef7da9fe0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 10:19:09 +0700 Subject: [PATCH 34/82] refactor(FE): Refactor expense report page to use tab-based layout --- src/app/report/expense/page.tsx | 8 +- .../report/expense/ReportExpenseTable.tsx | 901 ------------------ .../report/expense/ReportExpenseTabs.tsx | 40 + .../ReportExpenseExportPDF.tsx} | 1 + .../export/ReportExpenseExportXLSX.tsx | 109 +++ .../expense/filter/ReportExpenseFilter.ts | 73 ++ .../pdf/styles/ReportExpenseStyles.tsx | 365 ------- .../skeleton/ReportExpenseSkeleton.tsx | 51 + .../report/expense/tab/ReportExpenseTab.tsx | 755 +++++++++++++++ 9 files changed, 1031 insertions(+), 1272 deletions(-) delete mode 100644 src/components/pages/report/expense/ReportExpenseTable.tsx create mode 100644 src/components/pages/report/expense/ReportExpenseTabs.tsx rename src/components/pages/report/expense/{pdf/ReportExpenseExport.tsx => export/ReportExpenseExportPDF.tsx} (99%) create mode 100644 src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx create mode 100644 src/components/pages/report/expense/filter/ReportExpenseFilter.ts delete mode 100644 src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx create mode 100644 src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx create mode 100644 src/components/pages/report/expense/tab/ReportExpenseTab.tsx diff --git a/src/app/report/expense/page.tsx b/src/app/report/expense/page.tsx index 99d2862e..bb497283 100644 --- a/src/app/report/expense/page.tsx +++ b/src/app/report/expense/page.tsx @@ -1,13 +1,9 @@ 'use client'; -import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable'; +import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs'; const ReportExpense = () => { - return ( -
- -
- ); + return ; }; export default ReportExpense; diff --git a/src/components/pages/report/expense/ReportExpenseTable.tsx b/src/components/pages/report/expense/ReportExpenseTable.tsx deleted file mode 100644 index c809c153..00000000 --- a/src/components/pages/report/expense/ReportExpenseTable.tsx +++ /dev/null @@ -1,901 +0,0 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; -import useSWR from 'swr'; -import Button from '@/components/Button'; -import Card from '@/components/Card'; -import DateInput from '@/components/input/DateInput'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; -import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; -import { ReportExpense } from '@/types/api/report/report-expense'; -import { Icon } from '@iconify/react'; -import { ColumnDef } from '@tanstack/react-table'; -import { ReportExpenseApi } from '@/services/api/report'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import Pagination from '@/components/Pagination'; -import Dropdown from '@/components/dropdown/Dropdown'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; -import * as XLSX from 'xlsx'; -import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; -import toast from 'react-hot-toast'; -import { - KandangApi, - LocationApi, - NonstockApi, - SupplierApi, -} from '@/services/api/master-data'; -import { Supplier } from '@/types/api/master-data/supplier'; -import { Kandang } from '@/types/api/master-data/kandang'; -import { Nonstock } from '@/types/api/master-data/nonstock'; - -const ReportExpenseTable = () => { - // ===== STATE MANAGEMENT ===== - const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); - const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const [dropdownOpen, setDropdownOpen] = useState(false); - const [pdfProgress, setPdfProgress] = useState(0); - const [excelProgress, setExcelProgress] = useState(0); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; - - // ===== SUBMISSION STATE ===== - const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== TABLE FILTER STATE ===== - const { - state: filterState, - updateFilter, - setPage, - setPageSize, - reset: resetFilterState, - toQueryString, - } = useTableFilter({ - initial: { - location_id: '', - supplier_id: '', - kandang_id: '', - nonstock_id: '', - realization_date: '', - category: '', - search: '', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); - - // ===== SELECT OPTIONS ===== - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name'); - - const { - setInputValue: setSupplierInputValue, - options: supplierOptions, - isLoadingOptions: isLoadingSupplierOptions, - loadMore: loadMoreSuppliers, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangOptions, - loadMore: loadMoreKandangs, - } = useSelect(KandangApi.basePath, 'id', 'name'); - - const { - setInputValue: setNonstockInputValue, - options: nonstockOptions, - isLoadingOptions: isLoadingNonstockOptions, - loadMore: loadMoreNonstocks, - } = useSelect(NonstockApi.basePath, 'id', 'name'); - - const categoryOptions = useMemo( - () => [ - { value: 'BOP', label: 'BOP' }, - { value: 'NON-BOP', label: 'Non BOP' }, - ], - [] - ); - - // Mendapatkan value option select dari filter state - const selectedLocation = useMemo( - () => - locationOptions.find( - (opt) => String(opt.value) === filterState.location_id - ) || null, - [locationOptions, filterState.location_id] - ); - const selectedSupplier = useMemo( - () => - supplierOptions.find( - (opt) => String(opt.value) === filterState.supplier_id - ) || null, - [supplierOptions, filterState.supplier_id] - ); - const selectedKandang = useMemo( - () => - kandangOptions.find( - (opt) => String(opt.value) === filterState.kandang_id - ) || null, - [kandangOptions, filterState.kandang_id] - ); - const selectedNonstock = useMemo( - () => - nonstockOptions.find( - (opt) => String(opt.value) === filterState.nonstock_id - ) || null, - [nonstockOptions, filterState.nonstock_id] - ); - const selectedCategory = useMemo( - () => - categoryOptions.find((opt) => opt.value === filterState.category) || null, - [categoryOptions, filterState.category] - ); - - // ===== FILTER CHANGE HANDLERS ===== - const locationChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('location_id', option ? String(option.value) : ''); - updateFilter('kandang_id', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const kandangChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('kandang_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const supplierChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('supplier_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const nonstockChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('nonstock_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const categoryChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('category', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const realizationDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - updateFilter('realization_date', e.target.value || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const searchChangeHandler = useCallback( - (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); - setIsSubmitted(false); - }, - [updateFilter] - ); - - // ===== RESET FILTERS ===== - const resetFilters = useCallback(() => { - resetFilterState(); - setIsSubmitted(false); - }, [resetFilterState]); - - // ===== SUBMIT HANDLER ===== - const handleSubmit = useCallback(() => { - setIsSubmitted(true); - setPage(1); - }, [setPage]); - - // ===== DATA FETCHING FOR TABLE ===== - const { data: reportExpenseResponse, isLoading } = useSWR( - isSubmitted - ? () => { - return ['report-expense', toQueryString()]; - } - : null, - ([, query]) => { - const endpoint = `${ReportExpenseApi.basePath}${query}`; - return ReportExpenseApi.getAllFetcher(endpoint); - } - ); - - const data: ReportExpense[] = useMemo( - () => - isResponseSuccess(reportExpenseResponse) - ? (reportExpenseResponse?.data as ReportExpense[]) || [] - : [], - [reportExpenseResponse] - ); - - const meta = useMemo( - () => - isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta - ? reportExpenseResponse.meta - : null, - [reportExpenseResponse] - ); - - // ===== EXPORT DATA FETCHER ===== - const reportExpenseExport = useCallback(async (): Promise< - ReportExpense[] | null - > => { - const params = new URLSearchParams(toQueryString().replace('?', '')); - params.set('limit', 'limit'); - params.set('page', '1'); - - const endpoint = `${ReportExpenseApi.basePath}?${params.toString()}`; - const response = await ReportExpenseApi.getAllFetcher(endpoint); - - return isResponseSuccess(response) ? response.data : null; - }, [toQueryString]); - - // ===== EXPORT HANDLERS ===== - const handleExportPdf = useCallback(async () => { - if (isPdfExportLoading) return; - setIsPdfExportLoading(true); - setPdfProgress(0); - - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - try { - // Stage 1: Fetching data (0-20%) - setPdfProgress(10); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const allData = await reportExpenseExport(); - if (!allData || allData.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - setIsPdfExportLoading(false); - setPdfProgress(0); - return; - } - - // Stage 2: Data fetched - langsung loncat ke progress tinggi - setPdfProgress(30); - await new Promise((resolve) => setTimeout(resolve, 50)); - const progressInterval = setInterval(() => { - setPdfProgress((prev) => { - // Increment kecil dan random antara 0.5-2% - const increment = Math.random() * 1.5 + 0.5; - const newProgress = Math.min(prev + increment, 50); - return newProgress; - }); - }, 300); // Update setiap 300ms - - const pdfParams = { - location_name: selectedLocation?.label, - supplier_name: selectedSupplier?.label, - kandang_name: selectedKandang?.label, - nonstock_name: selectedNonstock?.label, - category: selectedCategory?.label, - realization_date: filterState.realization_date, - search: filterState.search, - }; - - setDropdownOpen(false); - - // Stage 3: Langsung loncat ke 80-85% untuk menghindari stuck - const baseProgress = 80 + Math.floor(Math.random() * 16); // Random 80-85% - setPdfProgress(baseProgress); - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Stage 4: Berikan jeda untuk UI update - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - // Proses PDF yang sebenarnya - await generateReportExpensePDF(allData, pdfParams); - - clearInterval(progressInterval); - - // Stage 5: Finalizing (98-100%) - setPdfProgress(99); - await new Promise((resolve) => setTimeout(resolve, 100)); - - setPdfProgress(100); - toast.success('PDF berhasil dibuat dan diunduh.'); - - // Reset progress setelah selesai - setTimeout(() => setPdfProgress(0), 500); - } catch (error) { - console.error('PDF Export Error:', error); - toast.error('Gagal membuat PDF. Silakan coba lagi.'); - setPdfProgress(0); - } finally { - setIsPdfExportLoading(false); - } - }, [ - reportExpenseExport, - selectedLocation, - selectedSupplier, - selectedKandang, - selectedNonstock, - selectedCategory, - filterState.realization_date, - filterState.search, - ]); - - const handleExportExcel = useCallback(async () => { - if (isExcelExportLoading) return; - setIsExcelExportLoading(true); - setExcelProgress(0); - setDropdownOpen(false); - - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - try { - // Stage 1: Fetching data (0-20%) - setExcelProgress(15); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const allDataForExport = await reportExpenseExport(); - - if (!allDataForExport || allDataForExport.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - setIsExcelExportLoading(false); - setExcelProgress(0); - return; - } - - // Stage 2: Data fetched (20-40%) - setExcelProgress(30); - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Stage 3: Grouping data (40-60%) - setExcelProgress(50); - const groupedBySupplier: Record = {}; - allDataForExport.forEach((item) => { - const supplierName = item.supplier?.name || 'Unknown Supplier'; - if (!groupedBySupplier[supplierName]) { - groupedBySupplier[supplierName] = []; - } - groupedBySupplier[supplierName].push(item); - }); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Stage 4: Creating workbook (60-80%) - setExcelProgress(70); - const workbook = XLSX.utils.book_new(); - - const supplierEntries = Object.entries(groupedBySupplier); - const totalSuppliers = supplierEntries.length; - - for (let i = 0; i < supplierEntries.length; i++) { - const [supplierName, supplierData] = supplierEntries[i]; - - // Update progress per supplier - const progressIncrement = (20 / totalSuppliers) * (i + 1); - setExcelProgress(70 + progressIncrement); - - const totals = supplierData.reduce( - (acc, item) => ({ - qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0), - total_pengajuan: - acc.total_pengajuan + - (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), - qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0), - total_realisasi: - acc.total_realisasi + - (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), - }), - { - qty_pengajuan: 0, - total_pengajuan: 0, - qty_realisasi: 0, - total_realisasi: 0, - } - ); - - const excelData = supplierData.map((item, index) => ({ - No: index + 1, - 'No. PO': item.po_number || '', - 'No. Referensi': item.reference_number || '', - 'Tanggal Realisasi': item.realization_date - ? formatDate(item.realization_date, 'DD MMM YYYY') - : '', - 'Tanggal Transaksi': item.transaction_date - ? formatDate(item.transaction_date, 'DD MMM YYYY') - : '', - Kategori: item.category || '', - Produk: item.pengajuan?.nonstock?.name || '', - Lokasi: item.kandang?.location?.name || '', - Kandang: item.kandang?.name || '', - 'Qty Pengajuan': item.pengajuan?.qty || 0, - 'Harga Pengajuan': item.pengajuan?.price || 0, - 'Total Pengajuan': - (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), - 'Qty Realisasi': item.realisasi?.qty || 0, - 'Harga Realisasi': item.realisasi?.price || 0, - 'Total Realisasi': - (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), - 'Status Pencairan': item.latest_approval?.step_name || '', - })); - - excelData.push({ - No: 'Total' as unknown as number, - 'No. PO': '', - 'No. Referensi': '', - 'Tanggal Realisasi': '', - 'Tanggal Transaksi': '', - Kategori: '', - Produk: '', - Lokasi: '', - Kandang: '', - 'Qty Pengajuan': totals.qty_pengajuan, - 'Harga Pengajuan': 0, - 'Total Pengajuan': totals.total_pengajuan, - 'Qty Realisasi': totals.qty_realisasi, - 'Harga Realisasi': 0, - 'Total Realisasi': totals.total_realisasi, - 'Status Pencairan': '', - }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - const colWidths = [ - { wch: 5 }, // No - { wch: 20 }, // No. PO - { wch: 20 }, // No. Referensi - { wch: 15 }, // Tanggal Realisasi - { wch: 15 }, // Tanggal Transaksi - { wch: 15 }, // Kategori - { wch: 30 }, // Produk - { wch: 20 }, // Lokasi - { wch: 15 }, // Kandang - { wch: 15 }, // Qty Pengajuan - { wch: 15 }, // Harga Pengajuan - { wch: 20 }, // Total Pengajuan - { wch: 15 }, // Qty Realisasi - { wch: 15 }, // Harga Realisasi - { wch: 20 }, // Total Realisasi - { wch: 20 }, // Status Pencairan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = supplierName.slice(0, 31); - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - - // Small delay to allow UI update - if (i < supplierEntries.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - - // Stage 5: Writing file (90-100%) - setExcelProgress(95); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; - XLSX.writeFile(workbook, filename); - - setExcelProgress(100); - toast.success('Excel berhasil dibuat dan diunduh.'); - - // Reset progress - setTimeout(() => setExcelProgress(0), 500); - } catch (error) { - console.error('Excel Export Error:', error); - toast.error('Gagal membuat Excel. Silakan coba lagi.'); - setExcelProgress(0); - } finally { - setIsExcelExportLoading(false); - } - }, [isExcelExportLoading, reportExpenseExport]); - - // ===== PAGINATION HANDLERS ===== - const handlePageChange = (page: number) => { - setPage(page); - }; - - const handleRowChange = (pageSize: number) => { - setPageSize(pageSize); - }; - - const handleNextPage = () => { - if (meta && filterState.page < meta.total_pages) { - setPage(filterState.page + 1); - } - }; - - const handlePrevPage = () => { - if (filterState.page > 1) { - setPage(filterState.page - 1); - } - }; - - // ===== TABLE COLUMNS DEFINITION ===== - const columns = useMemo((): ColumnDef[] => { - return [ - { - header: 'No', - accessorFn: (_, index) => - (filterState.page - 1) * filterState.pageSize + index + 1, - }, - { - header: 'No. PO', - accessorKey: 'po_number', - }, - { - header: 'No. Referensi', - accessorKey: 'reference_number', - }, - { - header: 'Tanggal Realisasi', - accessorKey: 'realization_date', - cell: ({ row }) => { - return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); - }, - }, - { - header: 'Tanggal Transaksi', - accessorKey: 'transaction_date', - cell: ({ row }) => { - return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); - }, - }, - { - header: 'Kategori', - accessorKey: 'category', - }, - { - header: 'Produk', - accessorFn: (row) => row.pengajuan?.nonstock?.name, - }, - { - header: 'Supplier', - accessorFn: (row) => row.supplier?.name, - }, - { - header: 'Lokasi', - accessorFn: (row) => row.kandang?.location?.name, - }, - { - header: 'Kandang', - accessorFn: (row) => row.kandang?.name, - }, - { - header: 'Pengajuan', - columns: [ - { - header: 'Qty', - id: 'qty_pengajuan', - accessorFn: (row) => row.pengajuan?.qty, - cell: ({ row }) => - row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', - }, - { - header: 'Harga', - id: 'harga_pengajuan', - accessorFn: (row) => row.pengajuan?.price, - cell: ({ row }) => - formatCurrency(row.original.pengajuan?.price || 0), - }, - { - header: 'Total', - id: 'total_pengajuan', - accessorFn: (row) => - (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), - cell: ({ row }) => { - const total = - (row.original.pengajuan?.qty || 0) * - (row.original.pengajuan?.price || 0); - return formatCurrency(total); - }, - }, - ], - }, - { - header: 'Realisasi', - columns: [ - { - header: 'Qty', - id: 'qty_realisasi', - accessorFn: (row) => row.realisasi?.qty, - cell: ({ row }) => - row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', - }, - { - header: 'Harga', - id: 'harga_realisasi', - accessorFn: (row) => row.realisasi?.price, - cell: ({ row }) => - formatCurrency(row.original.realisasi?.price || 0), - }, - { - header: 'Total', - id: 'total_realisasi', - accessorFn: (row) => - (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), - cell: ({ row }) => { - const total = - (row.original.realisasi?.qty || 0) * - (row.original.realisasi?.price || 0); - return formatCurrency(total); - }, - }, - ], - }, - { - header: 'Status Pencairan', - cell: (props) => ( - - ), - }, - { - header: 'Status BOP', - cell: (props) => ( - - ), - }, - ]; - }, [filterState.page, filterState.pageSize]); - - // ===== RENDER ===== - return ( -
- {isAnyExportLoading && ( -
- - {((isPdfExportLoading && pdfProgress > 0) || - (isExcelExportLoading && excelProgress > 0)) && ( -
-
- {(() => { - const currentProgress = isPdfExportLoading - ? pdfProgress - : excelProgress; - const exportType = isPdfExportLoading ? 'PDF' : 'Excel'; - - if (currentProgress < 20) - return 'Mengambil data dari server...'; - if (currentProgress < 30) return 'Memproses data laporan...'; - if (currentProgress < 40) - return `Menyiapkan struktur dokumen ${exportType}...`; - if (currentProgress < 50) - return 'Mengelompokkan data per supplier...'; - if (currentProgress < 70) - return 'Merender tabel dan kalkulasi...'; - if (currentProgress < 96) - return `Memformat dokumen ${exportType}...`; - if (currentProgress < 100) - return 'Menyelesaikan dan mengunduh...'; - return 'Selesai!'; - })()}{' '} - {Math.round(isPdfExportLoading ? pdfProgress : excelProgress)}% -
- {((isPdfExportLoading && pdfProgress >= 35 && pdfProgress < 90) || - (isExcelExportLoading && - excelProgress >= 35 && - excelProgress < 90)) && ( -
- {(isPdfExportLoading ? pdfProgress : excelProgress) < 96 - ? 'Proses ini membutuhkan waktu lebih lama untuk data dalam jumlah besar. Mohon bersabar...' - : 'Sedang memproses baris data. Hampir selesai...'} -
- )} -
- )} -
- )} - -
-
- - -
-
- { - setDropdownOpen(!dropdownOpen); - }} - > - Export - - } - align='end' - direction='bottom' - open={dropdownOpen} - > - - - - - -
-
-
- } - > -
- - - - - - - } - /> -
- - - {/* ===== TABLE CONTENT ===== */} - {!isSubmitted ? ( -
- Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -
- ) : isLoading ? ( -
- -
- ) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
- ) : ( - <> - - columns={columns} - data={data} - pageSize={10} - className={{ - containerClassName: 'mb-0', - headerRowClassName: cn( - TABLE_DEFAULT_STYLING, - 'whitespace-nowrap' - ), - bodyRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'), - paginationClassName: 'hidden', - }} - /> - {meta && ( -
- -
- )} - - )} -
- ); -}; - -export default ReportExpenseTable; diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx new file mode 100644 index 00000000..704d1f6f --- /dev/null +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; + +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import ReportExpenseTab from './tab/ReportExpenseTab'; + +const ReportExpenseTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Laporan Biaya Operasional', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default ReportExpenseTabs; diff --git a/src/components/pages/report/expense/pdf/ReportExpenseExport.tsx b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx similarity index 99% rename from src/components/pages/report/expense/pdf/ReportExpenseExport.tsx rename to src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx index a7ff8599..6ec2c559 100644 --- a/src/components/pages/report/expense/pdf/ReportExpenseExport.tsx +++ b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx @@ -2,6 +2,7 @@ import { ReportExpense } from '@/types/api/report/report-expense'; import { formatCurrency, formatDate } from '@/lib/helper'; import jsPDF from 'jspdf'; import autoTable, { UserOptions } from 'jspdf-autotable'; + interface jsPDFWithAutoTable extends jsPDF { lastAutoTable: { finalY: number; diff --git a/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx b/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx new file mode 100644 index 00000000..cc27b526 --- /dev/null +++ b/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx @@ -0,0 +1,109 @@ +import * as XLSX from 'xlsx'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { formatCurrency, formatDate } from '@/lib/helper'; + +export const generateReportExpenseExcel = async ( + data: ReportExpense[] +): Promise => { + // Group by supplier + const groupedBySupplier: Record = {}; + data.forEach((item) => { + const supplierName = item.supplier?.name || 'Unknown Supplier'; + if (!groupedBySupplier[supplierName]) { + groupedBySupplier[supplierName] = []; + } + groupedBySupplier[supplierName].push(item); + }); + + const workbook = XLSX.utils.book_new(); + + Object.entries(groupedBySupplier).forEach(([supplierName, supplierData]) => { + const totals = supplierData.reduce( + (acc, item) => ({ + qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0), + total_pengajuan: + acc.total_pengajuan + + (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), + qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0), + total_realisasi: + acc.total_realisasi + + (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), + }), + { + qty_pengajuan: 0, + total_pengajuan: 0, + qty_realisasi: 0, + total_realisasi: 0, + } + ); + + const excelData = supplierData.map((item, index) => ({ + No: index + 1, + 'No. PO': item.po_number || '', + 'No. Referensi': item.reference_number || '', + 'Tanggal Realisasi': item.realization_date + ? formatDate(item.realization_date, 'DD MMM YYYY') + : '', + 'Tanggal Transaksi': item.transaction_date + ? formatDate(item.transaction_date, 'DD MMM YYYY') + : '', + Kategori: item.category || '', + Produk: item.pengajuan?.nonstock?.name || '', + Lokasi: item.kandang?.location?.name || '', + Kandang: item.kandang?.name || '', + 'Qty Pengajuan': item.pengajuan?.qty || 0, + 'Harga Pengajuan': item.pengajuan?.price || 0, + 'Total Pengajuan': + (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), + 'Qty Realisasi': item.realisasi?.qty || 0, + 'Harga Realisasi': item.realisasi?.price || 0, + 'Total Realisasi': + (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), + 'Status Pencairan': item.latest_approval?.step_name || '', + })); + + excelData.push({ + No: 'Total' as unknown as number, + 'No. PO': '', + 'No. Referensi': '', + 'Tanggal Realisasi': '', + 'Tanggal Transaksi': '', + Kategori: '', + Produk: '', + Lokasi: '', + Kandang: '', + 'Qty Pengajuan': totals.qty_pengajuan, + 'Harga Pengajuan': 0, + 'Total Pengajuan': totals.total_pengajuan, + 'Qty Realisasi': totals.qty_realisasi, + 'Harga Realisasi': 0, + 'Total Realisasi': totals.total_realisasi, + 'Status Pencairan': '', + }); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + const colWidths = [ + { wch: 5 }, + { wch: 20 }, + { wch: 20 }, + { wch: 15 }, + { wch: 15 }, + { wch: 15 }, + { wch: 30 }, + { wch: 20 }, + { wch: 15 }, + { wch: 15 }, + { wch: 15 }, + { wch: 20 }, + { wch: 15 }, + { wch: 20 }, + ]; + worksheet['!cols'] = colWidths; + + const sheetName = supplierName.slice(0, 31); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/report/expense/filter/ReportExpenseFilter.ts b/src/components/pages/report/expense/filter/ReportExpenseFilter.ts new file mode 100644 index 00000000..b8bd3c56 --- /dev/null +++ b/src/components/pages/report/expense/filter/ReportExpenseFilter.ts @@ -0,0 +1,73 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type ReportExpenseFilterProps = { + location_id: string | null; + supplier_id: string | null; + kandang_id: string | null; + nonstock_id: string | null; + realization_date: string | null; + category: string | null; +}; + +export type ReportExpenseFilterFormType = { + location_id: OptionType | null; + supplier_id: OptionType | null; + kandang_id: OptionType | null; + nonstock_id: OptionType | null; + realization_date: string | null; + category: OptionType | null; +}; + +export const ReportExpenseFilterSchema = yup.object({ + location_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + supplier_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Supplier wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + kandang_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + nonstock_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Produk wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + realization_date: yup.string().nullable(), + category: yup + .mixed() + .nullable() + .test('is-not-empty', 'Kategori wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), +}) as yup.ObjectSchema; + +export type ReportExpenseFilterValues = yup.InferType< + typeof ReportExpenseFilterSchema +>; diff --git a/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx b/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx deleted file mode 100644 index 65505a5f..00000000 --- a/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { StyleSheet } from '@react-pdf/renderer'; - -const pdfStyles = StyleSheet.create({ - page: { - fontSize: 18, - fontFamily: 'Helvetica', - padding: 20, - backgroundColor: '#FFFFFF', - }, - header: { - marginBottom: 20, - }, - logo: { - width: 120, - height: 30, - marginBottom: 8, - }, - companyInfo: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 4, - color: '#1f74bf', - }, - address: { - fontSize: 7, - color: '#666666', - maxWidth: 400, - marginBottom: 10, - }, - divider: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - marginBottom: 15, - }, - titleSection: { - flexDirection: 'row', - marginBottom: 20, - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - title: { - fontSize: 18, - fontWeight: 'bold', - flex: 3, - color: '#1f74bf', - }, - poInfo: { - flex: 1, - fontSize: 7, - textAlign: 'right', - }, - sectionTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellLast: { - flex: 1, - padding: 3, - fontSize: 7, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellHeaderLast: { - flex: 1, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightLast: { - flex: 1, - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellNarrow: { - width: '1%', - minWidth: 20, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'center', - }, - tableCellNarrowHeader: { - width: '1%', - minWidth: 20, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'center', - }, - tableCellWrap: { - flex: 1, - maxWidth: 80, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - flexWrap: 'wrap', - }, - tableCellWrapHeader: { - flex: 1, - maxWidth: 80, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - // Nested header styles - tableHeaderGroup: { - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - backgroundColor: '#F5F5F5', - }, - tableHeaderGroupLast: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - backgroundColor: '#F5F5F5', - }, - tableHeaderGroupTitle: { - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'center', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - tableSubHeaderRow: { - flexDirection: 'row', - }, - // Specific width columns - tableCellXSmall: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellXSmallHeader: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellSmall: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellSmallHeader: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellMedium: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellMediumHeader: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellRightXSmall: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightSmall: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightMedium: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - grandTotalRow: { - flexDirection: 'row', - borderTopWidth: 1, - borderTopColor: '#000000', - borderTopStyle: 'solid', - }, - grandTotalLabel: { - flex: 3, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - }, - grandTotalValue: { - flex: 1, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'right', - borderRightWidth: 0, - }, - allocationSection: { - marginBottom: 8, - }, - allocationTable: { - borderWidth: 1, - borderColor: '#000000', - }, - innerTable: { - marginTop: 5, - borderWidth: 1, - borderColor: '#000000', - }, - innerRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - innerCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - innerCellLast: { - flex: 1, - padding: 3, - fontSize: 7, - }, - innerCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - innerCellRightLast: { - flex: 1, - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - footer: { - marginTop: 30, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - footerCompany: { - fontSize: 18, - fontWeight: 'bold', - textAlign: 'right', - flex: 1, - color: '#1f74bf', - }, - specialInstructionTable: { - width: '60%', - maxWidth: 300, - borderWidth: 1, - borderColor: '#000000', - flex: 1, - }, -}); - -export default pdfStyles; diff --git a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx new file mode 100644 index 00000000..f78344d7 --- /dev/null +++ b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { ColumnDef } from '@tanstack/react-table'; + +type ReportExpenseColumn = + | ColumnDef + | { + header: string; + columns: Array<{ + header: string; + accessorKey?: string; + cell?: (props: { + row: { original: ReportExpense }; + }) => React.ReactNode; + }>; + }; + +const ReportExpenseSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ReportExpenseColumn[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ReportExpenseSkeleton; diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx new file mode 100644 index 00000000..2581ec5c --- /dev/null +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -0,0 +1,755 @@ +'use client'; + +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { useFormik } from 'formik'; +import { + ReportExpenseFilterSchema, + type ReportExpenseFilterValues, +} from '@/components/pages/report/expense/filter/ReportExpenseFilter'; +import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; +import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; +import Table from '@/components/Table'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { ReportExpenseApi } from '@/services/api/report'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import Modal, { useModal } from '@/components/Modal'; +import Pagination from '@/components/Pagination'; +import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; +import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF'; +import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX'; +import toast from 'react-hot-toast'; +import { + KandangApi, + LocationApi, + NonstockApi, + SupplierApi, +} from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Nonstock } from '@/types/api/master-data/nonstock'; +import { ColumnDef } from '@tanstack/react-table'; +import { httpClient } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; + +interface ReportExpenseTabProps { + tabId: string; +} + +interface FilterParams { + location_id?: string; + supplier_id?: string; + kandang_id?: string; + nonstock_id?: string; + realization_date?: string; + category?: string; + search?: string; +} + +const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + const [filterParams, setFilterParams] = useState({}); + + // ===== PAGINATION STATE ===== + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSuppliers, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setNonstockInputValue, + options: nonstockOptions, + isLoadingOptions: isLoadingNonstocks, + loadMore: loadMoreNonstocks, + } = useSelect(NonstockApi.basePath, 'id', 'name', 'search'); + + const categoryOptions = useMemo( + () => [ + { value: 'BOP', label: 'BOP' }, + { value: 'NON-BOP', label: 'Non BOP' }, + ], + [] + ); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + location_id: null, + supplier_id: null, + kandang_id: null, + nonstock_id: null, + realization_date: null, + category: null, + }, + validationSchema: ReportExpenseFilterSchema, + onSubmit: (values) => { + setFilterParams({ + location_id: values.location_id?.value + ? String(values.location_id.value) + : undefined, + supplier_id: values.supplier_id?.value + ? String(values.supplier_id.value) + : undefined, + kandang_id: values.kandang_id?.value + ? String(values.kandang_id.value) + : undefined, + nonstock_id: values.nonstock_id?.value + ? String(values.nonstock_id.value) + : undefined, + realization_date: values.realization_date || undefined, + category: values.category?.value + ? String(values.category.value) + : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setPage(1); + }, + }); + + // ===== FILTER VALUES ===== + const locationValue = useMemo( + () => formik.values.location_id, + [formik.values.location_id] + ); + const supplierValue = useMemo( + () => formik.values.supplier_id, + [formik.values.supplier_id] + ); + const kandangValue = useMemo( + () => formik.values.kandang_id, + [formik.values.kandang_id] + ); + const nonstockValue = useMemo( + () => formik.values.nonstock_id, + [formik.values.nonstock_id] + ); + const categoryValue = useMemo( + () => formik.values.category, + [formik.values.category] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + if (filterParams.location_id) count += 1; + if (filterParams.supplier_id) count += 1; + if (filterParams.kandang_id) count += 1; + if (filterParams.nonstock_id) count += 1; + if (filterParams.realization_date) count += 1; + if (filterParams.category) count += 1; + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: reportExpenseResponse, isLoading } = useSWR( + isSubmitted + ? () => { + const params = new URLSearchParams(); + if (filterParams.location_id) + params.append('location_id', filterParams.location_id); + if (filterParams.supplier_id) + params.append('supplier_id', filterParams.supplier_id); + if (filterParams.kandang_id) + params.append('kandang_id', filterParams.kandang_id); + if (filterParams.nonstock_id) + params.append('nonstock_id', filterParams.nonstock_id); + if (filterParams.realization_date) + params.append('realization_date', filterParams.realization_date); + if (filterParams.category) + params.append('category', filterParams.category); + params.append('page', String(page)); + params.append('limit', String(pageSize)); + + return [`${ReportExpenseApi.basePath}?${params.toString()}`]; + } + : null, + ([url]: string[]) => httpClient>(url) + ); + + const data: ReportExpense[] = useMemo( + () => + isResponseSuccess(reportExpenseResponse) + ? (reportExpenseResponse.data as ReportExpense[]) || [] + : [], + [reportExpenseResponse] + ); + + const meta = useMemo( + () => + isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta + ? reportExpenseResponse.meta + : null, + [reportExpenseResponse] + ); + + // ===== EXPORT DATA FETCHER ===== + const reportExpenseExport = useCallback(async (): Promise< + ReportExpense[] | null + > => { + const params = new URLSearchParams(); + if (filterParams.location_id) + params.append('location_id', filterParams.location_id); + if (filterParams.supplier_id) + params.append('supplier_id', filterParams.supplier_id); + if (filterParams.kandang_id) + params.append('kandang_id', filterParams.kandang_id); + if (filterParams.nonstock_id) + params.append('nonstock_id', filterParams.nonstock_id); + if (filterParams.realization_date) + params.append('realization_date', filterParams.realization_date); + if (filterParams.category) params.append('category', filterParams.category); + params.append('limit', '100'); + params.append('page', '1'); + + const response = await httpClient>( + `${ReportExpenseApi.basePath}?${params.toString()}` + ); + + return isResponseSuccess(response) ? response.data : null; + }, [filterParams]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await reportExpenseExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateReportExpenseExcel(allDataForExport); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [reportExpenseExport]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allData = await reportExpenseExport(); + if (!allData || allData.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const pdfParams = { + location_name: locationValue?.label, + supplier_name: supplierValue?.label, + realization_date: formik.values.realization_date || undefined, + }; + + await generateReportExpensePDF(allData, pdfParams); + + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + reportExpenseExport, + locationValue, + supplierValue, + kandangValue, + nonstockValue, + categoryValue, + formik.values.realization_date, + ]); + + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
+ + + +
+ + + Export + +
+ + +
+ + } + > + + + +
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + handleExportExcel, + handleExportPDF, + setTabActions, + ]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + // ===== TABLE COLUMNS DEFINITION ===== + const columns = useMemo((): ColumnDef[] => { + return [ + { + header: 'No', + cell: (props) => (page - 1) * pageSize + props.row.index + 1, + }, + { + header: 'No. PO', + accessorKey: 'po_number', + }, + { + header: 'No. Referensi', + accessorKey: 'reference_number', + }, + { + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: ({ row }) => { + return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); + }, + }, + { + header: 'Tanggal Transaksi', + accessorKey: 'transaction_date', + cell: ({ row }) => { + return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); + }, + }, + { + header: 'Kategori', + accessorKey: 'category', + }, + { + header: 'Produk', + accessorFn: (row) => row.pengajuan?.nonstock?.name, + }, + { + header: 'Supplier', + accessorFn: (row) => row.supplier?.name, + }, + { + header: 'Lokasi', + accessorFn: (row) => row.kandang?.location?.name, + }, + { + header: 'Kandang', + accessorFn: (row) => row.kandang?.name, + }, + { + header: 'Pengajuan', + columns: [ + { + header: 'Qty', + id: 'qty_pengajuan', + accessorFn: (row) => row.pengajuan?.qty, + cell: ({ row }) => + row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', + }, + { + header: 'Harga', + id: 'harga_pengajuan', + accessorFn: (row) => row.pengajuan?.price, + cell: ({ row }) => + formatCurrency(row.original.pengajuan?.price || 0), + }, + { + header: 'Total', + id: 'total_pengajuan', + accessorFn: (row) => + (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), + cell: ({ row }) => { + const total = + (row.original.pengajuan?.qty || 0) * + (row.original.pengajuan?.price || 0); + return formatCurrency(total); + }, + }, + ], + }, + { + header: 'Realisasi', + columns: [ + { + header: 'Qty', + id: 'qty_realisasi', + accessorFn: (row) => row.realisasi?.qty, + cell: ({ row }) => + row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', + }, + { + header: 'Harga', + id: 'harga_realisasi', + accessorFn: (row) => row.realisasi?.price, + cell: ({ row }) => + formatCurrency(row.original.realisasi?.price || 0), + }, + { + header: 'Total', + id: 'total_realisasi', + accessorFn: (row) => + (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), + cell: ({ row }) => { + const total = + (row.original.realisasi?.qty || 0) * + (row.original.realisasi?.price || 0); + return formatCurrency(total); + }, + }, + ], + }, + { + header: 'Status Pencairan', + cell: (props) => ( + + ), + }, + { + header: 'Status BOP', + cell: (props) => ( + + ), + }, + ]; + }, [page, pageSize]); + + return ( + <> +
+ {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( +
+ +
+ ) : !data || data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( + <> +
+ {meta && ( +
+ + setPage((currPage) => + currPage > 1 ? currPage - 1 : currPage + ) + } + onNextPage={() => + setPage((currPage) => + meta && meta.total_pages && currPage < meta.total_pages + ? currPage + 1 + : currPage + ) + } + onPageChange={(pageNumber) => setPage(pageNumber)} + rowOptions={[10, 20, 50, 100]} + onRowChange={setPageSize} + /> +
+ )} + + )} + + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+ {/* Modal Body */} +
+ { + formik.setFieldValue('location_id', val); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('kandang_id', val); + }} + onInputChange={setKandangInputValue} + onMenuScrollToBottom={loadMoreKandangs} + isClearable + isDisabled={!formik.values.location_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('supplier_id', val); + }} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('nonstock_id', val); + }} + onInputChange={setNonstockInputValue} + onMenuScrollToBottom={loadMoreNonstocks} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('category', val); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue( + 'realization_date', + e.target.value || null + ); + }} + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ + ); +}; + +export default ReportExpenseTab; From e9da5210addddddab4be15c085775b10fbe5dd7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 10:22:13 +0700 Subject: [PATCH 35/82] refactor(FE): Simplify type definition for ReportExpenseColumn --- .../pages/report/expense/skeleton/ReportExpenseSkeleton.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx index f78344d7..3e13c539 100644 --- a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx +++ b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx @@ -11,9 +11,7 @@ type ReportExpenseColumn = columns: Array<{ header: string; accessorKey?: string; - cell?: (props: { - row: { original: ReportExpense }; - }) => React.ReactNode; + cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode; }>; }; From ec3a0367ddfef1762816ba6d97493c6ab875eb59 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 10:37:12 +0700 Subject: [PATCH 36/82] refactor(FE): Hide delete button if only one item remains in table --- .../table-view/SalesOrderProductTable.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 18f6145b..70282648 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -49,17 +49,19 @@ const SalesOrderProductTable = ({ > - + {data.length > 1 && ( + + )} )} From e8e4f7b877224d0f877f92075200d5f4b3565659 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 15 Feb 2026 08:47:07 +0700 Subject: [PATCH 37/82] refactor(FE): Simplify product filtering logic in RecordingForm --- .../production/recording/form/RecordingForm.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 7ea58f0f..b903a8af 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -484,6 +484,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { location_id: depletionProductsLocationId, kandang_id: depletionProductsKandangId, + type: 'AYAM', }); const today = new Date().toISOString().split('T')[0]; @@ -784,18 +785,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isResponseSuccess(depletionProductsData) && selectedKandang) { const data = depletionProductsData.data as unknown as ProductWarehouse[]; data.forEach((product) => { - const productName = product.product.name; - - if ( - productName.toLowerCase().includes('culling') || - productName.toLowerCase().includes('mati') || - productName.toLowerCase().includes('afkir') - ) { - options.push({ - value: product.id, - label: product.product.name, - }); - } + options.push({ + value: product.id, + label: product.product.name, + }); }); } From 512ccddfc78c0b7b1fad150d20bd8ca9f27d1670 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 09:59:50 +0700 Subject: [PATCH 38/82] refactor(FE): Refactor ChickinForm and ProjectFlockClosingForm components --- .../production/chickin/form/ChickinForm.tsx | 206 ++++++++--------- .../closing/ProjectFlockClosingForm.tsx | 216 ++++++++++-------- 2 files changed, 214 insertions(+), 208 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index bd3ff57c..c28b456d 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -16,6 +16,7 @@ import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/Chi import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { Icon } from '@iconify/react'; import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; import RequirePermission from '@/components/helper/RequirePermission'; import { BaseApproval } from '@/types/api/api-general'; @@ -53,135 +54,126 @@ const ChickinFormKandang = ({ }; return ( -
+
+ {/* Header */} - {/* Informasi Kandang */} -
-
-

Informasi Kandang

+ {approvals && !approvalsLoading && ( + + )} - {approvals && !approvalsLoading && ( -
- -
- )} + {/* Informasi Kandang */} +
+

+ Informasi Kandang +

{/* Badge Row */}
- - {' '} - Aktif - + text='Aktif' + className={{ badge: 'w-fit text-nowrap' }} + />
- - - {` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} - + text={` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} + className={{ badge: 'w-fit text-nowrap' }} + />
- {/* Information Grid */} -
- {/* Area */} -
- Area + {/* Information Card */} + +
+
+
+ {' '} + Area +
+
+ {initialValues.project_flock.area.name} +
+
+
+
+ {' '} + Lokasi +
+
+ {initialValues.project_flock?.location.name} +
+
+
+
+ {' '} + Kandang +
+
+ {initialValues.kandang.name} +
+
+
+
+ {' '} + Jumlah DOC +
+
+ {formatNumber( + initialValues.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
-
- {initialValues.project_flock.area.name} -
- - {/* Lokasi */} -
- Lokasi -
-
- {initialValues.project_flock?.location.name} -
- - {/* Kandang */} -
- Kandang -
-
{initialValues.kandang.name}
- - {/* Jumlah DOC */} -
- Jumlah DOC -
-
- {formatNumber( - initialValues.chickins?.reduce( - (total, chickin) => total + chickin.usage_qty, - 0 - ) ?? 0 - )}{' '} - Ekor -
-
+
-
-
-

Informasi Chick In

+ {/* Informasi Chick In */} +
+

+ Informasi Chick In +

{/* Badge Row */}
- - {' '} - Perlu Chick In ({initialValues.available_qtys?.length ?? 0}) - +
- setOpenChickin(!openChickin)} - > - {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} - - + text={ + <> + {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} + + + } + className={{ badge: 'w-fit text-nowrap cursor-pointer' }} + />
{openChickin && ( @@ -198,7 +190,7 @@ const ChickinFormKandang = ({ afterSubmit={afterSubmitFormChickin} /> -
+ ); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index aab21172..162eb6a2 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -1,10 +1,12 @@ 'use client'; import Button from '@/components/Button'; +import Card from '@/components/Card'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; -import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; +import StatusBadge from '@/components/helper/StatusBadge'; +import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ClosingExpense, @@ -20,7 +22,6 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; -import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ApprovalApi } from '@/services/api/approval'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -78,112 +79,113 @@ const ProjectFlockClosingForm = ({ closeModal.closeModal(); }; - const errorStock = useMemo(() => { - return isResponseSuccess(closingData) - ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) - : true; - }, [closingData]); + // const errorStock = useMemo(() => { + // return isResponseSuccess(closingData) + // ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) + // : true; + // }, [closingData]); - const errorExpense = useMemo(() => { - return isResponseSuccess(closingData) - ? closingData?.data?.expenses.every((expense) => expense.step < 5) - : true; - }, [closingData]); + // const errorExpense = useMemo(() => { + // return isResponseSuccess(closingData) + // ? closingData?.data?.expenses.every((expense) => expense.step < 5) + // : true; + // }, [closingData]); const isCanCloseValid = true; return ( <> -
+
+ {/* Header */} + leftIconClassName='hover:text-gray-400' + subtitle='Close Flock' + className='sticky top-0 z-10 bg-base-100' + /> {/* Informasi Kandang */} -
-
-

Informasi Kandang

+
+

+ Informasi Kandang +

{/* Badge Row */}
- - {' '} - Aktif - + text='Aktif' + className={{ badge: 'w-fit text-nowrap' }} + />
- - - {` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`} - + text={` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`} + className={{ badge: 'w-fit text-nowrap' }} + />
- {/* Information Grid */} -
- {/* Area */} -
- Area + {/* Information Card */} + +
+
+
+ {' '} + Area +
+
+ {projectFlock.area?.name} +
+
+
+
+ {' '} + Lokasi +
+
+ {projectFlock.location?.name} +
+
+
+
+ {' '} + Kandang +
+
+ {projectFlockKandang.kandang?.name} +
+
+
+
+ {' '} + Jumlah DOC +
+
+ {formatNumber( + projectFlockKandang.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
-
{projectFlock.area?.name}
- - {/* Lokasi */} -
- Lokasi -
-
{projectFlock.location?.name}
- - {/* Kandang */} -
- Kandang -
-
- {projectFlockKandang.kandang?.name} -
- - {/* Jumlah DOC */} -
- Jumlah - DOC -
-
- {formatNumber( - projectFlockKandang.chickins?.reduce( - (total, chickin) => total + chickin.usage_qty, - 0 - ) ?? 0 - )}{' '} - Ekor -
-
+
{/* Table Biaya */} -
-
-

Biaya

+
+

+ Biaya +

data={ isResponseSuccess(closingData) ? closingData.data?.expenses : [] @@ -192,6 +194,16 @@ const ProjectFlockClosingForm = ({ { header: 'PO Number', accessorKey: 'po_number', + cell(props) { + return props.row.original.po_number || '-'; + }, + }, + { + header: 'Ref Number', + accessorKey: 'reference_number', + cell(props) { + return props.row.original.reference_number || '-'; + }, }, { header: 'Total', @@ -208,11 +220,11 @@ const ProjectFlockClosingForm = ({ }} variant='soft' color={ - props.row.original.step < 5 - ? props.row.original.step == 1 + props.row.original.step === 6 + ? 'success' + : props.row.original.step === 1 ? 'neutral' - : 'success' - : 'error' + : 'warning' } > {formatTitleCase(props.row.original.step_name)} @@ -222,13 +234,13 @@ const ProjectFlockClosingForm = ({ }, ]} className={{ - containerClassName: cn('my-4'), + containerClassName: 'mb-0', tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', tableClassName: 'font-inter w-full table-sm min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', + headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', paginationClassName: 'hidden', @@ -242,9 +254,10 @@ const ProjectFlockClosingForm = ({
{/* Table Persediaan Gudang */} -
-
-

Persediaan Gudang

+
+

+ Persediaan Gudang +

data={ isResponseSuccess(closingData) @@ -270,13 +283,13 @@ const ProjectFlockClosingForm = ({ }, ]} className={{ - containerClassName: cn('my-4'), + containerClassName: 'mb-0', tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', tableClassName: 'font-inter w-full table-sm min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', + headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', paginationClassName: 'hidden', @@ -289,10 +302,11 @@ const ProjectFlockClosingForm = ({ )} */}
-
+
+ ); }; From 9d6cc901629052e507e33273ca5b4fb7a7a4361c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 11:52:30 +0700 Subject: [PATCH 39/82] refactor(FE): Update table columns and improve UI for Project Flock pages --- .../closing/ProjectFlockClosingForm.tsx | 20 +++++--- .../detail/ProjectFlockDetail.tsx | 48 ++++++++++++++----- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 162eb6a2..da54e4d0 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -191,13 +191,6 @@ const ProjectFlockClosingForm = ({ isResponseSuccess(closingData) ? closingData.data?.expenses : [] } columns={[ - { - header: 'PO Number', - accessorKey: 'po_number', - cell(props) { - return props.row.original.po_number || '-'; - }, - }, { header: 'Ref Number', accessorKey: 'reference_number', @@ -205,9 +198,19 @@ const ProjectFlockClosingForm = ({ return props.row.original.reference_number || '-'; }, }, + { + header: 'PO Number', + accessorKey: 'po_number', + cell(props) { + return props.row.original.po_number || '-'; + }, + }, { header: 'Total', accessorKey: 'total', + cell(props) { + return formatNumber(props.row.original.total); + }, }, { header: 'Status', @@ -276,6 +279,9 @@ const ProjectFlockClosingForm = ({ { header: 'Quantity', accessorKey: 'quantity', + cell(props) { + return formatNumber(props.row.original.quantity); + }, }, { header: 'UOM', diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 47491dfa..c2b8e903 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -226,15 +226,37 @@ const ProjectFlockDetail = ({

- Kandang Aktif + Kandang

-
- +
+ {projectFlock.kandangs?.filter( + (kandang) => kandang.status !== 'NON_ACTIVE' + ).length > 0 && ( + kandang.status !== 'NON_ACTIVE' + ).length ?? 0 + })`} + className={{ badge: 'w-fit' }} + /> + )} + + {projectFlock.kandangs?.filter( + (kandang) => kandang.status === 'NON_ACTIVE' + ).length > 0 && ( + kandang.status === 'NON_ACTIVE' + ).length ?? 0 + })`} + className={{ badge: 'w-fit' }} + /> + )} } - className={{ badge: 'w-fit text-nowrap cursor-pointer' }} + className={{ badge: 'w-fit cursor-pointer' }} />
@@ -368,10 +390,12 @@ const ProjectFlockDetail = ({ - +
+ +
From da27f4c5816f9ccf27b58b22ae6ef79ac1647984 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 13:15:06 +0700 Subject: [PATCH 40/82] refactor(FE): Refactor status badge logic in ProjectFlockClosingForm --- .../closing/ProjectFlockClosingForm.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index da54e4d0..860a426a 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -24,6 +24,26 @@ import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; import { ApprovalApi } from '@/services/api/approval'; import RequirePermission from '@/components/helper/RequirePermission'; +import { Color } from '@/types/theme'; + +const getExpenseStatusBadgeColor = (step: number): Color => { + switch (step) { + case 1: + return 'neutral'; + case 2: + return 'info'; + case 3: + return 'warning'; + case 4: + return 'error'; + case 5: + return 'warning'; + case 6: + return 'success'; + default: + return 'neutral'; + } +}; const ProjectFlockClosingForm = ({ projectFlock, @@ -217,21 +237,15 @@ const ProjectFlockClosingForm = ({ accessorKey: 'status', cell(props) { return ( - - {formatTitleCase(props.row.original.step_name)} - + /> ); }, }, From d4c6a05c0cbbcd1cbbdda639e7be80820e896211 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 13:21:29 +0700 Subject: [PATCH 41/82] refactor(FE): Update button behavior based on kandang status --- .../detail/ProjectFlockDetail.tsx | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index c2b8e903..1ad3f983 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -398,24 +398,26 @@ const ProjectFlockDetail = ({
- - - - - + + + + )} - Close + {selectedKandang?.status === 'NON_ACTIVE' ? ( + <> + Unclose + + ) : ( + <> + Close + + )} From ed576fc8eb4d3549db47be462d08710341c6f744 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 13:49:02 +0700 Subject: [PATCH 42/82] feat(FE): Improve empty state handling and add "Unclose Flock" functionality --- src/components/Table.tsx | 22 +++++++++++---- .../closing/ProjectFlockClosingForm.tsx | 28 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b40d9db5..d9d81543 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({ })); const emptyContentDefaultValue = ( -
- +
+ Tidak ada data yang dapat ditampilkan...
@@ -452,6 +452,20 @@ const Table = ({ ); })} + + {(data.length === 0 || table.getRowModel().rows.length === 0) && + !isLoading && ( +
+ + + )} ({
+ {emptyContent} +
- {(data.length === 0 || table.getRowModel().rows.length === 0) && - !isLoading && - emptyContent} - {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 860a426a..996425a0 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -70,6 +70,10 @@ const ProjectFlockClosingForm = ({ ) ); + const isKandangClosed = useMemo(() => { + return projectFlockKandang.kandang?.status === 'NON_ACTIVE'; + }, [projectFlockKandang]); + const isCanClose = useMemo(() => { return isResponseSuccess(projectFlockKandangApprovals) ? projectFlockKandangApprovals?.data?.[0]?.step_number <= 2 @@ -81,8 +85,10 @@ const ProjectFlockClosingForm = ({ const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( projectFlockKandang?.id as number, { - closed_date: isCanClose ? formatDate(new Date(), 'YYYY-MM-DD') : '', - action: isCanClose ? 'close' : 'unclose', + closed_date: !isKandangClosed + ? formatDate(new Date(), 'YYYY-MM-DD') + : '', + action: !isKandangClosed ? 'close' : 'unclose', } ); @@ -121,7 +127,7 @@ const ProjectFlockClosingForm = ({ leftIcon='heroicons:chevron-left' leftIconHref={`/production/project-flock/detail?projectFlockId=${projectFlock.id}`} leftIconClassName='hover:text-gray-400' - subtitle='Close Flock' + subtitle={isKandangClosed ? 'Unclose Flock' : 'Close Flock'} className='sticky top-0 z-10 bg-base-100' /> @@ -134,8 +140,8 @@ const ProjectFlockClosingForm = ({ {/* Badge Row */}
@@ -332,8 +338,14 @@ const ProjectFlockClosingForm = ({ disabled={!isCanCloseValid} onClick={() => closeModal.openModal()} > - {' '} - {isCanClose ? 'Close' : 'Unclose'} + {' '} + {isKandangClosed ? 'Unclose' : 'Close'}
@@ -342,7 +354,7 @@ const ProjectFlockClosingForm = ({ ref={closeModal.ref} type='error' text={ - isCanClose + !isKandangClosed ? 'Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai' : 'Apakah kamu yakin ingin membuka kembali project ini ? *Project ini akan kembali ke status aktif' } From e94967ea4c3c35938dd742c062941f69a94e75d6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 14:28:55 +0700 Subject: [PATCH 43/82] refactor(FE): Adjust grid column layout based on selectedKandang status --- .../project-flock/detail/ProjectFlockDetail.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 1ad3f983..44e5cf98 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -397,7 +397,13 @@ const ProjectFlockDetail = ({ /> -
+
{selectedKandang?.status !== 'NON_ACTIVE' && ( Date: Wed, 18 Feb 2026 14:33:28 +0700 Subject: [PATCH 44/82] refactor(FE): Replace Badge with StatusBadge in ProjectFlockDetail --- .../project-flock/detail/ProjectFlockDetail.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 44e5cf98..d817c69b 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -377,13 +377,13 @@ const ProjectFlockDetail = ({ disabled={projectFlock?.approval?.step_number == 1} />
- - Kapasitas {kandang?.capacity} Ekor - + Kapasitas {kandang?.capacity} Ekor} + className={{ badge: 'w-fit text-nowrap' }} + />
))} From 15289951e6f6dc9b18d106793e29d902d2a0d9d7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 14:42:41 +0700 Subject: [PATCH 45/82] refactor(FE): Remove unused CSS classes from table components --- .../project-flock/closing/ProjectFlockClosingForm.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 996425a0..7ffe7b24 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -258,15 +258,14 @@ const ProjectFlockClosingForm = ({ ]} className={{ containerClassName: 'mb-0', - tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', - tableClassName: 'font-inter w-full table-sm min-h-full!', + tableWrapperClassName: 'overflow-x-auto max-w-120', + tableClassName: 'font-inter w-full table-sm', headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', }} /> {/* {errorExpense && ( @@ -310,15 +309,14 @@ const ProjectFlockClosingForm = ({ ]} className={{ containerClassName: 'mb-0', - tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', - tableClassName: 'font-inter w-full table-sm min-h-full!', + tableWrapperClassName: 'overflow-x-auto max-w-120', + tableClassName: 'font-inter w-full table-sm', headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', }} /> {/* {errorStock && ( From 02165df89ccf6c8b5671d6bf88371487cad13f0a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 14:50:11 +0700 Subject: [PATCH 46/82] refactor(FE): Update status badge text to use English labels --- .../project-flock/closing/ProjectFlockClosingForm.tsx | 2 +- .../production/project-flock/detail/ProjectFlockDetail.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 7ffe7b24..f963a793 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -141,7 +141,7 @@ const ProjectFlockClosingForm = ({
diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index d817c69b..db3d45aa 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -235,7 +235,7 @@ const ProjectFlockDetail = ({ ).length > 0 && ( kandang.status !== 'NON_ACTIVE' ).length ?? 0 @@ -249,7 +249,7 @@ const ProjectFlockDetail = ({ ).length > 0 && ( kandang.status === 'NON_ACTIVE' ).length ?? 0 From 2169c0ea6298beaae88dae93be4f2589ef4b3bb0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 15:00:26 +0700 Subject: [PATCH 47/82] refactor(FE): Update StatusBadge color and text in ChickinForm --- src/components/pages/production/chickin/form/ChickinForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index c28b456d..b9c73934 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -77,8 +77,8 @@ const ChickinFormKandang = ({ {/* Badge Row */}
From d085b18788e2b745d6062916335bb3ed8aa36ff8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 15:40:06 +0700 Subject: [PATCH 48/82] refactor(FE): Refactor folder structure for closing-related components --- src/components/pages/closing/ClosingDetail.tsx | 12 ++++++------ src/components/pages/closing/ClosingsTable.tsx | 2 +- .../closing/{ => finance}/ClosingFinanceTable.tsx | 0 .../closing/{ => overhead}/ClosingOverheadTable.tsx | 0 .../ClosingSapronakCalculationTable.tsx | 0 .../ClosingIncomingSapronaksSummaryTable.tsx | 0 .../{ => sapronak}/ClosingIncomingSapronaksTable.tsx | 0 .../ClosingOutgoingSapronaksSummaryTable.tsx | 0 .../{ => sapronak}/ClosingOutgoingSapronaksTable.tsx | 0 .../closing/{ => tab}/ClosingFinanceTabContent.tsx | 2 +- .../closing/{ => tab}/ClosingOverheadTabContent.tsx | 2 +- .../{ => tab}/ClosingProductionDataTabContent.tsx | 0 .../ClosingSapronakCalculationTabContent.tsx | 2 +- .../closing/{ => tab}/ClosingSapronakTabContent.tsx | 8 ++++---- 14 files changed, 14 insertions(+), 14 deletions(-) rename src/components/pages/closing/{ => finance}/ClosingFinanceTable.tsx (100%) rename src/components/pages/closing/{ => overhead}/ClosingOverheadTable.tsx (100%) rename src/components/pages/closing/{ => sapronak-calculation}/ClosingSapronakCalculationTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingIncomingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingIncomingSapronaksTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingOutgoingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingOutgoingSapronaksTable.tsx (100%) rename src/components/pages/closing/{ => tab}/ClosingFinanceTabContent.tsx (77%) rename src/components/pages/closing/{ => tab}/ClosingOverheadTabContent.tsx (89%) rename src/components/pages/closing/{ => tab}/ClosingProductionDataTabContent.tsx (100%) rename src/components/pages/closing/{ => tab}/ClosingSapronakCalculationTabContent.tsx (92%) rename src/components/pages/closing/{ => tab}/ClosingSapronakTabContent.tsx (77%) diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index c3c91a5a..51f7618d 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -6,17 +6,17 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; -import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent'; -import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent'; +import ClosingSapronakTabContent from '@/components/pages/closing/tab/ClosingSapronakTabContent'; +import ClosingProductionDataTabContent from '@/components/pages/closing/tab/ClosingProductionDataTabContent'; import { ClosingGeneralInformation, BaseClosingSales, ClosingHppExpedition, } from '@/types/api/closing'; -import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; -import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; -import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent'; +import ClosingSapronakCalculationTabContent from '@/components/pages/closing/tab/ClosingSapronakCalculationTabContent'; +import ClosingOverheadTabContent from '@/components/pages/closing/tab/ClosingOverheadTabContent'; +import ClosingFinanceTabContent from '@/components/pages/closing/tab/ClosingFinanceTabContent'; import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable'; import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; @@ -96,7 +96,7 @@ const ClosingDetail: React.FC = ({ return ( <> -
+
diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/table/ClosingGeneralInformationTable.tsx similarity index 100% rename from src/components/pages/closing/ClosingGeneralInformationTable.tsx rename to src/components/pages/closing/table/ClosingGeneralInformationTable.tsx diff --git a/src/stores/closing/closing-tab.store.ts b/src/stores/closing/closing-tab.store.ts new file mode 100644 index 00000000..1f81c26a --- /dev/null +++ b/src/stores/closing/closing-tab.store.ts @@ -0,0 +1,21 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { + createClosingTabSlice, + ClosingTabSlice, +} from '@/stores/closing/slices/closing-tab.slice'; + +export type ClosingTabStore = ClosingTabSlice; + +export const useClosingTabStore = create()( + devtools( + (...args) => ({ + ...createClosingTabSlice(...args), + }), + { + name: 'ClosingTabStore', + } + ) +); diff --git a/src/stores/closing/slices/closing-tab.slice.ts b/src/stores/closing/slices/closing-tab.slice.ts new file mode 100644 index 00000000..cd47bbdc --- /dev/null +++ b/src/stores/closing/slices/closing-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ClosingTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createClosingTabSlice: StateCreator< + ClosingTabSlice, + [], + [], + ClosingTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); From 32354e3c2df7cb1ce66b4573675bf9d22c19dac0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 16:32:26 +0700 Subject: [PATCH 52/82] refactor(FE): Adjust padding on tab header wrapper in ClosingDetailTabs --- src/components/pages/closing/ClosingDetailTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 2ec3fe1e..2a5fa638 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -136,7 +136,7 @@ const ClosingDetail: React.FC = ({ variant='boxed' className={{ tabHeaderWrapper: - 'justify-between items-center p-3 border-b border-base-content/10', + 'justify-between items-center py-3 border-b border-base-content/10', tab: 'w-fit', content: 'p-0 m-0', }} From 0235494d46c28ca408061243a564d34d1b13d479 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:29:01 +0700 Subject: [PATCH 53/82] refactor(FE): Refactor HPP Expedition handling in ClosingDetailPage --- src/app/closing/detail/page.tsx | 21 --------- .../pages/closing/ClosingDetailTabs.tsx | 9 +--- .../closing/tab/HppExpeditionClosingTab.tsx | 19 ++++++++ .../table/HppExpeditionClosingTable.tsx | 43 ++++++++++++++----- 4 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 src/components/pages/closing/tab/HppExpeditionClosingTab.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 8f164f44..d081951c 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -46,21 +46,6 @@ const ClosingDetailPage = () => { : ClosingApi.getPenjualan(Number(closingId)) ); - const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( - kandangId - ? `hpp-ekspedisi-${closingId}-${kandangId}` - : closingId - ? `hpp-ekspedisi-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getHppEkspedisiByKandang( - Number(closingId), - Number(kandangId) - ) - : ClosingApi.getHppEkspedisi(Number(closingId)) - ); - if (!closingId) { router.back(); @@ -79,7 +64,6 @@ const ClosingDetailPage = () => { const isLoading = isLoadingClosing || isLoadingSales || - isLoadingHppEkspedisi || isLoadingProject || isLoadingKandang; @@ -92,11 +76,6 @@ const ClosingDetailPage = () => { id={Number(closingId)} initialValue={closing.data} salesData={isResponseSuccess(salesData) ? salesData.data : undefined} - hppExpeditionData={ - isResponseSuccess(hppEkspedisiData) - ? hppEkspedisiData.data - : undefined - } projectData={ isResponseSuccess(projectData) ? projectData.data : undefined } diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 2a5fa638..94340283 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -12,13 +12,12 @@ import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionD import { ClosingGeneralInformation, BaseClosingSales, - ClosingHppExpedition, } from '@/types/api/closing'; import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab'; import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab'; import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab'; import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable'; -import HppExpeditionClosingTable from './table/HppExpeditionClosingTable'; +import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab'; import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; @@ -27,7 +26,6 @@ interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; salesData?: BaseClosingSales; - hppExpeditionData?: ClosingHppExpedition; projectData?: ProjectFlock; kandangData?: ProjectFlockKandang; } @@ -36,7 +34,6 @@ const ClosingDetail: React.FC = ({ id, initialValue, salesData, - hppExpeditionData, projectData, kandangData, }) => { @@ -79,9 +76,7 @@ const ClosingDetail: React.FC = ({ { id: 'hppEkspedisi', label: 'HPP Ekspedisi', - content: ( - - ), + content: , }, { id: 'dataProduksi', diff --git a/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx new file mode 100644 index 00000000..ad7f0ec1 --- /dev/null +++ b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx @@ -0,0 +1,19 @@ +import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable'; + +interface HppExpeditionClosingTabProps { + projectFlockId: number; +} + +const HppExpeditionClosingTab = ({ + projectFlockId, +}: HppExpeditionClosingTabProps) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default HppExpeditionClosingTab; diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index 2229180e..6c19c2a8 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -5,27 +5,49 @@ import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; import { formatCurrency } from '@/lib/helper'; -import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; interface HppExpeditionClosingTableProps { - type?: 'detail'; - initialValues?: BaseHppExpedition; + projectFlockId: number; } const HppExpeditionClosingTable = ({ - initialValues, + projectFlockId, }: HppExpeditionClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: hppExpedition, isLoading } = useSWR( + kandangId + ? `/closing/hpp-expedition/${projectFlockId}/${kandangId}` + : `/closing/hpp-expedition/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getHppEkspedisiByKandang(projectFlockId, Number(kandangId)) + : ClosingApi.getHppEkspedisi(projectFlockId) + ); + const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { - return initialValues?.expedition_costs || []; - }, [initialValues]); + if (isResponseSuccess(hppExpedition)) { + return hppExpedition.data.expedition_costs || []; + } + return []; + }, [hppExpedition]); const totals = useMemo(() => { - const totalHpp = initialValues?.total_hpp_amount || 0; - + if (isResponseSuccess(hppExpedition)) { + return { + totalHpp: hppExpedition.data.total_hpp_amount || 0, + }; + } return { - totalHpp, + totalHpp: 0, }; - }, [initialValues]); + }, [hppExpedition]); const costOfRevenueExpeditionColumns: ColumnDef[] = useMemo( @@ -81,6 +103,7 @@ const HppExpeditionClosingTable = ({ 0} className={{ tableWrapperClassName: 'overflow-x-auto', From d9bd73d8c1e88eee02fad0bc1a2954bdbf6daa24 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:32:33 +0700 Subject: [PATCH 54/82] refactor(FE): Refactor sales data fetching and component structure --- src/app/closing/detail/page.tsx | 18 +------- .../pages/closing/ClosingDetailTabs.tsx | 9 ++-- .../pages/closing/tab/SalesClosingTab.tsx | 19 +++++++++ .../pages/closing/table/SalesClosingTable.tsx | 42 +++++++++++++++---- 4 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 src/components/pages/closing/tab/SalesClosingTab.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index d081951c..c8d5c47e 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -34,18 +34,6 @@ const ClosingDetailPage = () => { () => ProjectFlockKandangApi.getSingle(Number(kandangId)) ); - const { data: salesData, isLoading: isLoadingSales } = useSWR( - kandangId - ? `sales-${closingId}-${kandangId}` - : closingId - ? `sales-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId)) - : ClosingApi.getPenjualan(Number(closingId)) - ); - if (!closingId) { router.back(); @@ -62,10 +50,7 @@ const ClosingDetailPage = () => { } const isLoading = - isLoadingClosing || - isLoadingSales || - isLoadingProject || - isLoadingKandang; + isLoadingClosing || isLoadingProject || isLoadingKandang; return (
@@ -75,7 +60,6 @@ const ClosingDetailPage = () => { = ({ id, initialValue, - salesData, projectData, kandangData, }) => { @@ -60,7 +57,7 @@ const ClosingDetail: React.FC = ({ { id: 'penjualan', label: 'Penjualan', - content: , + content: , }, { id: 'overhead', @@ -91,7 +88,7 @@ const ClosingDetail: React.FC = ({ ]; return validTabs; - }, [initialValue, salesData, kandangData, id]); + }, [initialValue, kandangData, id]); return ( <> diff --git a/src/components/pages/closing/tab/SalesClosingTab.tsx b/src/components/pages/closing/tab/SalesClosingTab.tsx new file mode 100644 index 00000000..23ec720b --- /dev/null +++ b/src/components/pages/closing/tab/SalesClosingTab.tsx @@ -0,0 +1,19 @@ +import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable'; + +interface SalesClosingTabProps { + projectFlockId: number; +} + +const SalesClosingTab = ({ + projectFlockId, +}: SalesClosingTabProps) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default SalesClosingTab; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index b30cfb4a..6ad716b2 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -5,6 +5,7 @@ import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; import { BaseClosingSales, BaseSales, @@ -13,20 +14,46 @@ import { import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; interface SalesClosingTableProps { - type?: 'detail'; - initialValues?: BaseClosingSales; + projectFlockId: number; } -const SalesClosingTable = ({ initialValues }: SalesClosingTableProps) => { +const SalesClosingTable = ({ + projectFlockId, +}: SalesClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: sales, isLoading } = useSWR( + kandangId + ? `/closing/sales/${projectFlockId}/${kandangId}` + : `/closing/sales/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getPenjualanByKandang( + projectFlockId, + Number(kandangId) + ) + : ClosingApi.getPenjualan(projectFlockId) + ); + const salesData: BaseSales[] = useMemo(() => { - return initialValues?.sales || []; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.sales || []; + } + return []; + }, [sales]); const summary: ClosingSalesSummary | undefined = useMemo(() => { - return initialValues?.summary; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.summary; + } + return undefined; + }, [sales]); const totals = useMemo(() => { if (salesData.length === 0) { @@ -306,6 +333,7 @@ const SalesClosingTable = ({ initialValues }: SalesClosingTableProps) => {
0} className={{ tableWrapperClassName: 'overflow-x-auto', From 1fe722cb81362f371d3e251f2fdf991a993b88ea Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:33:31 +0700 Subject: [PATCH 55/82] refactor(FE): Refactor code formatting for consistency and readability --- src/app/closing/detail/page.tsx | 3 +-- src/components/pages/closing/ClosingDetailTabs.tsx | 4 +--- src/components/pages/closing/tab/SalesClosingTab.tsx | 8 ++------ src/components/pages/closing/table/SalesClosingTable.tsx | 9 ++------- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index c8d5c47e..96487258 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -49,8 +49,7 @@ const ClosingDetailPage = () => { return; } - const isLoading = - isLoadingClosing || isLoadingProject || isLoadingKandang; + const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang; return (
diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 3b9e7dce..59fea1ba 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -9,9 +9,7 @@ import ClosingGeneralInformationTable from '@/components/pages/closing/table/Clo import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab'; import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab'; -import { - ClosingGeneralInformation, -} from '@/types/api/closing'; +import { ClosingGeneralInformation } from '@/types/api/closing'; import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab'; import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab'; import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab'; diff --git a/src/components/pages/closing/tab/SalesClosingTab.tsx b/src/components/pages/closing/tab/SalesClosingTab.tsx index 23ec720b..ee343da0 100644 --- a/src/components/pages/closing/tab/SalesClosingTab.tsx +++ b/src/components/pages/closing/tab/SalesClosingTab.tsx @@ -4,14 +4,10 @@ interface SalesClosingTabProps { projectFlockId: number; } -const SalesClosingTab = ({ - projectFlockId, -}: SalesClosingTabProps) => { +const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => { return (
- {projectFlockId && ( - - )} + {projectFlockId && }
); }; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index 6ad716b2..0d8a31ba 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -22,9 +22,7 @@ interface SalesClosingTableProps { projectFlockId: number; } -const SalesClosingTable = ({ - projectFlockId, -}: SalesClosingTableProps) => { +const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { const searchParams = useSearchParams(); const kandangId = searchParams.get('kandangId'); @@ -34,10 +32,7 @@ const SalesClosingTable = ({ : `/closing/sales/${projectFlockId}`, () => kandangId - ? ClosingApi.getPenjualanByKandang( - projectFlockId, - Number(kandangId) - ) + ? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId)) : ClosingApi.getPenjualan(projectFlockId) ); From c53430fa1fec86f2186db2227b23ae7780159072 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:35:42 +0700 Subject: [PATCH 56/82] refactor(FE): Organize Sapronak tables into a dedicated folder --- src/components/pages/closing/tab/SapronakClosingTab.tsx | 8 ++++---- .../{ => sapronak}/IncomingSapronaksSummaryTable.tsx | 0 .../table/{ => sapronak}/IncomingSapronaksTable.tsx | 0 .../{ => sapronak}/OutgoingSapronaksSummaryTable.tsx | 0 .../table/{ => sapronak}/OutgoingSapronaksTable.tsx | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/components/pages/closing/table/{ => sapronak}/IncomingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/table/{ => sapronak}/IncomingSapronaksTable.tsx (100%) rename src/components/pages/closing/table/{ => sapronak}/OutgoingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/table/{ => sapronak}/OutgoingSapronaksTable.tsx (100%) diff --git a/src/components/pages/closing/tab/SapronakClosingTab.tsx b/src/components/pages/closing/tab/SapronakClosingTab.tsx index 8f931671..1a5d26dd 100644 --- a/src/components/pages/closing/tab/SapronakClosingTab.tsx +++ b/src/components/pages/closing/tab/SapronakClosingTab.tsx @@ -1,9 +1,9 @@ 'use client'; -import IncomingSapronaksTable from '@/components/pages/closing/table/IncomingSapronaksTable'; -import OutgoingSapronaksTable from '@/components/pages/closing/table/OutgoingSapronaksTable'; -import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/IncomingSapronaksSummaryTable'; -import ClosingOutgoingSapronaksSummaryTable from '../table/OutgoingSapronaksSummaryTable'; +import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable'; +import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable'; +import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable'; +import ClosingOutgoingSapronaksSummaryTable from '../table/sapronak/OutgoingSapronaksSummaryTable'; interface SapronakClosingTabProps { projectFlockId?: number; diff --git a/src/components/pages/closing/table/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx similarity index 100% rename from src/components/pages/closing/table/IncomingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx diff --git a/src/components/pages/closing/table/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx similarity index 100% rename from src/components/pages/closing/table/IncomingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx diff --git a/src/components/pages/closing/table/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx similarity index 100% rename from src/components/pages/closing/table/OutgoingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx diff --git a/src/components/pages/closing/table/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx similarity index 100% rename from src/components/pages/closing/table/OutgoingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx From 9c953ca3825a873aace5a54deade8f1967d8fd5d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:37:54 +0700 Subject: [PATCH 57/82] refactor(FE): Fix incorrect import and component usage in SapronakClosingTab --- src/components/pages/closing/tab/SapronakClosingTab.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/tab/SapronakClosingTab.tsx b/src/components/pages/closing/tab/SapronakClosingTab.tsx index 1a5d26dd..21bb3b3f 100644 --- a/src/components/pages/closing/tab/SapronakClosingTab.tsx +++ b/src/components/pages/closing/tab/SapronakClosingTab.tsx @@ -3,7 +3,7 @@ import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable'; import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable'; import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable'; -import ClosingOutgoingSapronaksSummaryTable from '../table/sapronak/OutgoingSapronaksSummaryTable'; +import OutgoingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable'; interface SapronakClosingTabProps { projectFlockId?: number; @@ -20,9 +20,7 @@ const SapronakClosingTab = ({ projectFlockId }: SapronakClosingTabProps) => { - + )}
From 8fe19feaac1039f6e47ce8c6a4d0d6bdbbbf81f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:03:25 +0700 Subject: [PATCH 58/82] feat(FE): Add skeleton components for closing pages --- .../closing/skeleton/ClosingTabSkeleton.tsx | 36 + .../skeleton/FinanceClosingSkeleton.tsx | 40 ++ .../skeleton/HppExpeditionClosingSkeleton.tsx | 44 ++ .../skeleton/OverheadClosingSkeleton.tsx | 72 ++ .../ProductionDataClosingSkeleton.tsx | 33 + .../closing/skeleton/SalesClosingSkeleton.tsx | 84 +++ .../SapronakCalculationClosingSkeleton.tsx | 72 ++ .../skeleton/SapronakClosingSkeleton.tsx | 126 ++++ .../closing/tab/ProductionDataClosingTab.tsx | 15 +- .../closing/table/FinanceClosingTable.tsx | 619 +++++++++--------- .../table/HppExpeditionClosingTable.tsx | 56 +- .../closing/table/OverheadClosingTable.tsx | 178 ++--- .../pages/closing/table/SalesClosingTable.tsx | 52 +- .../table/SapronakCalculationClosingTable.tsx | 124 ++-- .../IncomingSapronaksSummaryTable.tsx | 81 ++- .../table/sapronak/IncomingSapronaksTable.tsx | 81 ++- .../OutgoingSapronaksSummaryTable.tsx | 81 ++- .../table/sapronak/OutgoingSapronaksTable.tsx | 81 ++- 18 files changed, 1262 insertions(+), 613 deletions(-) create mode 100644 src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx diff --git a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx new file mode 100644 index 00000000..821ce72a --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx @@ -0,0 +1,36 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTabSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
[]} + isLoading={true} + className={{ + skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4', + headerColumnClassName: 'whitespace-nowrap', + containerClassName: 'mb-0 overflow-hidden', + tableWrapperClassName: 'overflow-hidden', + }} + /> +
+ +
+ + ); +}; + +export default ClosingTabSkeleton; diff --git a/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx new file mode 100644 index 00000000..1168710c --- /dev/null +++ b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const FinanceClosingSkeleton = ({ + title = 'Data Keuangan Belum Tersedia', + subtitle = 'Tidak ada data keuangan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + +
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default FinanceClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx new file mode 100644 index 00000000..490839e1 --- /dev/null +++ b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx @@ -0,0 +1,44 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppExpeditionClosingSkeleton = ({ + columns, + title = 'Data HPP Ekspedisi Belum Tersedia', + subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'id', + header: 'No', + }, + { + id: 'expedition_vendor_name', + header: 'Nama Ekspedisi', + }, + { + id: 'hpp_amount', + header: 'HPP Ekspedisi', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default HppExpeditionClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx new file mode 100644 index 00000000..0ac2f4f3 --- /dev/null +++ b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx @@ -0,0 +1,72 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { Overhead } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const OverheadClosingSkeleton = ({ + columns, + title = 'Data Overhead Belum Tersedia', + subtitle = 'Tidak ada data overhead untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'name', + header: 'Nama Overhead', + }, + { + id: 'budget_quantity', + header: 'Budget Pengajuan - Jumlah', + }, + { + id: 'budget_unit_price', + header: 'Budget Pengajuan - Harga Satuan', + }, + { + id: 'budget_total_amount', + header: 'Budget Pengajuan - Total', + }, + { + id: 'actual_quantity', + header: 'Realisasi - Jumlah', + }, + { + id: 'actual_unit_price', + header: 'Realisasi - Harga Satuan', + }, + { + id: 'actual_total_amount', + header: 'Realisasi - Total', + }, + { + id: 'difference_quantity', + header: 'Selisih - Jumlah', + }, + { + id: 'difference_unit_price', + header: 'Selisih - Harga Satuan', + }, + { + id: 'difference_total_amount', + header: 'Selisih - Total', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default OverheadClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx new file mode 100644 index 00000000..e0031394 --- /dev/null +++ b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx @@ -0,0 +1,33 @@ +import { Icon } from '@iconify/react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const ProductionDataClosingSkeleton = ({ + title = 'Data Produksi Belum Tersedia', + subtitle = 'Tidak ada data produksi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( +
+
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default ProductionDataClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx new file mode 100644 index 00000000..c0ae3a28 --- /dev/null +++ b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx @@ -0,0 +1,84 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseSales } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SalesClosingSkeleton = ({ + columns, + title = 'Data Penjualan Belum Tersedia', + subtitle = 'Tidak ada data penjualan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'realization_date', + header: 'Tanggal Realisasi', + }, + { + id: 'age', + header: 'Umur', + }, + { + id: 'do_number', + header: 'No. DO', + }, + { + id: 'product', + header: 'Produk', + }, + { + id: 'customer', + header: 'Customer', + }, + { + id: 'qty', + header: 'Kuantitas', + }, + { + id: 'weight', + header: 'Kg', + }, + { + id: 'avg_weight', + header: 'AVG (Kg)', + }, + { + id: 'sales_price', + header: 'Harga Sales (Rp)', + }, + { + id: 'total_sales_price', + header: 'Total Sales (Rp)', + }, + { + id: 'actual_price', + header: 'Harga Act (Rp)', + }, + { + id: 'total_actual_price', + header: 'Total Act (Rp)', + }, + { + id: 'kandang', + header: 'Kandang', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SalesClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx new file mode 100644 index 00000000..6c1b9f3c --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx @@ -0,0 +1,72 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { RowSapronakCalculation } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakCalculationClosingSkeleton = ({ + columns, + title = 'Data Perhitungan Sapronak Belum Tersedia', + subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'date', + header: 'Tanggal', + }, + { + id: 'reference_number', + header: 'No. Referensi', + }, + { + id: 'qty_in', + header: 'QTY Masuk', + }, + { + id: 'qty_out', + header: 'QTY Keluar', + }, + { + id: 'qty_used', + header: 'QTY Pakai', + }, + { + id: 'balance', + header: 'Saldo', + }, + { + id: 'unit_price_in', + header: 'Harga Masuk', + }, + { + id: 'unit_price_out', + header: 'Harga Keluar', + }, + { + id: 'total_price_in', + header: 'Total Harga Masuk', + }, + { + id: 'total_price_out', + header: 'Total Harga Keluar', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SapronakCalculationClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx new file mode 100644 index 00000000..e3ea211b --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx @@ -0,0 +1,126 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { ClosingIncomingSapronak } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakClosingSkeleton = ({ + columns, + type = 'incoming', + title, + subtitle, + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + type?: 'incoming' | 'outgoing'; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultIncomingColumns: ColumnDef[] = [ + { + id: '#', + header: '#', + }, + { + id: 'date', + header: 'Tanggal', + }, + { + id: 'reference_number', + header: 'No. Referensi', + }, + { + id: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + id: 'product_name', + header: 'Produk', + }, + { + id: 'product_category', + header: 'Kategori Produk', + }, + { + id: 'source_warehouse', + header: 'Gudang Asal', + }, + { + id: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + id: 'quantity', + header: 'Kuantitas', + }, + { + id: 'notes', + header: 'Keterangan', + }, + ]; + + const defaultOutgoingColumns: ColumnDef[] = [ + { + id: '#', + header: '#', + }, + { + id: 'date', + header: 'Tanggal', + }, + { + id: 'reference_number', + header: 'No. Referensi', + }, + { + id: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + id: 'product_name', + header: 'Produk', + }, + { + id: 'product_category', + header: 'Kategori Produk', + }, + { + id: 'source_warehouse', + header: 'Gudang Asal', + }, + { + id: 'quantity', + header: 'Kuantitas', + }, + { + id: 'notes', + header: 'Keterangan', + }, + ]; + + const defaultTitle = + type === 'incoming' + ? 'Data Sapronak Masuk Belum Tersedia' + : 'Data Sapronak Keluar Belum Tersedia'; + + const defaultSubtitle = + type === 'incoming' + ? 'Silakan pilih periode atau filter untuk melihat data sapronak masuk.' + : 'Silakan pilih periode atau filter untuk melihat data sapronak keluar.'; + + return ( + + columns={ + columns || + (type === 'incoming' ? defaultIncomingColumns : defaultOutgoingColumns) + } + icon={ + + } + title={title || defaultTitle} + subtitle={subtitle || defaultSubtitle} + /> + ); +}; + +export default SapronakClosingSkeleton; diff --git a/src/components/pages/closing/tab/ProductionDataClosingTab.tsx b/src/components/pages/closing/tab/ProductionDataClosingTab.tsx index eebccae3..3647dc1f 100644 --- a/src/components/pages/closing/tab/ProductionDataClosingTab.tsx +++ b/src/components/pages/closing/tab/ProductionDataClosingTab.tsx @@ -5,6 +5,7 @@ import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; import { formatNumber } from '@/lib/helper'; +import ProductionDataClosingSkeleton from '@/components/pages/closing/skeleton/ProductionDataClosingSkeleton'; interface ProductionDataClosingTabProps { projectFlockId: number; @@ -22,18 +23,16 @@ const ProductionDataClosingTab = ({ ); if (isLoading) { - return ( -
- -
- ); + return ; } if (!productionData || !isResponseSuccess(productionData)) { return ( -
- Gagal memuat data produksi. -
+ ); } diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index 8dce38de..de4d6e47 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -7,6 +7,7 @@ import { HppItem, ProfitLossItem } from '@/types/api/closing'; import { useSearchParams } from 'next/navigation'; import { useMemo } from 'react'; import useSWR from 'swr'; +import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton'; const FinanceClosingTable = ({ projectFlockId, @@ -82,316 +83,336 @@ const FinanceClosingTable = ({ return (
- <> - -
-
-
Laba Rugi Brutto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.gross_profit.amount - ) - : '-'} + {isLoading ? ( + + ) : !isResponseSuccess(finance) ? ( + + ) : ( + <> + +
+
+
Laba Rugi Brutto
+
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.gross_profit.amount + ) + : '-'} +
+
+
+
Laba Rugi Netto
+
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit.amount + ) + : '-'} +
-
-
Laba Rugi Netto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit.amount - ) - : '-'} -
-
-
- - -
- - data={hppTableData} - isLoading={isLoading} - columns={[ - { - header: 'No.', - enableSorting: false, - accessorFn: (item, index) => { - if (item.code === 'custom_row') return '-'; - const dataRowsBefore = hppTableData - .slice(0, index) - .filter((row) => row.code !== 'custom_row').length; - return dataRowsBefore + 1; + + +
+ + data={hppTableData} + isLoading={isLoading} + columns={[ + { + header: 'No.', + enableSorting: false, + accessorFn: (item, index) => { + if (item.code === 'custom_row') return '-'; + const dataRowsBefore = hppTableData + .slice(0, index) + .filter((row) => row.code !== 'custom_row').length; + return dataRowsBefore + 1; + }, + footer: (props) => { + return 'HPP'; + }, }, - footer: (props) => { - return 'HPP'; + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => formatTitleCase(item.label || '-'), }, - }, - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => formatTitleCase(item.label || '-'), - }, - { - header: 'Budgeting', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'budgeting_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting - ?.rp_per_bird || 0 - ) - : '-'; + { + header: 'Budgeting', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'budgeting_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_bird || 0 + ) + : '-'; + }, }, - }, - { - header: 'Rp/Kg', - id: 'budgeting_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.rp_per_kg || - 0 - ) - : '-'; + { + header: 'Rp/Kg', + id: 'budgeting_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_kg || 0 + ) + : '-'; + }, }, - }, - { - header: 'Jumlah (Rp)', - id: 'budgeting_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.amount || 0), - footer: (props) => { - return props.column.id === 'budgeting_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.amount || 0 - ) - : '-'; + { + header: 'Jumlah (Rp)', + id: 'budgeting_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.amount || 0), + footer: (props) => { + return props.column.id === 'budgeting_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting?.amount || 0 + ) + : '-'; + }, }, - }, - ], - }, - { - header: 'Realization', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'realization_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_bird || 0 - ) - : '-'; + ], + }, + { + header: 'Realization', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'realization_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === + 'realization_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_bird || 0 + ) + : '-'; + }, }, - }, - { - header: 'Rp/Kg', - id: 'realization_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_kg || 0 - ) - : '-'; + { + header: 'Rp/Kg', + id: 'realization_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'realization_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_kg || 0 + ) + : '-'; + }, }, - }, - { - header: 'Jumlah (Rp)', - id: 'realization_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.amount || 0), - footer: (props) => { - return props.column.id === 'realization_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization?.amount || 0 - ) - : '-'; + { + header: 'Jumlah (Rp)', + id: 'realization_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.amount || 0), + footer: (props) => { + return props.column.id === 'realization_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization?.amount || + 0 + ) + : '-'; + }, }, - }, - ], - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( -
- - -
- {formatTitleCase(rowData.label ?? '-')} -
- - - ); - } - return null; - }} - renderFooter={isResponseSuccess(finance)} - /> - - - -
- - data={profitLossTableData} - isLoading={isLoading} - columns={[ - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => item.label, - cell: (item) => ( -
- {formatTitleCase(item.row.original.label || '-')} -
- ), - footer: () => ( -
LABA RUGI NETTO
- ), - }, - { - header: 'Rp/Ekor', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_bird || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Rp/Kg', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_kg || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Jumlah (Rp)', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.amount || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .amount || 0 - ) - : formatCurrency(0)} -
- ), - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( -
- - - - - - ); - } - return null; - }} - className={{ - paginationClassName: 'hidden', - }} - renderFooter={isResponseSuccess(finance)} - /> - - - + + + + ); + } + return null; + }} + renderFooter={isResponseSuccess(finance)} + /> + + + +
+ + data={profitLossTableData} + isLoading={isLoading} + columns={[ + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => item.label, + cell: (item) => ( +
+ {formatTitleCase(item.row.original.label || '-')} +
+ ), + footer: () => ( +
LABA RUGI NETTO
+ ), + }, + { + header: 'Rp/Ekor', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_bird || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Rp/Kg', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_kg || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Jumlah (Rp)', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.amount || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .amount || 0 + ) + : formatCurrency(0)} +
+ ), + }, + ]} + renderCustomRow={(row) => { + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
+ + + + + + ); + } + return null; + }} + className={{ + paginationClassName: 'hidden', + }} + renderFooter={isResponseSuccess(finance)} + /> + + + + )} ); }; diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index 6c19c2a8..a1ce4a94 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -10,6 +10,7 @@ import { BaseExpeditionCost } from '@/types/api/closing'; import { ClosingApi } from '@/services/api/closing'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; +import HppExpeditionClosingSkeleton from '@/components/pages/closing/skeleton/HppExpeditionClosingSkeleton'; interface HppExpeditionClosingTableProps { projectFlockId: number; @@ -100,28 +101,39 @@ const HppExpeditionClosingTable = ({ body: 'p-0', }} > -
{ + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
-
- {formatTitleCase(rowData.label ?? '-')} -
-
-
- {formatCurrency(rowData.rp_per_bird ?? 0)} -
-
-
- {formatCurrency(rowData.rp_per_kg ?? 0)} -
-
-
- {formatCurrency(rowData.amount ?? 0)} -
-
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatCurrency(rowData.rp_per_bird ?? 0)} +
+
+
+ {formatCurrency(rowData.rp_per_kg ?? 0)} +
+
+
+ {formatCurrency(rowData.amount ?? 0)} +
+
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> + {isLoading ? ( + + ) : costOfRevenueExpeditionData.length === 0 ? ( + + ) : ( +
0} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index f082697a..b1f64f64 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -14,6 +14,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import { useMemo } from 'react'; import useSWR from 'swr'; +import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton'; interface OverheadClosingTableProps { projectFlockId: number; @@ -209,95 +210,108 @@ const OverheadClosingTable = ({ return ( <> - - - data={ - kandangId - ? isResponseSuccess(overheadKandang) - ? (overheadKandang.data?.overheads ?? []) - : [] - : isResponseSuccess(overhead) - ? (overhead.data?.overheads ?? []) - : [] - } + {isLoadingOverhead ? ( + + ) : !isResponseSuccess(overhead) || + (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && !isResponseSuccess(overheadKandang)) ? ( + 0 - : false - } + iconName='heroicons:chart-bar' + title='Data Overhead Tidak Ditemukan' + subtitle='Tidak ada data overhead untuk periode ini.' /> - {kandangId && ( - + + data={ + kandangId + ? isResponseSuccess(overheadKandang) + ? (overheadKandang.data?.overheads ?? []) + : [] + : isResponseSuccess(overhead) + ? (overhead.data?.overheads ?? []) + : [] + } + columns={columns} className={{ - wrapper: 'w-full', - body: 'p-4 shadow-button-soft border border-base-content/10 rounded-lg', + containerClassName: 'my-4', + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'whitespace-nowrap' + ), }} - > -
-
-

Pembelian Kandang

-
-
- -
-
-
- Populasi Akhir KANDANG{' '} - Pemakaian - Di FARM + isLoading={isLoadingOverhead} + renderFooter={ + isResponseSuccess(overhead) + ? overhead.data?.overheads.length > 0 + : false + } + /> + {kandangId && ( + +
+
+

Pembelian Kandang

-
-
- Populasi Akhir Proyek +
+ +
+
+
+ Populasi Akhir KANDANG{' '} + {' '} + Pemakaian Di FARM +
+
+
+ Populasi Akhir Proyek +
+
+
+ +
+
+
+ {formatNumber(chickinPopulation ?? 0)} + + {formatCurrency( + isResponseSuccess(overhead) + ? overhead.data?.total.actual_total_amount + : 0 + )} +
+
+
+ {formatNumber(generalInformation?.population ?? 0)} +
+
+
+ +
+
+

+ {formatNumber(kandangTotal || 0)} +

-
- -
-
-
- {formatNumber(chickinPopulation ?? 0)} - - {formatCurrency( - isResponseSuccess(overhead) - ? overhead.data?.total.actual_total_amount - : 0 - )} -
-
-
- {formatNumber(generalInformation?.population ?? 0)} -
-
-
- -
-
-

- {formatNumber(kandangTotal || 0)} -

-
-
-
- )} - + + )} + + )} ); }; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index 0d8a31ba..fc3d4c55 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -17,6 +17,7 @@ import { Kandang } from '@/types/api/master-data/kandang'; import { ClosingApi } from '@/services/api/closing'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; +import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton'; interface SalesClosingTableProps { projectFlockId: number; @@ -325,27 +326,36 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { body: 'p-0', }} > -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> + {isLoading ? ( + + ) : salesData.length === 0 ? ( + + ) : ( +
0} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index ba92c3d0..53174a71 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -15,6 +15,7 @@ import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; import { ClosingGeneralInformation } from '@/types/api/closing'; import { useSearchParams } from 'next/navigation'; +import SapronakCalculationClosingSkeleton from '@/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton'; interface SapronakCalculationClosingTableProps { projectFlockId: number; @@ -193,21 +194,32 @@ const SapronakCalculationClosingTable = ({ body: 'p-4 shadow', }} > - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc?.rows ?? []) - : [] - } - columns={docColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.doc?.rows.length > 0 - } - /> + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.doc?.rows ?? []) + : [] + } + columns={docColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length > 0 + } + /> + )} - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.ovk?.rows ?? []) - : [] - } - columns={ovkColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.ovk?.rows.length > 0 - } - /> + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.ovk?.rows ?? []) + : [] + } + columns={ovkColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length > 0 + } + /> + )} - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pakan?.rows ?? []) - : [] - } - columns={pakanColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.pakan?.rows.length > 0 - } - /> + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.pakan?.rows ?? []) + : [] + } + columns={pakanColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length > 0 + } + /> + )} ); diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 49e4f108..d9f4f33a 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -15,6 +15,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksSummaryTableProps { projectFlockId: number; @@ -131,40 +132,52 @@ const ClosingIncomingSapronaksSummaryTable = ({ titleClassName='w-full p-0!' >
- - data={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries?.data?.length === 0, - }), - }} - /> + {isLoadingIncomingSapronakSummaries ? ( + + ) : isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries?.data?.length === 0, + }), + }} + /> + )}
diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 3d3a9d70..e8e88582 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -16,6 +16,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksTableProps { projectFlockId: number; @@ -167,40 +168,52 @@ const ClosingIncomingSapronaksTable = ({ - - data={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronaks) && - incomingSapronaks?.data?.length === 0, - }), - }} - /> + {isLoadingIncomingSapronaks ? ( + + ) : isResponseSuccess(incomingSapronaks) && + incomingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronaks) && + incomingSapronaks?.data?.length === 0, + }), + }} + /> + )} diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index 42fcb588..7acc6d67 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -15,6 +15,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksSummaryTableProps { projectFlockId: number; @@ -131,40 +132,52 @@ const ClosingOutgoingSapronaksSummaryTable = ({ titleClassName='w-full p-0!' >
- - data={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries?.data?.length === 0, - }), - }} - /> + {isLoadingOutgoingSapronakSummaries ? ( + + ) : isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries?.data?.length === 0, + }), + }} + /> + )}
diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index acbbc52d..e6ab67db 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -16,6 +16,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksTableProps { projectFlockId: number; @@ -167,40 +168,52 @@ const ClosingOutgoingSapronaksTable = ({ - - data={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks?.data?.length === 0, - }), - }} - /> + {isLoadingOutgoingSapronaks ? ( + + ) : isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks?.data?.length === 0, + }), + }} + /> + )} From befc1c12174a4a7f1bf419649e7d83a34039a168 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:14:28 +0700 Subject: [PATCH 59/82] refactor(FE): Refactor skeleton components to remove default columns --- .../closing/skeleton/ClosingTabSkeleton.tsx | 4 +- .../skeleton/HppExpeditionClosingSkeleton.tsx | 19 +--- .../skeleton/OverheadClosingSkeleton.tsx | 47 +-------- .../closing/skeleton/SalesClosingSkeleton.tsx | 59 +---------- .../SapronakCalculationClosingSkeleton.tsx | 47 +-------- .../skeleton/SapronakClosingSkeleton.tsx | 98 ++----------------- .../closing/table/OverheadClosingTable.tsx | 50 +++++----- .../table/SapronakCalculationClosingTable.tsx | 9 +- .../IncomingSapronaksSummaryTable.tsx | 3 +- .../table/sapronak/IncomingSapronaksTable.tsx | 3 +- .../OutgoingSapronaksSummaryTable.tsx | 3 +- .../table/sapronak/OutgoingSapronaksTable.tsx | 3 +- 12 files changed, 55 insertions(+), 290 deletions(-) diff --git a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx index 821ce72a..44defca8 100644 --- a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx +++ b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx @@ -8,7 +8,7 @@ const ClosingTabSkeleton = ({ title, subtitle, }: { - columns: ColumnDef[]; + columns: ColumnDef[]; icon: React.ReactNode; title: string; subtitle: string; @@ -17,7 +17,7 @@ const ClosingTabSkeleton = ({
[]} + columns={columns} isLoading={true} className={{ skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4', diff --git a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx index 490839e1..d9be9971 100644 --- a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx @@ -9,29 +9,14 @@ const HppExpeditionClosingSkeleton = ({ subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'id', - header: 'No', - }, - { - id: 'expedition_vendor_name', - header: 'Nama Ekspedisi', - }, - { - id: 'hpp_amount', - header: 'HPP Ekspedisi', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx index 0ac2f4f3..7404f5d2 100644 --- a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx @@ -9,57 +9,14 @@ const OverheadClosingSkeleton = ({ subtitle = 'Tidak ada data overhead untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'name', - header: 'Nama Overhead', - }, - { - id: 'budget_quantity', - header: 'Budget Pengajuan - Jumlah', - }, - { - id: 'budget_unit_price', - header: 'Budget Pengajuan - Harga Satuan', - }, - { - id: 'budget_total_amount', - header: 'Budget Pengajuan - Total', - }, - { - id: 'actual_quantity', - header: 'Realisasi - Jumlah', - }, - { - id: 'actual_unit_price', - header: 'Realisasi - Harga Satuan', - }, - { - id: 'actual_total_amount', - header: 'Realisasi - Total', - }, - { - id: 'difference_quantity', - header: 'Selisih - Jumlah', - }, - { - id: 'difference_unit_price', - header: 'Selisih - Harga Satuan', - }, - { - id: 'difference_total_amount', - header: 'Selisih - Total', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx index c0ae3a28..a9ec35aa 100644 --- a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx @@ -9,69 +9,14 @@ const SalesClosingSkeleton = ({ subtitle = 'Tidak ada data penjualan untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'realization_date', - header: 'Tanggal Realisasi', - }, - { - id: 'age', - header: 'Umur', - }, - { - id: 'do_number', - header: 'No. DO', - }, - { - id: 'product', - header: 'Produk', - }, - { - id: 'customer', - header: 'Customer', - }, - { - id: 'qty', - header: 'Kuantitas', - }, - { - id: 'weight', - header: 'Kg', - }, - { - id: 'avg_weight', - header: 'AVG (Kg)', - }, - { - id: 'sales_price', - header: 'Harga Sales (Rp)', - }, - { - id: 'total_sales_price', - header: 'Total Sales (Rp)', - }, - { - id: 'actual_price', - header: 'Harga Act (Rp)', - }, - { - id: 'total_actual_price', - header: 'Total Act (Rp)', - }, - { - id: 'kandang', - header: 'Kandang', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx index 6c1b9f3c..97d4a56c 100644 --- a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx @@ -9,57 +9,14 @@ const SapronakCalculationClosingSkeleton = ({ subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'date', - header: 'Tanggal', - }, - { - id: 'reference_number', - header: 'No. Referensi', - }, - { - id: 'qty_in', - header: 'QTY Masuk', - }, - { - id: 'qty_out', - header: 'QTY Keluar', - }, - { - id: 'qty_used', - header: 'QTY Pakai', - }, - { - id: 'balance', - header: 'Saldo', - }, - { - id: 'unit_price_in', - header: 'Harga Masuk', - }, - { - id: 'unit_price_out', - header: 'Harga Keluar', - }, - { - id: 'total_price_in', - header: 'Total Harga Masuk', - }, - { - id: 'total_price_out', - header: 'Total Harga Keluar', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx index e3ea211b..130cd846 100644 --- a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx @@ -1,103 +1,20 @@ import { Icon } from '@iconify/react'; import ClosingTabSkeleton from './ClosingTabSkeleton'; -import { ClosingIncomingSapronak } from '@/types/api/closing'; import { ColumnDef } from '@tanstack/react-table'; -const SapronakClosingSkeleton = ({ +const SapronakClosingSkeleton = ({ columns, type = 'incoming', title, subtitle, iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; type?: 'incoming' | 'outgoing'; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultIncomingColumns: ColumnDef[] = [ - { - id: '#', - header: '#', - }, - { - id: 'date', - header: 'Tanggal', - }, - { - id: 'reference_number', - header: 'No. Referensi', - }, - { - id: 'transaction_type', - header: 'Jenis Transaksi', - }, - { - id: 'product_name', - header: 'Produk', - }, - { - id: 'product_category', - header: 'Kategori Produk', - }, - { - id: 'source_warehouse', - header: 'Gudang Asal', - }, - { - id: 'destination_warehouse', - header: 'Gudang Tujuan', - }, - { - id: 'quantity', - header: 'Kuantitas', - }, - { - id: 'notes', - header: 'Keterangan', - }, - ]; - - const defaultOutgoingColumns: ColumnDef[] = [ - { - id: '#', - header: '#', - }, - { - id: 'date', - header: 'Tanggal', - }, - { - id: 'reference_number', - header: 'No. Referensi', - }, - { - id: 'transaction_type', - header: 'Jenis Transaksi', - }, - { - id: 'product_name', - header: 'Produk', - }, - { - id: 'product_category', - header: 'Kategori Produk', - }, - { - id: 'source_warehouse', - header: 'Gudang Asal', - }, - { - id: 'quantity', - header: 'Kuantitas', - }, - { - id: 'notes', - header: 'Keterangan', - }, - ]; - const defaultTitle = type === 'incoming' ? 'Data Sapronak Masuk Belum Tersedia' @@ -105,15 +22,12 @@ const SapronakClosingSkeleton = ({ const defaultSubtitle = type === 'incoming' - ? 'Silakan pilih periode atau filter untuk melihat data sapronak masuk.' - : 'Silakan pilih periode atau filter untuk melihat data sapronak keluar.'; + ? 'Tidak ada data sapronak masuk untuk periode ini.' + : 'Tidak ada data sapronak keluar untuk periode ini.'; return ( - - columns={ - columns || - (type === 'incoming' ? defaultIncomingColumns : defaultOutgoingColumns) - } + + columns={columns} icon={ } diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index b1f64f64..152c588c 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -210,27 +210,27 @@ const OverheadClosingTable = ({ return ( <> - {isLoadingOverhead ? ( - - ) : !isResponseSuccess(overhead) || - (!kandangId && overhead.data?.overheads.length === 0) || - (kandangId && !isResponseSuccess(overheadKandang)) ? ( - - ) : ( - + + {isLoadingOverhead ? ( + + ) : !isResponseSuccess(overhead) || + (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && !isResponseSuccess(overheadKandang)) ? ( + + ) : ( data={ kandangId @@ -256,7 +256,8 @@ const OverheadClosingTable = ({ : false } /> - {kandangId && ( + )} + {kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && ( - )} - - )} + )} + ); }; diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index 53174a71..1e2f7534 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -195,10 +195,11 @@ const SapronakCalculationClosingTable = ({ }} > {isLoading ? ( - + ) : isResponseSuccess(sapronakCalculation) && sapronakCalculation.data?.doc?.rows?.length === 0 ? ( {isLoading ? ( - + ) : isResponseSuccess(sapronakCalculation) && sapronakCalculation.data?.ovk?.rows?.length === 0 ? ( {isLoading ? ( - + ) : isResponseSuccess(sapronakCalculation) && sapronakCalculation.data?.pakan?.rows?.length === 0 ? (
{isLoadingIncomingSapronakSummaries ? ( - + ) : isResponseSuccess(incomingSapronakSummaries) && incomingSapronakSummaries.data.length === 0 ? ( {isLoadingIncomingSapronaks ? ( - + ) : isResponseSuccess(incomingSapronaks) && incomingSapronaks.data.length === 0 ? (
{isLoadingOutgoingSapronakSummaries ? ( - + ) : isResponseSuccess(outgoingSapronakSummaries) && outgoingSapronakSummaries.data.length === 0 ? ( {isLoadingOutgoingSapronaks ? ( - + ) : isResponseSuccess(outgoingSapronaks) && outgoingSapronaks.data.length === 0 ? ( Date: Thu, 19 Feb 2026 10:15:20 +0700 Subject: [PATCH 60/82] refactor(FE): Refactor Card and SapronakClosingSkeleton components for readability --- .../closing/table/OverheadClosingTable.tsx | 98 +++++++++---------- .../IncomingSapronaksSummaryTable.tsx | 5 +- .../table/sapronak/IncomingSapronaksTable.tsx | 5 +- .../OutgoingSapronaksSummaryTable.tsx | 5 +- .../table/sapronak/OutgoingSapronaksTable.tsx | 5 +- 5 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index 152c588c..fef1edc6 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -258,58 +258,58 @@ const OverheadClosingTable = ({ /> )} {kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && ( - -
-
-

Pembelian Kandang

+ +
+
+

Pembelian Kandang

+
+
+ +
+
+
+ Populasi Akhir KANDANG{' '} + Pemakaian + Di FARM
-
- -
-
-
- Populasi Akhir KANDANG{' '} - {' '} - Pemakaian Di FARM -
-
-
- Populasi Akhir Proyek -
-
-
- -
-
-
- {formatNumber(chickinPopulation ?? 0)} - - {formatCurrency( - isResponseSuccess(overhead) - ? overhead.data?.total.actual_total_amount - : 0 - )} -
-
-
- {formatNumber(generalInformation?.population ?? 0)} -
-
-
- -
-
-

- {formatNumber(kandangTotal || 0)} -

+
+
+ Populasi Akhir Proyek
- +
+ +
+
+
+ {formatNumber(chickinPopulation ?? 0)} + + {formatCurrency( + isResponseSuccess(overhead) + ? overhead.data?.total.actual_total_amount + : 0 + )} +
+
+
+ {formatNumber(generalInformation?.population ?? 0)} +
+
+
+ +
+
+

+ {formatNumber(kandangTotal || 0)} +

+
+
+ )} diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 0e54a9de..d4e01bd2 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -133,7 +133,10 @@ const ClosingIncomingSapronaksSummaryTable = ({ >
{isLoadingIncomingSapronakSummaries ? ( - + ) : isResponseSuccess(incomingSapronakSummaries) && incomingSapronakSummaries.data.length === 0 ? ( {isLoadingIncomingSapronaks ? ( - + ) : isResponseSuccess(incomingSapronaks) && incomingSapronaks.data.length === 0 ? (
{isLoadingOutgoingSapronakSummaries ? ( - + ) : isResponseSuccess(outgoingSapronakSummaries) && outgoingSapronakSummaries.data.length === 0 ? ( {isLoadingOutgoingSapronaks ? ( - + ) : isResponseSuccess(outgoingSapronaks) && outgoingSapronaks.data.length === 0 ? ( Date: Thu, 19 Feb 2026 10:43:37 +0700 Subject: [PATCH 61/82] refactor(FE): Refactor table components to improve styling and structure --- .../closing/table/FinanceClosingTable.tsx | 63 +++++- .../table/HppExpeditionClosingTable.tsx | 93 ++++----- .../closing/table/OverheadClosingTable.tsx | 32 ++- .../pages/closing/table/SalesClosingTable.tsx | 88 +++++---- .../table/SapronakCalculationClosingTable.tsx | 70 ++++++- .../IncomingSapronaksSummaryTable.tsx | 165 +++++++--------- .../table/sapronak/IncomingSapronaksTable.tsx | 185 ++++++++--------- .../OutgoingSapronaksSummaryTable.tsx | 166 +++++++--------- .../table/sapronak/OutgoingSapronaksTable.tsx | 186 ++++++++---------- 9 files changed, 539 insertions(+), 509 deletions(-) diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index de4d6e47..734e7a7b 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -82,7 +82,7 @@ const FinanceClosingTable = ({ }, [finance]); return ( -
+
{isLoading ? ( ) : !isResponseSuccess(finance) ? ( @@ -96,10 +96,13 @@ const FinanceClosingTable = ({ -
+
Laba Rugi Brutto
@@ -127,10 +130,13 @@ const FinanceClosingTable = ({ variant='bordered' collapsible className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > -
+
data={hppTableData} isLoading={isLoading} @@ -263,6 +269,24 @@ const FinanceClosingTable = ({ ], }, ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} renderCustomRow={(row) => { const rowData = row.original; if (rowData.code === 'custom_row') { @@ -296,10 +320,13 @@ const FinanceClosingTable = ({ variant='bordered' collapsible className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > -
+
data={profitLossTableData} isLoading={isLoading} @@ -363,6 +390,25 @@ const FinanceClosingTable = ({ ), }, ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} renderCustomRow={(row) => { const rowData = row.original; if (rowData.code === 'custom_row') { @@ -404,9 +450,6 @@ const FinanceClosingTable = ({ } return null; }} - className={{ - paginationClassName: 'hidden', - }} renderFooter={isResponseSuccess(finance)} />
diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index a1ce4a94..20bd556d 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -91,53 +91,56 @@ const HppExpeditionClosingTable = ({ ); return ( - <> -
-
-

HPP Ekspedisi

- + + {isLoading ? ( + + ) : costOfRevenueExpeditionData.length === 0 ? ( + + ) : ( +
0} className={{ - wrapper: 'w-full bg-base-100', - body: 'p-0', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} - > - {isLoading ? ( - - ) : costOfRevenueExpeditionData.length === 0 ? ( - - ) : ( -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - )} - - - - + /> + )} + + ); }; diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index fef1edc6..7ed62c0e 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -209,15 +209,18 @@ const OverheadClosingTable = ({ ); return ( - <> +
{isLoadingOverhead ? ( @@ -243,11 +246,24 @@ const OverheadClosingTable = ({ } columns={columns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: cn( - TABLE_DEFAULT_STYLING.headerColumnClassName, + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', 'whitespace-nowrap' ), + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} isLoading={isLoadingOverhead} renderFooter={ @@ -312,7 +328,7 @@ const OverheadClosingTable = ({ )} - +
); }; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index fc3d4c55..e6ded656 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -316,50 +316,54 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { ); return ( - <> -
-
-

Penjualan

- + + {isLoading ? ( + + ) : salesData.length === 0 ? ( + + ) : ( +
0} className={{ - wrapper: 'w-full bg-base-100', - body: 'p-0', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} - > - {isLoading ? ( - - ) : salesData.length === 0 ? ( - - ) : ( -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - )} - - - - + /> + )} + + ); }; diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index 1e2f7534..1ad4d3d7 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -179,7 +179,7 @@ const SapronakCalculationClosingTable = ({ ); return ( -
+
{/* Table DOC jika kategori Project Flock Growing */} {isLoading ? ( @@ -213,7 +216,22 @@ const SapronakCalculationClosingTable = ({ } columns={docColumns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} renderFooter={ isResponseSuccess(sapronakCalculation) && @@ -229,7 +247,10 @@ const SapronakCalculationClosingTable = ({ collapsible defaultCollapsed={true} className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > {isLoading ? ( @@ -251,7 +272,22 @@ const SapronakCalculationClosingTable = ({ } columns={ovkColumns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} renderFooter={ isResponseSuccess(sapronakCalculation) && @@ -267,7 +303,10 @@ const SapronakCalculationClosingTable = ({ collapsible defaultCollapsed={true} className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > {isLoading ? ( @@ -289,7 +328,22 @@ const SapronakCalculationClosingTable = ({ } columns={pakanColumns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} renderFooter={ isResponseSuccess(sapronakCalculation) && diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index d4e01bd2..05fbebd2 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -5,12 +5,10 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -56,8 +54,6 @@ const ClosingIncomingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -94,97 +90,78 @@ const ClosingIncomingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries.data.length > 0 - : false - ); - } - }, [incomingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Masuk
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
- {isLoadingIncomingSapronakSummaries ? ( - - ) : isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries?.data?.length === 0, - }), - }} - /> - )} -
- -
+ {isLoadingIncomingSapronakSummaries ? ( + + ) : isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 268827b7..8e345441 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -5,13 +5,11 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -52,8 +50,6 @@ const ClosingIncomingSapronaksTable = ({ ClosingApi.getAllIncomingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -118,109 +114,90 @@ const ClosingIncomingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks.data.length > 0 - : false - ); - } - }, [incomingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Masuk
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
-
-
- -
+
+
+
- - {isLoadingIncomingSapronaks ? ( - - ) : isResponseSuccess(incomingSapronaks) && - incomingSapronaks.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronaks) && - incomingSapronaks?.data?.length === 0, - }), - }} - /> - )}
- - + + {isLoadingIncomingSapronaks ? ( + + ) : isResponseSuccess(incomingSapronaks) && + incomingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index b5ce3f9b..e5c2eea5 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -5,12 +5,10 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -56,8 +54,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -94,97 +90,79 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries.data.length > 0 - : false - ); - } - }, [outgoingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Keluar
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
- {isLoadingOutgoingSapronakSummaries ? ( - - ) : isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries?.data?.length === 0, - }), - }} - /> - )} -
- -
+ {isLoadingOutgoingSapronakSummaries ? ( + + ) : isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: + 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index dfb0178a..02ddc1bf 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -5,13 +5,11 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -52,8 +50,6 @@ const ClosingOutgoingSapronaksTable = ({ ClosingApi.getAllOutgoingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -118,109 +114,91 @@ const ClosingOutgoingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks.data.length > 0 - : false - ); - } - }, [outgoingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Keluar
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
-
-
- -
+
+
+
- - {isLoadingOutgoingSapronaks ? ( - - ) : isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks?.data?.length === 0, - }), - }} - /> - )}
- - + + {isLoadingOutgoingSapronaks ? ( + + ) : isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: + 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; From 350ff0fbbea150a8211b3a14099707fa7f529813 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:46:03 +0700 Subject: [PATCH 62/82] refactor(FE): Simplify headerRowClassName formatting in tables --- .../closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx | 3 +-- .../pages/closing/table/sapronak/OutgoingSapronaksTable.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index e5c2eea5..60e4e310 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -150,8 +150,7 @@ const ClosingOutgoingSapronaksSummaryTable = ({ tableWrapperClassName: 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: - 'border-b border-b-gray-200 bg-gray-50', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', bodyRowClassName: diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index 02ddc1bf..46f1a46f 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -186,8 +186,7 @@ const ClosingOutgoingSapronaksTable = ({ tableWrapperClassName: 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: - 'border-b border-b-gray-200 bg-gray-50', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', bodyRowClassName: From 4c1f11d859db77fbfa4d7d44fd3a4c0d9d70cafa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:50:52 +0700 Subject: [PATCH 63/82] refactor(FE): Update table column headers from '#' to 'No' --- .../closing/table/sapronak/IncomingSapronaksSummaryTable.tsx | 2 +- .../pages/closing/table/sapronak/IncomingSapronaksTable.tsx | 2 +- .../closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx | 2 +- .../pages/closing/table/sapronak/OutgoingSapronaksTable.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 05fbebd2..ca16d143 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -60,7 +60,7 @@ const ClosingIncomingSapronaksSummaryTable = ({ const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 8e345441..c8f225d9 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -55,7 +55,7 @@ const ClosingIncomingSapronaksTable = ({ const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index 60e4e310..e1c41b30 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -60,7 +60,7 @@ const ClosingOutgoingSapronaksSummaryTable = ({ const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index 46f1a46f..d2179fb3 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -55,7 +55,7 @@ const ClosingOutgoingSapronaksTable = ({ const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { From c3a69bc66aa465be6689f246b930555f68c45362 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:55:07 +0700 Subject: [PATCH 64/82] refactor(FE): Add Icon component and update table styles --- .../pages/closing/table/FinanceClosingTable.tsx | 3 ++- .../sapronak/IncomingSapronaksSummaryTable.tsx | 1 + .../table/sapronak/IncomingSapronaksTable.tsx | 15 ++++++++++++++- .../sapronak/OutgoingSapronaksSummaryTable.tsx | 1 + .../table/sapronak/OutgoingSapronaksTable.tsx | 15 ++++++++++++++- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index 734e7a7b..1e1195fe 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -151,7 +151,7 @@ const FinanceClosingTable = ({ .filter((row) => row.code !== 'custom_row').length; return dataRowsBefore + 1; }, - footer: (props) => { + footer: () => { return 'HPP'; }, }, @@ -286,6 +286,7 @@ const FinanceClosingTable = ({ footerRowClassName: 'border-t-2 border-gray-300', footerColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', }} renderCustomRow={(row) => { const rowData = row.original; diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index ca16d143..9c43675c 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index c8f225d9..0bbad454 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; @@ -135,7 +136,19 @@ const ClosingIncomingSapronaksTable = ({ placeholder='Cari Sapronak Masuk' value={tableFilterState.search} onChange={searchChangeHandler} - className={{ wrapper: 'sm:max-w-3xs' }} + startAdornment={ + + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index e1c41b30..591b38d6 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index d2179fb3..652b3f60 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; @@ -135,7 +136,19 @@ const ClosingOutgoingSapronaksTable = ({ placeholder='Cari Sapronak Keluar' value={tableFilterState.search} onChange={searchChangeHandler} - className={{ wrapper: 'sm:max-w-3xs' }} + startAdornment={ + + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} /> From 8a1e0f080f0cf0183f92275ea29d1b622da47394 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 11:14:39 +0700 Subject: [PATCH 65/82] refactor(FE): Refactor table components for consistent styling and cleanup --- .../table/SapronakCalculationClosingTable.tsx | 2 +- .../sapronak/IncomingSapronaksSummaryTable.tsx | 18 ++++++------------ .../table/sapronak/IncomingSapronaksTable.tsx | 13 ++++++------- .../sapronak/OutgoingSapronaksSummaryTable.tsx | 16 +++++----------- .../table/sapronak/OutgoingSapronaksTable.tsx | 13 ++++++------- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index 1ad4d3d7..0f0a7857 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -179,7 +179,7 @@ const SapronakCalculationClosingTable = ({ ); return ( -
+
{/* Table DOC jika kategori Project Flock Growing */} = (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -92,10 +87,10 @@ const ClosingIncomingSapronaksSummaryTable = ({ }, [sorting, updateFilter]); return ( -
+
+
-
+
= (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -92,7 +87,7 @@ const ClosingOutgoingSapronaksSummaryTable = ({ }, [sorting, updateFilter]); return ( -
+
+
-
+
Date: Thu, 19 Feb 2026 11:27:24 +0700 Subject: [PATCH 66/82] refactor(FE): Refactor table and tab styles for consistent spacing and layout --- src/components/pages/closing/ClosingDetailTabs.tsx | 2 +- src/components/pages/closing/ClosingsTable.tsx | 2 +- src/components/pages/closing/table/FinanceClosingTable.tsx | 2 +- .../pages/closing/table/HppExpeditionClosingTable.tsx | 2 +- src/components/pages/closing/table/OverheadClosingTable.tsx | 2 +- src/components/pages/closing/table/SalesClosingTable.tsx | 2 +- .../pages/closing/table/SapronakCalculationClosingTable.tsx | 2 +- .../pages/closing/table/sapronak/IncomingSapronaksTable.tsx | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 59fea1ba..d5dc996c 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -126,7 +126,7 @@ const ClosingDetail: React.FC = ({ variant='boxed' className={{ tabHeaderWrapper: - 'justify-between items-center py-3 border-b border-base-content/10', + 'relative justify-between items-center py-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10', tab: 'w-fit', content: 'p-0 m-0', }} diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 2716932f..5a8aab17 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -17,7 +17,7 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { LocationApi } from '@/services/api/master-data'; diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index 1e1195fe..82e8b309 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -82,7 +82,7 @@ const FinanceClosingTable = ({ }, [finance]); return ( -
+
{isLoading ? ( ) : !isResponseSuccess(finance) ? ( diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index 20bd556d..5389e3d5 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -91,7 +91,7 @@ const HppExpeditionClosingTable = ({ ); return ( -
+
+
{ ); return ( -
+
+
{/* Table DOC jika kategori Project Flock Growing */} +
Date: Thu, 19 Feb 2026 11:38:34 +0700 Subject: [PATCH 67/82] refactor(FE): Refactor UI and improve conditional rendering in closing pages --- .../pages/closing/ClosingKandangList.tsx | 7 +++---- .../closing/table/OverheadClosingTable.tsx | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index dd3083a7..b324c512 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -10,18 +10,17 @@ const ClosingKandangList = ({ projectData?: ProjectFlock; }) => { return ( -
+
-

Kandang

-
+
{projectData?.kandangs?.map((kandang) => ( diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index 2d3faaec..a6c31e6c 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -224,15 +224,28 @@ const OverheadClosingTable = ({ > {isLoadingOverhead ? ( - ) : !isResponseSuccess(overhead) || - (!kandangId && overhead.data?.overheads.length === 0) || - (kandangId && !isResponseSuccess(overheadKandang)) ? ( + ) : !isResponseSuccess(overhead) ? ( + ) : kandangId && !isResponseSuccess(overheadKandang) ? ( + + ) : (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && + isResponseSuccess(overheadKandang) && + overheadKandang.data?.overheads.length === 0) ? ( + ) : ( data={ From 82975219a89773067f3d0d852238c7f1e8bd5ec7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 11:43:46 +0700 Subject: [PATCH 68/82] refactor(FE): Update border styles for ClosingDetailTabs and ClosingKandangList --- src/components/pages/closing/ClosingDetailTabs.tsx | 2 +- src/components/pages/closing/ClosingKandangList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index d5dc996c..dc8bd6f8 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -126,7 +126,7 @@ const ClosingDetail: React.FC = ({ variant='boxed' className={{ tabHeaderWrapper: - 'relative justify-between items-center py-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10', + 'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10', tab: 'w-fit', content: 'p-0 m-0', }} diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index b324c512..bd2823c4 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -10,7 +10,7 @@ const ClosingKandangList = ({ projectData?: ProjectFlock; }) => { return ( -
+
From a0af934002aef3d0b6a4c0554abbc826b709871d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 11:52:57 +0700 Subject: [PATCH 69/82] feat(FE): Add headings and improve layout for financial tables --- .../pages/closing/ClosingKandangList.tsx | 1 + .../closing/table/FinanceClosingTable.tsx | 99 +++++++++++++------ 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index bd2823c4..4ecf607f 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -14,6 +14,7 @@ const ClosingKandangList = ({
+

Kandang

{projectData?.kandangs?.map((kandang) => (
- - data={isResponseSuccess(closings) ? closings?.data : []} - columns={closingsColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={isResponseSuccess(closings) ? closings?.meta?.page : 0} - totalItems={ - isResponseSuccess(closings) ? closings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoadingClosings} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-0': - isResponseSuccess(closings) && closings?.data?.length === 0, - }), - }} - /> + {isLoadingClosings ? ( +
+ +
+ ) : data.length === 0 ? ( + + } + title='Data Closing Belum Tersedia' + subtitle='Tidak ada data closing untuk saat ini.' + /> + ) : ( + + data={data} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={tableFilterState.page} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-0': data.length === 0, + }), + }} + /> + )}
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'location_id', + val?.value ? String(val.value) : null + ); + } + }} + onInputChange={setLocationInputValue} + isLoading={isLoadingLocationOptions} + isClearable + onMenuScrollToBottom={loadMoreLocations} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('project_status', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
); }; diff --git a/src/components/pages/closing/filter/ClosingFilter.ts b/src/components/pages/closing/filter/ClosingFilter.ts new file mode 100644 index 00000000..77f0c9d2 --- /dev/null +++ b/src/components/pages/closing/filter/ClosingFilter.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export type ClosingFilterType = { + location_id: string | null; + project_status: string | null; +}; + +export const ClosingFilterSchema = yup.object({ + location_id: yup.string().nullable(), + project_status: yup.string().nullable(), +}); + +export type ClosingFilterValues = yup.InferType; diff --git a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx new file mode 100644 index 00000000..4b59510a --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Closing } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTableSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ClosingTableSkeleton; From e9784bd5ed2a9964f82cc868481e4e97f373b59e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 14:33:35 +0700 Subject: [PATCH 75/82] refactor(FE): Refactor ClosingsTable component and update styles --- src/app/closing/page.tsx | 2 +- .../pages/closing/ClosingsTable.tsx | 216 +++++++++++------- 2 files changed, 133 insertions(+), 85 deletions(-) diff --git a/src/app/closing/page.tsx b/src/app/closing/page.tsx index acaa3ee8..0717c350 100644 --- a/src/app/closing/page.tsx +++ b/src/app/closing/page.tsx @@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable'; const Closing = () => { return ( -
+
); diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 294106ff..ad402829 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -3,15 +3,15 @@ import { ChangeEventHandler, useEffect, useState, useMemo } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; import Modal, { useModal } from '@/components/Modal'; import SelectInputRadio from '@/components/input/SelectInputRadio'; @@ -31,32 +31,66 @@ import { import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton'; const RowOptionsMenu = ({ - type = 'dropdown', props, + popoverPosition = 'bottom', + detailClickHandler, }: { - type: 'dropdown' | 'collapse'; props: CellContext; + popoverPosition: 'bottom' | 'top'; + detailClickHandler: (id: number) => void; }) => { + const popoverId = `closing#${props.row.original.id}`; + const popoverAnchorName = `--anchor-closing#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + + const detailClickHandlerWrapper = () => { + detailClickHandler(props.row.original.id); + closePopover(); + }; + return ( - -
- - - -
-
+
+ + + + + +
+ + + +
+
+
); }; const ClosingsTable = () => { + // ===== ROUTER ===== + const router = useRouter(); + // ===== FILTER MODAL STATE ===== const filterModal = useModal(); @@ -170,22 +204,18 @@ const ClosingsTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const detailClickHandler = (id: number) => { + router.push(`/closing/detail/?closingId=${id}`); + }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, @@ -268,16 +298,32 @@ const ClosingsTable = () => { return ( <> -
-
-
-
+
+
+
+
+ {/* Space for action buttons if needed in the future */} +
+ +
+ } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
-
- {isLoadingClosings ? ( -
- -
- ) : data.length === 0 ? ( - - } - title='Data Closing Belum Tersedia' - subtitle='Tidak ada data closing untuk saat ini.' - /> - ) : ( - - data={data} - columns={closingsColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={tableFilterState.page} - totalItems={ - isResponseSuccess(closings) ? closings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoadingClosings} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-0': data.length === 0, - }), - }} - /> - )} + {isLoadingClosings ? ( +
+ +
+ ) : data.length === 0 ? ( + + } + title='Data Closing Belum Tersedia' + subtitle='Tidak ada data closing untuk saat ini.' + /> + ) : ( + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn('mt-3', { + 'w-full mb-0': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
{/* Filter Modal */} From 60e360537e92e2074c21851823b0ccd078a1929a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 14:37:20 +0700 Subject: [PATCH 76/82] refactor(FE): Refactor ClosingsTable header for improved styling --- src/components/pages/closing/ClosingsTable.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index ad402829..22b4dd52 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -300,11 +300,7 @@ const ClosingsTable = () => { <>
-
-
- {/* Space for action buttons if needed in the future */} -
- +
Date: Thu, 19 Feb 2026 14:41:21 +0700 Subject: [PATCH 77/82] feat(FE): Add StatusBadge to display project status in ClosingsTable --- .../pages/closing/ClosingsTable.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 22b4dd52..12114110 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -13,6 +13,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; +import StatusBadge from '@/components/helper/StatusBadge'; import Modal, { useModal } from '@/components/Modal'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useFormik } from 'formik'; @@ -24,6 +25,7 @@ import { LocationApi } from '@/services/api/master-data'; import { Location } from '@/types/api/master-data/location'; import { ClosingApi } from '@/services/api/closing'; import { Closing } from '@/types/api/closing'; +import { Color } from '@/types/theme'; import { ClosingFilterSchema, ClosingFilterType, @@ -91,6 +93,21 @@ const ClosingsTable = () => { // ===== ROUTER ===== const router = useRouter(); + // ===== STATUS BADGE COLOR HELPER ===== + const getProjectStatusBadgeColor = (status: string): Color => { + const normalizedValue = status.toLowerCase(); + + if (normalizedValue === 'aktif') { + return 'success'; + } + + if (normalizedValue === 'pengajuan') { + return 'neutral'; + } + + return 'neutral'; + }; + // ===== FILTER MODAL STATE ===== const filterModal = useModal(); @@ -195,6 +212,19 @@ const ClosingsTable = () => { { accessorKey: 'project_status', header: 'Status', + cell: (props) => { + const status = props.row.original.project_status; + const badgeColor = getProjectStatusBadgeColor(status); + return ( + + ); + }, }, { header: 'Aksi', From a4ff92520ab12020e9ccbc242aa1a82a93e535e8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 14:50:43 +0700 Subject: [PATCH 78/82] refactor(FE): Update toast message for Project Flock approval/rejection --- .../pages/production/project-flock/ProjectFlockTable.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index e8280fa8..8ec79009 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -271,7 +271,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); if (isResponseSuccess(approveProjectFlockRes)) { - toast.success('Project Flock berhasil di-approve!'); + const successMessage = + approvalAction === 'APPROVED' + ? 'Project Flock berhasil di-approve!' + : 'Project Flock berhasil di-reject!'; + toast.success(successMessage); confirmModal.closeModal(); } if (isResponseError(approveProjectFlockRes)) { From 6ac903313c766c9797563d17b1fb198c86ac6fe9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 15:22:28 +0700 Subject: [PATCH 79/82] refactor(FE): Refactor chickin approval modal logic into Zustand store --- .../chickin/form/tabs/ChickLogsView.tsx | 62 ++++++------------- .../project-flock/ProjectFlockTable.tsx | 51 +++++++++++++++ .../production/chickin/chickin.store.ts | 19 ++++++ .../chickin/slices/chickin-approval.slice.ts | 58 +++++++++++++++++ 4 files changed, 147 insertions(+), 43 deletions(-) create mode 100644 src/stores/production/chickin/chickin.store.ts create mode 100644 src/stores/production/chickin/slices/chickin-approval.slice.ts diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index e800ee68..bdffda33 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -2,8 +2,6 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import Card from '@/components/Card'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useModal } from '@/components/Modal'; -import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; @@ -13,6 +11,7 @@ import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandan import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const ChickinLogsView = ({ initialValues, @@ -23,32 +22,26 @@ const ChickinLogsView = ({ afterSubmit?: () => void; rawDataApprovals: BaseApproval[]; }) => { - const confirmModal = useModal(); - const [isApproveLoading, setIsApproveLoading] = useState(false); const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + const { openChickinApproveModal } = useChickinStore(); const handleClickApprove = () => { - confirmModal.openModal(); - }; - - const confirmationModalApproveClickHandler = async (notes?: string) => { - setChickinErrorMessage(''); - setIsApproveLoading(true); - const approveChickinRes = await ChickinApi.singleApproval( - initialValues?.id as number, - 'APPROVED', - notes - ); - if (isResponseSuccess(approveChickinRes)) { - toast.success(approveChickinRes?.message as string); - } - if (isResponseError(approveChickinRes)) { - toast.error(approveChickinRes?.message as string); - setChickinErrorMessage(approveChickinRes?.message as string); - } - confirmModal.closeModal(); - setIsApproveLoading(false); - afterSubmit && afterSubmit(); + openChickinApproveModal(initialValues, async (notes?: string) => { + setChickinErrorMessage(''); + const approveChickinRes = await ChickinApi.singleApproval( + initialValues?.id as number, + 'APPROVED', + notes + ); + if (isResponseSuccess(approveChickinRes)) { + toast.success(approveChickinRes?.message as string); + } + if (isResponseError(approveChickinRes)) { + toast.error(approveChickinRes?.message as string); + setChickinErrorMessage(approveChickinRes?.message as string); + } + afterSubmit && afterSubmit(); + }); }; return ( @@ -83,7 +76,7 @@ const ChickinLogsView = ({ key={chickin.id || index} variant='bordered' className={{ - wrapper: 'w-full', + wrapper: 'w-full mt-3', body: 'p-3', }} > @@ -176,23 +169,6 @@ const ChickinLogsView = ({
)}
- - { - confirmationModalApproveClickHandler(notes); - }, - isLoading: isApproveLoading, - }} - /> ); }; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 8ec79009..040948ff 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -36,6 +36,7 @@ import PopoverContent from '@/components/popover/PopoverContent'; import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const RowOptionsMenu = ({ props, @@ -193,6 +194,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const deleteModal = useModal(); const confirmModal = useModal(); const successModal = useModal(); + const chickinApproveModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); @@ -200,6 +202,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const [isApproveLoading, setIsApproveLoading] = useState(false); const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const { + isChickinApproveModalOpen, + isChickinApproveLoading, + chickinApproveCallback, + closeChickinApproveModal, + setChickinApproveLoading, + } = useChickinStore(); // ===== Fetch Data ===== const { @@ -292,6 +301,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { refreshProjectFlocks(); }, [refresh]); + useEffect(() => { + if (isChickinApproveModalOpen) { + chickinApproveModal.openModal(); + } else { + chickinApproveModal.closeModal(); + } + }, [isChickinApproveModalOpen, chickinApproveModal]); + useEffect(() => { if (isSuccess) { successModal.openModal(); @@ -974,6 +991,40 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { onClose={handleSuccessModalClose} secondaryButton={undefined} /> + + {/* Chickin Approval Modal */} + { + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + }, + }} + primaryButton={{ + text: 'Ya', + color: 'success', + onClick: async (notes) => { + if (chickinApproveCallback) { + setChickinApproveLoading(true); + try { + await chickinApproveCallback(notes); + } finally { + setChickinApproveLoading(false); + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + } + } + }, + isLoading: isChickinApproveLoading, + }} + /> ); }; diff --git a/src/stores/production/chickin/chickin.store.ts b/src/stores/production/chickin/chickin.store.ts new file mode 100644 index 00000000..697b1de4 --- /dev/null +++ b/src/stores/production/chickin/chickin.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; +import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; + +export type ChickinStore = ChickinApprovalSlice; + +export const useChickinStore = create()( + devtools( + (...args) => ({ + ...createChickinApprovalSlice(...args), + }), + { + name: 'ChickinStore', + } + ) +); diff --git a/src/stores/production/chickin/slices/chickin-approval.slice.ts b/src/stores/production/chickin/slices/chickin-approval.slice.ts new file mode 100644 index 00000000..30f0a857 --- /dev/null +++ b/src/stores/production/chickin/slices/chickin-approval.slice.ts @@ -0,0 +1,58 @@ +import { StateCreator } from 'zustand'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; + +export type ChickinApprovalSlice = { + // State + isChickinApproveModalOpen: boolean; + selectedChickinForApproval: ProjectFlockKandang | null; + isChickinApproveLoading: boolean; + chickinApproveCallback: ((notes?: string) => Promise) | null; + + // Actions + openChickinApproveModal: ( + data: ProjectFlockKandang, + callback: (notes?: string) => Promise + ) => void; + closeChickinApproveModal: () => void; + setChickinApproveLoading: (loading: boolean) => void; + resetChickinApproval: () => void; +}; + +export const createChickinApprovalSlice: StateCreator< + ChickinApprovalSlice, + [], + [], + ChickinApprovalSlice +> = (set) => ({ + // Initial state + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + + // Actions + openChickinApproveModal: (data, callback) => + set({ + isChickinApproveModalOpen: true, + selectedChickinForApproval: data, + chickinApproveCallback: callback, + }), + + closeChickinApproveModal: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + chickinApproveCallback: null, + }), + + setChickinApproveLoading: (loading) => + set({ isChickinApproveLoading: loading }), + + resetChickinApproval: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + }), +}); From e22f95cc58fe77ba5666f5baf972b34e6af69f10 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 15:28:27 +0700 Subject: [PATCH 80/82] refactor(FE): Remove unused variable `approval.step_name` in RecordingTable --- src/components/pages/production/recording/RecordingTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a67f44f9..13068563 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -852,8 +852,7 @@ const RecordingTable = () => { const status = approval.action; const statusColor = getStatusBadgeColor(status); - - const statusText = approval.step_name || getStatusText(status); + const statusText = getStatusText(status); return ( Date: Thu, 19 Feb 2026 15:35:08 +0700 Subject: [PATCH 81/82] refactor(FE): Update status mappings for "CREATED" to "Pengajuan" --- .../pages/production/recording/RecordingTable.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 13068563..29f2a82a 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -41,7 +41,8 @@ const statusTextMap: Record = { Disetujui: 'Disetujui', REJECTED: 'Ditolak', Ditolak: 'Ditolak', - CREATED: 'Dibuat', + CREATED: 'Pengajuan', + Pengajuan: 'Pengajuan', UPDATED: 'Diperbarui', }; @@ -59,13 +60,11 @@ const statusBadgeColorMap: Record = { rejected: 'error', ditolak: 'error', CREATED: 'neutral', - Dibuat: 'neutral', + Pengajuan: 'neutral', created: 'neutral', - dibuat: 'neutral', + pengajuan: 'neutral', UPDATED: 'warning', - Diperbarui: 'warning', updated: 'warning', - diperbarui: 'warning', }; const getStatusBadgeColor = (status: string): Color => { From 1a137e7500d638fcb4bb786b53dbf5194a8cbc4e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 15:37:46 +0700 Subject: [PATCH 82/82] refactor(FE): Normalize status keys to uppercase in status utilities --- .../production/recording/RecordingTable.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 29f2a82a..65e658f9 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -38,37 +38,26 @@ import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', - Disetujui: 'Disetujui', REJECTED: 'Ditolak', - Ditolak: 'Ditolak', CREATED: 'Pengajuan', - Pengajuan: 'Pengajuan', UPDATED: 'Diperbarui', }; const getStatusText = (status: string): string => { - return statusTextMap[status] || status; + const normalizedStatus = status.toUpperCase(); + return statusTextMap[normalizedStatus] || status; }; const statusBadgeColorMap: Record = { APPROVED: 'success', - Disetujui: 'success', - approved: 'success', - disetujui: 'success', REJECTED: 'error', - Ditolak: 'error', - rejected: 'error', - ditolak: 'error', CREATED: 'neutral', - Pengajuan: 'neutral', - created: 'neutral', - pengajuan: 'neutral', UPDATED: 'warning', - updated: 'warning', }; const getStatusBadgeColor = (status: string): Color => { - return statusBadgeColorMap[status] || 'neutral'; + const normalizedStatus = status.toUpperCase(); + return statusBadgeColorMap[normalizedStatus] || 'neutral'; }; const RowOptionsMenu = ({