mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/expense-export' into 'development'
[FEAT/FE] Expense Export See merge request mbugroup/lti-web-client!426
This commit is contained in:
@@ -41,7 +41,7 @@ import Dropdown from '@/components/dropdown/Dropdown';
|
||||
import { Expense } from '@/types/api/expense';
|
||||
import { ExpenseApi } from '@/services/api/expense';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
@@ -84,43 +84,6 @@ 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,
|
||||
@@ -314,6 +277,8 @@ const ExpensesTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||
useState(false);
|
||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||
const [, setApprovalNotes] = useState('');
|
||||
const [bulkApprovalStatus, setBulkApprovalStatus] =
|
||||
@@ -603,7 +568,7 @@ const ExpensesTable = () => {
|
||||
toast.success('Ekspor berhasil');
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
);
|
||||
} finally {
|
||||
setIsExportProgressLoading(false);
|
||||
@@ -818,6 +783,20 @@ const ExpensesTable = () => {
|
||||
resetFilter();
|
||||
};
|
||||
|
||||
const exportToExcel = useCallback(async () => {
|
||||
setIsLoadingExportingToExcel(true);
|
||||
|
||||
try {
|
||||
await ExpenseApi.exportToExcel(getTableFilterQueryString());
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingExportingToExcel(false);
|
||||
}
|
||||
}, [getTableFilterQueryString]);
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
@@ -1031,6 +1010,17 @@ const ExpensesTable = () => {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={exportToExcel}
|
||||
isLoading={isLoadingExportingToExcel}
|
||||
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 ke Excel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='none'
|
||||
|
||||
@@ -9,7 +9,11 @@ import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import {
|
||||
getErrorMessage,
|
||||
isResponseError,
|
||||
isResponseSuccess,
|
||||
} from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||
import {
|
||||
MarketingApi,
|
||||
@@ -37,43 +41,6 @@ import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import MarketingTableSkeleton from '@/components/pages/marketing/skeleton/MarketingTableSkeleton';
|
||||
|
||||
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 RowsOptionsMenu = ({
|
||||
props,
|
||||
deleteClickHandler,
|
||||
@@ -537,7 +504,7 @@ const MarketingTable = () => {
|
||||
toast.success('Ekspor berhasil');
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
);
|
||||
} finally {
|
||||
setIsExportProgressLoading(false);
|
||||
|
||||
@@ -41,7 +41,7 @@ import Table from '@/components/Table';
|
||||
import { type Recording } from '@/types/api/production/recording';
|
||||
import { getRecordingRestriction } from './recording-utils';
|
||||
import { RecordingApi } from '@/services/api/production';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import toast from 'react-hot-toast';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
@@ -52,43 +52,6 @@ import { Color } from '@/types/theme';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
|
||||
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',
|
||||
@@ -839,7 +802,7 @@ const RecordingTable = () => {
|
||||
toast.success('Ekspor berhasil');
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
);
|
||||
} finally {
|
||||
setIsExportProgressLoading(false);
|
||||
|
||||
@@ -33,7 +33,7 @@ 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';
|
||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -43,43 +43,6 @@ 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',
|
||||
@@ -518,7 +481,7 @@ const PurchaseTable = () => {
|
||||
await PurchaseApi.exportToExcel(getTableFilterQueryString());
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getExportErrorMessage(error, 'Gagal mengekspor data pembelian')
|
||||
await getErrorMessage(error, 'Gagal mengekspor data pembelian')
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingExportingToExcel(false);
|
||||
@@ -563,7 +526,7 @@ const PurchaseTable = () => {
|
||||
toast.success('Ekspor berhasil');
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||
);
|
||||
} finally {
|
||||
setIsExportProgressLoading(false);
|
||||
|
||||
@@ -24,7 +24,7 @@ import Table from '@/components/Table';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import Pagination from '@/components/Pagination';
|
||||
@@ -189,26 +189,47 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
||||
[formik.values.category]
|
||||
);
|
||||
|
||||
const buildReportExpenseQueryString = useCallback(
|
||||
(extraParams?: Record<string, string>) => {
|
||||
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('project_flock_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);
|
||||
}
|
||||
|
||||
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
|
||||
params.set(key, value);
|
||||
});
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[filterParams]
|
||||
);
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||
() => {
|
||||
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('project_flock_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));
|
||||
const queryString = buildReportExpenseQueryString({
|
||||
page: String(page),
|
||||
limit: String(pageSize),
|
||||
});
|
||||
|
||||
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
|
||||
return [`${ReportExpenseApi.basePath}?${queryString}`];
|
||||
},
|
||||
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
||||
);
|
||||
@@ -233,47 +254,31 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
||||
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 queryString = buildReportExpenseQueryString({
|
||||
page: '1',
|
||||
limit: '100',
|
||||
});
|
||||
|
||||
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
||||
`${ReportExpenseApi.basePath}?${params.toString()}`
|
||||
`${ReportExpenseApi.basePath}?${queryString}`
|
||||
);
|
||||
|
||||
return isResponseSuccess(response) ? response.data : null;
|
||||
}, [filterParams]);
|
||||
}, [buildReportExpenseQueryString]);
|
||||
|
||||
// ===== 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.');
|
||||
await ReportExpenseApi.exportToExcel(buildReportExpenseQueryString());
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
||||
);
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [reportExpenseExport]);
|
||||
}, [buildReportExpenseQueryString]);
|
||||
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios from 'axios';
|
||||
import {
|
||||
BaseApiResponse,
|
||||
ErrorApiResponse,
|
||||
@@ -15,3 +16,40 @@ export const isResponseError = <T>(
|
||||
): res is ErrorApiResponse => {
|
||||
return res?.status === 'error';
|
||||
};
|
||||
|
||||
export const getErrorMessage = 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;
|
||||
};
|
||||
|
||||
@@ -708,6 +708,33 @@ export class ExpenseApiService extends BaseApiService<
|
||||
return formData;
|
||||
};
|
||||
|
||||
async exportToExcel(initialQueryString: string) {
|
||||
const params = new URLSearchParams(initialQueryString);
|
||||
|
||||
params.set('export', 'excel');
|
||||
params.set('type', 'all');
|
||||
params.set('page', '1');
|
||||
params.set('limit', '99999999999');
|
||||
|
||||
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 = `BOP-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||
link.setAttribute('download', fileName);
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { formatDate } from '@/lib/helper';
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { httpClientFetcher } from '@/services/http/client';
|
||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
ReportDepreciation,
|
||||
@@ -20,6 +21,33 @@ export class ReportExpenseApiService extends BaseApiService<
|
||||
): Promise<BaseApiResponse<ReportExpense[]>> {
|
||||
return await httpClientFetcher<BaseApiResponse<ReportExpense[]>>(endpoint);
|
||||
}
|
||||
|
||||
async exportToExcel(initialQueryString: string) {
|
||||
const params = new URLSearchParams(initialQueryString);
|
||||
|
||||
params.set('export', 'excel');
|
||||
params.set('type', 'all');
|
||||
params.set('page', '1');
|
||||
params.set('limit', '99999999999');
|
||||
|
||||
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 = `Laporan-BOP-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||
link.setAttribute('download', fileName);
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense');
|
||||
|
||||
Reference in New Issue
Block a user