feat: implement purchase export progress input

This commit is contained in:
ValdiANS
2026-04-22 11:06:34 +07:00
parent ddfd1206a7
commit 9293b6321f
2 changed files with 241 additions and 1 deletions
+209 -1
View File
@@ -1,5 +1,6 @@
'use client';
import axios from 'axios';
import {
ChangeEventHandler,
useCallback,
@@ -18,8 +19,9 @@ import Link from 'next/link';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import DateInput from '@/components/input/DateInput';
import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
@@ -28,6 +30,7 @@ import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import Dropdown from '@/components/dropdown/Dropdown';
import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
@@ -40,6 +43,43 @@ import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme';
const getExportErrorMessage = async (
error: unknown,
fallbackMessage: string
) => {
if (axios.isAxiosError(error)) {
const responseData = error.response?.data;
if (responseData instanceof Blob) {
try {
const parsed = JSON.parse(await responseData.text()) as {
message?: string;
};
return parsed.message || fallbackMessage;
} catch {
return fallbackMessage;
}
}
if (
responseData &&
typeof responseData === 'object' &&
'message' in responseData &&
typeof responseData.message === 'string'
) {
return responseData.message;
}
return error.message || fallbackMessage;
}
if (error instanceof Error) {
return error.message;
}
return fallbackMessage;
};
// ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = {
APPROVED: 'Disetujui',
@@ -152,9 +192,12 @@ const PurchaseTable = () => {
// ===== STATE MANAGEMENT =====
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
null
);
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
// ===== 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<HTMLInputElement> =
useCallback((e) => {
setExportProgressStartDate(e.target.value);
}, []);
const exportProgressEndDateChangeHandler: ChangeEventHandler<HTMLInputElement> =
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 (
<>
<div className='w-full'>
@@ -482,6 +576,50 @@ const PurchaseTable = () => {
onClick={filterModal.openModal}
className='px-3 py-2.5'
/>
<Dropdown
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={exportProgressInputToExcelClickHandler}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Ekspor Input Progress (Excel)
</Button>
</Dropdown>
</div>
</div>
@@ -562,6 +700,76 @@ const PurchaseTable = () => {
onClick: confirmationModalDeleteClickHandler,
}}
/>
<Modal
ref={exportProgressInputModal.ref}
className={{
modalBox: 'max-w-lg rounded-lg p-0',
}}
closeOnBackdrop
>
<div className='flex flex-col'>
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
<h4 className='text-sm font-semibold text-base-content'>
Ekspor Input Progress
</h4>
<Button
variant='ghost'
color='none'
onClick={() => {
exportProgressInputModal.closeModal();
resetExportProgressForm();
}}
className='p-1'
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
<div className='flex flex-col gap-4 p-4'>
<DateInput
name='export_progress_start_date'
label='Tanggal Mulai'
value={exportProgressStartDate}
onChange={exportProgressStartDateChangeHandler}
isNestedModal
required
/>
<DateInput
name='export_progress_end_date'
label='Tanggal Selesai'
value={exportProgressEndDate}
onChange={exportProgressEndDateChangeHandler}
isNestedModal
required
/>
</div>
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
<Button
variant='outline'
color='none'
onClick={() => {
exportProgressInputModal.closeModal();
resetExportProgressForm();
}}
className='px-3 py-2.5'
>
Batal
</Button>
<Button
color='success'
onClick={submitExportProgressInputHandler}
isLoading={isExportProgressLoading}
disabled={!exportProgressStartDate || !exportProgressEndDate}
className='px-3 py-2.5'
>
Submit
</Button>
</div>
</div>
</Modal>
</>
);
};
+32
View File
@@ -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<Blob>(
`${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();
},
};