diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index f7f68805..61fa7fa6 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -1,5 +1,6 @@ 'use client'; +import axios from 'axios'; import { ChangeEventHandler, useCallback, @@ -35,6 +36,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import ButtonFilter from '@/components/helper/ButtonFilter'; import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton'; +import Dropdown from '@/components/dropdown/Dropdown'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; @@ -73,6 +75,43 @@ type ApprovalStatusValue = const isApprovalDateRequired = (status?: ApprovalStatusValue) => status === 'REALISASI' || status === 'SELESAI'; +const getExportErrorMessage = async ( + error: unknown, + fallbackMessage: string +) => { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + + if (responseData instanceof Blob) { + try { + const parsed = JSON.parse(await responseData.text()) as { + message?: string; + }; + return parsed.message || fallbackMessage; + } catch { + return fallbackMessage; + } + } + + if ( + responseData && + typeof responseData === 'object' && + 'message' in responseData && + typeof responseData.message === 'string' + ) { + return responseData.message; + } + + return error.message || fallbackMessage; + } + + if (error instanceof Error) { + return error.message; + } + + return fallbackMessage; +}; + const RowOptionsMenu = ({ popoverPosition = 'bottom', props, @@ -236,6 +275,7 @@ const ExpensesTable = () => { const approveModal = useModal(); const rejectModal = useModal(); const bulkApproveFormModal = useModal(); + const exportProgressInputModal = useModal(); // ===== FILTER MODAL STATE ===== const filterModal = useModal(); @@ -246,11 +286,14 @@ const ExpensesTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); + const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [, setApprovalNotes] = useState(''); const [bulkApprovalStatus, setBulkApprovalStatus] = useState | null>(null); const [bulkApprovalDate, setBulkApprovalDate] = useState(''); const [bulkApprovalNotes, setBulkApprovalNotes] = useState(''); + const [exportProgressStartDate, setExportProgressStartDate] = useState(''); + const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -492,6 +535,53 @@ const ExpensesTable = () => { setBulkApprovalNotes(e.target.value); }; + const resetExportProgressForm = useCallback(() => { + setExportProgressStartDate(''); + setExportProgressEndDate(''); + }, []); + + const exportProgressStartDateChangeHandler: ChangeEventHandler< + HTMLInputElement + > = (e) => { + setExportProgressStartDate(e.target.value); + }; + + const exportProgressEndDateChangeHandler: ChangeEventHandler< + HTMLInputElement + > = (e) => { + setExportProgressEndDate(e.target.value); + }; + + const exportProgressInputToExcelClickHandler = () => { + resetExportProgressForm(); + exportProgressInputModal.openModal(); + }; + + const submitExportProgressInputHandler = async () => { + if (!exportProgressStartDate || !exportProgressEndDate) { + return; + } + + setIsExportProgressLoading(true); + + try { + await ExpenseApi.exportInputProgressToExcel( + exportProgressStartDate, + exportProgressEndDate + ); + + exportProgressInputModal.closeModal(); + resetExportProgressForm(); + toast.success('Ekspor berhasil'); + } catch (error) { + toast.error( + await getExportErrorMessage(error, 'Gagal mengekspor input progress') + ); + } finally { + setIsExportProgressLoading(false); + } + }; + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -841,6 +931,50 @@ const ExpensesTable = () => { onClick={handleFilterModalOpen} className='px-3 py-2.5' /> + + +
+ + + Export + +
+ + +
+ + } + > + +
@@ -1037,6 +1171,76 @@ const ExpensesTable = () => { + +
+
+

+ Ekspor Input Progress +

+ +
+ +
+ + + +
+ +
+ + +
+
+
+ { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + + if (responseData instanceof Blob) { + try { + const parsed = JSON.parse(await responseData.text()) as { + message?: string; + }; + return parsed.message || fallbackMessage; + } catch { + return fallbackMessage; + } + } + + if ( + responseData && + typeof responseData === 'object' && + 'message' in responseData && + typeof responseData.message === 'string' + ) { + return responseData.message; + } + + return error.message || fallbackMessage; + } + + if (error instanceof Error) { + return error.message; + } + + return fallbackMessage; +}; + const RowsOptionsMenu = ({ props, deleteClickHandler, @@ -161,6 +199,9 @@ const MarketingTable = () => { const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState(''); const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] = useState(false); + const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); + const [exportProgressStartDate, setExportProgressStartDate] = useState(''); + const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const router = useRouter(); const deleteModal = useModal(); @@ -168,6 +209,7 @@ const MarketingTable = () => { const productsModal = useModal(); const deliveryModal = useModal(); const bulkDeliveryModal = useModal(); + const exportProgressInputModal = useModal(); const filterModal = useModal(); const { @@ -439,6 +481,56 @@ const MarketingTable = () => { setIsLoadingExportingToExcel(false); }; + const resetExportProgressForm = useCallback(() => { + setExportProgressStartDate(''); + setExportProgressEndDate(''); + }, []); + + const exportProgressStartDateChangeHandler: ChangeEventHandler = + useCallback((e) => { + setExportProgressStartDate(e.target.value); + }, []); + + const exportProgressEndDateChangeHandler: ChangeEventHandler = + useCallback((e) => { + setExportProgressEndDate(e.target.value); + }, []); + + const exportProgressInputToExcelClickHandler = useCallback(() => { + resetExportProgressForm(); + exportProgressInputModal.openModal(); + }, [exportProgressInputModal, resetExportProgressForm]); + + const submitExportProgressInputHandler = useCallback(async () => { + if (!exportProgressStartDate || !exportProgressEndDate) { + return; + } + + setIsExportProgressLoading(true); + + try { + await MarketingApi.exportInputProgressToExcel( + exportProgressStartDate, + exportProgressEndDate + ); + + exportProgressInputModal.closeModal(); + resetExportProgressForm(); + toast.success('Ekspor berhasil'); + } catch (error) { + toast.error( + await getExportErrorMessage(error, 'Gagal mengekspor input progress') + ); + } finally { + setIsExportProgressLoading(false); + } + }, [ + exportProgressEndDate, + exportProgressInputModal, + exportProgressStartDate, + resetExportProgressForm, + ]); + const columns = useMemo[]>(() => { return [ { @@ -745,6 +837,16 @@ const MarketingTable = () => { Export to Excel + +
@@ -928,6 +1030,75 @@ const MarketingTable = () => { + +
+
+

+ Ekspor Input Progress +

+ +
+ +
+ + + +
+ +
+ + +
+
+
{ + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + + if (responseData instanceof Blob) { + try { + const parsed = JSON.parse(await responseData.text()) as { + message?: string; + }; + return parsed.message || fallbackMessage; + } catch { + return fallbackMessage; + } + } + + if ( + responseData && + typeof responseData === 'object' && + 'message' in responseData && + typeof responseData.message === 'string' + ) { + return responseData.message; + } + + return error.message || fallbackMessage; + } + + if (error instanceof Error) { + return error.message; + } + + return fallbackMessage; +}; + // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', @@ -355,10 +394,14 @@ const RecordingTable = () => { const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); + const [exportProgressStartDate, setExportProgressStartDate] = useState(''); + const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const singleDeleteModal = useModal(); const approveModal = useModal(); const rejectModal = useModal(); + const exportProgressInputModal = useModal(); const { data: recordings, @@ -698,6 +741,60 @@ const RecordingTable = () => { setIsLoadingExportingToExcel(false); }; + const resetExportProgressForm = useCallback(() => { + setExportProgressStartDate(''); + setExportProgressEndDate(''); + }, []); + + const exportProgressStartDateChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setExportProgressStartDate(e.target.value); + }, + [] + ); + + const exportProgressEndDateChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setExportProgressEndDate(e.target.value); + }, + [] + ); + + const exportProgressInputToExcelClickHandler = useCallback(() => { + resetExportProgressForm(); + exportProgressInputModal.openModal(); + }, [exportProgressInputModal, resetExportProgressForm]); + + const submitExportProgressInputHandler = useCallback(async () => { + if (!exportProgressStartDate || !exportProgressEndDate) { + return; + } + + setIsExportProgressLoading(true); + + try { + await RecordingApi.exportInputProgressToExcel( + exportProgressStartDate, + exportProgressEndDate + ); + + exportProgressInputModal.closeModal(); + resetExportProgressForm(); + toast.success('Ekspor berhasil'); + } catch (error) { + toast.error( + await getExportErrorMessage(error, 'Gagal mengekspor input progress') + ); + } finally { + setIsExportProgressLoading(false); + } + }, [ + exportProgressEndDate, + exportProgressInputModal, + exportProgressStartDate, + resetExportProgressForm, + ]); + useEffect(() => { if (isResponseSuccess(recordings) && recordings.data) { const newSelection: Record = {}; @@ -1368,6 +1465,16 @@ const RecordingTable = () => { Export to Excel + + @@ -1551,6 +1658,76 @@ const RecordingTable = () => { }} /> + +
+
+

+ Ekspor Input Progress +

+ +
+ +
+ + + +
+ +
+ + +
+
+
+ { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data; + + if (responseData instanceof Blob) { + try { + const parsed = JSON.parse(await responseData.text()) as { + message?: string; + }; + return parsed.message || fallbackMessage; + } catch { + return fallbackMessage; + } + } + + if ( + responseData && + typeof responseData === 'object' && + 'message' in responseData && + typeof responseData.message === 'string' + ) { + return responseData.message; + } + + return error.message || fallbackMessage; + } + + if (error instanceof Error) { + return error.message; + } + + return fallbackMessage; +}; + // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', @@ -152,9 +192,12 @@ const PurchaseTable = () => { // ===== STATE MANAGEMENT ===== const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [selectedPurchase, setSelectedPurchase] = useState( null ); + const [exportProgressStartDate, setExportProgressStartDate] = useState(''); + const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const [sorting, setSorting] = useState([]); // ===== TABLE FILTER STATE ===== @@ -183,6 +226,7 @@ const PurchaseTable = () => { // ===== MODAL HOOKS ===== const filterModal = useModal(); const deleteModal = useModal(); + const exportProgressInputModal = useModal(); // ===== API DATA FETCHING ===== const { @@ -431,6 +475,56 @@ const PurchaseTable = () => { updateFilter('approval_status', ''); }; + const resetExportProgressForm = useCallback(() => { + setExportProgressStartDate(''); + setExportProgressEndDate(''); + }, []); + + const exportProgressStartDateChangeHandler: ChangeEventHandler = + useCallback((e) => { + setExportProgressStartDate(e.target.value); + }, []); + + const exportProgressEndDateChangeHandler: ChangeEventHandler = + useCallback((e) => { + setExportProgressEndDate(e.target.value); + }, []); + + const exportProgressInputToExcelClickHandler = useCallback(() => { + resetExportProgressForm(); + exportProgressInputModal.openModal(); + }, [exportProgressInputModal, resetExportProgressForm]); + + const submitExportProgressInputHandler = useCallback(async () => { + if (!exportProgressStartDate || !exportProgressEndDate) { + return; + } + + setIsExportProgressLoading(true); + + try { + await PurchaseApi.exportInputProgressToExcel( + exportProgressStartDate, + exportProgressEndDate + ); + + exportProgressInputModal.closeModal(); + resetExportProgressForm(); + toast.success('Ekspor berhasil'); + } catch (error) { + toast.error( + await getExportErrorMessage(error, 'Gagal mengekspor input progress') + ); + } finally { + setIsExportProgressLoading(false); + } + }, [ + exportProgressEndDate, + exportProgressInputModal, + exportProgressStartDate, + resetExportProgressForm, + ]); + return ( <>
@@ -482,6 +576,50 @@ const PurchaseTable = () => { onClick={filterModal.openModal} className='px-3 py-2.5' /> + + +
+ + + Export + +
+ + +
+ + } + > + +
@@ -562,6 +700,76 @@ const PurchaseTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + +
+
+

+ Ekspor Input Progress +

+ +
+ +
+ + + +
+ +
+ + +
+
+
); }; diff --git a/src/services/api/expense.ts b/src/services/api/expense.ts index 329b18d4..b842f8bc 100644 --- a/src/services/api/expense.ts +++ b/src/services/api/expense.ts @@ -9,6 +9,7 @@ import { UpdateExpensePayload, } from '@/types/api/expense'; import { httpClient } from '@/services/http/client'; +import { formatDate } from '@/lib/helper'; export class ExpenseApiService extends BaseApiService< Expense, @@ -706,6 +707,33 @@ export class ExpenseApiService extends BaseApiService< return formData; }; + + async exportInputProgressToExcel(startDate: string, endDate: string) { + const params = new URLSearchParams(); + + params.set('export', 'excel'); + params.set('type', 'progress'); + params.set('start_date', formatDate(startDate, 'YYYY-MM-DD')); + params.set('end_date', formatDate(endDate, 'YYYY-MM-DD')); + + const queryString = `?${params.toString()}`; + + const res = await httpClient(`${this.basePath}${queryString}`, { + method: 'GET', + responseType: 'blob', + }); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + + const fileName = `input-progres-BOP-${formatDate(startDate, 'DD-MM-YYYY')}-ke-${formatDate(endDate, 'DD-MM-YYYY')}.xlsx`; + link.setAttribute('download', fileName); + + document.body.appendChild(link); + link.click(); + link.remove(); + } } export const ExpenseApi = new ExpenseApiService('/expenses'); diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index ab8fe8b3..2689a7ac 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -211,6 +211,33 @@ class MarketingExportService extends BaseApiService< toast.error('Gagal melakukan export marketing! Coba lagi.'); } } + + async exportInputProgressToExcel(startDate: string, endDate: string) { + const params = new URLSearchParams(); + + params.set('export', 'excel'); + params.set('type', 'progress'); + params.set('start_date', formatDate(startDate, 'YYYY-MM-DD')); + params.set('end_date', formatDate(endDate, 'YYYY-MM-DD')); + + const queryString = `?${params.toString()}`; + + const res = await httpClient(`${this.basePath}${queryString}`, { + method: 'GET', + responseType: 'blob', + }); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + + const fileName = `input-progres-penjualan-${formatDate(startDate, 'DD-MM-YYYY')}-ke-${formatDate(endDate, 'DD-MM-YYYY')}.xlsx`; + link.setAttribute('download', fileName); + + document.body.appendChild(link); + link.click(); + link.remove(); + } } export const SalesOrderApi = new SalesOrderService('/marketing/sales-orders'); diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 5009b261..f8e15591 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -116,6 +116,33 @@ export class RecordingService extends BaseApiService< link.click(); link.remove(); } + + async exportInputProgressToExcel(startDate: string, endDate: string) { + const params = new URLSearchParams(); + + params.set('export', 'excel'); + params.set('type', 'progress'); + params.set('start_date', formatDate(startDate, 'YYYY-MM-DD')); + params.set('end_date', formatDate(endDate, 'YYYY-MM-DD')); + + const queryString = `?${params.toString()}`; + + const res = await httpClient(`${this.basePath}${queryString}`, { + method: 'GET', + responseType: 'blob', + }); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + + const fileName = `input-progres-recording-${formatDate(startDate, 'DD-MM-YYYY')}-ke-${formatDate(endDate, 'DD-MM-YYYY')}.xlsx`; + link.setAttribute('download', fileName); + + document.body.appendChild(link); + link.click(); + link.remove(); + } } export const RecordingApi = new RecordingService('/production/recordings'); diff --git a/src/services/api/purchase.ts b/src/services/api/purchase.ts index 38ace6be..db9f01ed 100644 --- a/src/services/api/purchase.ts +++ b/src/services/api/purchase.ts @@ -10,6 +10,8 @@ import { } from '@/types/api/purchase/purchase'; import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; +import { formatDate } from '@/lib/helper'; +import { httpClient } from '../http/client'; const basePurchaseApi = new BaseApiService< Purchase, @@ -112,4 +114,34 @@ export const PurchaseApi = { }); }, }, + + async exportInputProgressToExcel(startDate: string, endDate: string) { + const params = new URLSearchParams(); + + params.set('export', 'excel'); + params.set('type', 'progress'); + params.set('start_date', formatDate(startDate, 'YYYY-MM-DD')); + params.set('end_date', formatDate(endDate, 'YYYY-MM-DD')); + + const queryString = `?${params.toString()}`; + + const res = await httpClient( + `${basePurchaseApi.basePath}${queryString}`, + { + method: 'GET', + responseType: 'blob', + } + ); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + + const fileName = `input-progres-pembelian-${formatDate(startDate, 'DD-MM-YYYY')}-ke-${formatDate(endDate, 'DD-MM-YYYY')}.xlsx`; + link.setAttribute('download', fileName); + + document.body.appendChild(link); + link.click(); + link.remove(); + }, };