feat: implement expense export progress input

This commit is contained in:
ValdiANS
2026-04-22 11:05:54 +07:00
parent c374a4a4e9
commit aae633edee
2 changed files with 232 additions and 0 deletions
@@ -1,5 +1,6 @@
'use client'; 'use client';
import axios from 'axios';
import { import {
ChangeEventHandler, ChangeEventHandler,
useCallback, useCallback,
@@ -35,6 +36,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton'; import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton';
import Dropdown from '@/components/dropdown/Dropdown';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { ExpenseApi } from '@/services/api/expense'; import { ExpenseApi } from '@/services/api/expense';
@@ -73,6 +75,43 @@ type ApprovalStatusValue =
const isApprovalDateRequired = (status?: ApprovalStatusValue) => const isApprovalDateRequired = (status?: ApprovalStatusValue) =>
status === 'REALISASI' || status === 'SELESAI'; 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 = ({ const RowOptionsMenu = ({
popoverPosition = 'bottom', popoverPosition = 'bottom',
props, props,
@@ -236,6 +275,7 @@ const ExpensesTable = () => {
const approveModal = useModal(); const approveModal = useModal();
const rejectModal = useModal(); const rejectModal = useModal();
const bulkApproveFormModal = useModal(); const bulkApproveFormModal = useModal();
const exportProgressInputModal = useModal();
// ===== FILTER MODAL STATE ===== // ===== FILTER MODAL STATE =====
const filterModal = useModal(); const filterModal = useModal();
@@ -246,11 +286,14 @@ const ExpensesTable = () => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false);
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
const [, setApprovalNotes] = useState(''); const [, setApprovalNotes] = useState('');
const [bulkApprovalStatus, setBulkApprovalStatus] = const [bulkApprovalStatus, setBulkApprovalStatus] =
useState<OptionType<ApprovalStatusValue> | null>(null); useState<OptionType<ApprovalStatusValue> | null>(null);
const [bulkApprovalDate, setBulkApprovalDate] = useState(''); const [bulkApprovalDate, setBulkApprovalDate] = useState('');
const [bulkApprovalNotes, setBulkApprovalNotes] = useState(''); const [bulkApprovalNotes, setBulkApprovalNotes] = useState('');
const [exportProgressStartDate, setExportProgressStartDate] = useState('');
const [exportProgressEndDate, setExportProgressEndDate] = useState('');
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
@@ -492,6 +535,53 @@ const ExpensesTable = () => {
setBulkApprovalNotes(e.target.value); 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 () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -841,6 +931,50 @@ const ExpensesTable = () => {
onClick={handleFilterModalOpen} onClick={handleFilterModalOpen}
className='px-3 py-2.5' 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>
</div> </div>
@@ -1037,6 +1171,76 @@ const ExpensesTable = () => {
</div> </div>
</Modal> </Modal>
<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>
<ExpensesFilterModal <ExpensesFilterModal
ref={filterModal.ref} ref={filterModal.ref}
onSubmit={handleFilterSubmit} onSubmit={handleFilterSubmit}
+28
View File
@@ -8,6 +8,7 @@ import {
UpdateExpensePayload, UpdateExpensePayload,
} from '@/types/api/expense'; } from '@/types/api/expense';
import { httpClient } from '@/services/http/client'; import { httpClient } from '@/services/http/client';
import { formatDate } from '@/lib/helper';
export class ExpenseApiService extends BaseApiService< export class ExpenseApiService extends BaseApiService<
Expense, Expense,
@@ -681,6 +682,33 @@ export class ExpenseApiService extends BaseApiService<
return formData; 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<Blob>(`${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'); export const ExpenseApi = new ExpenseApiService('/expenses');