mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'fix/daily-marketing-export' into 'development'
[FIX/FE] Daily Marketing Export See merge request mbugroup/lti-web-client!448
This commit is contained in:
@@ -160,3 +160,62 @@ const handleFilterLocationChange = useCallback(
|
||||
|
||||
- `SupplierTable`, `KandangsTable`, `LocationsTable`, `CustomersTable` in `src/components/pages/master-data/`
|
||||
- Use same pattern for data tables in other modules (inventory, finance, purchase, etc.)
|
||||
|
||||
## Server-side file export pattern
|
||||
|
||||
All file exports (Excel, PDF, or any format) must use **server-side generation** — the server returns a binary blob and the browser triggers a download. Never generate files client-side with `xlsx`, `@react-pdf/renderer`, `jspdf`, or similar libraries.
|
||||
|
||||
**Rule:** Export methods live in the API service class, not in components. Components only build the query string and call the service method.
|
||||
|
||||
### Service method (in `src/services/api/{feature}.ts`)
|
||||
|
||||
```ts
|
||||
async exportToExcel(initialQueryString: string) {
|
||||
const params = new URLSearchParams(initialQueryString);
|
||||
|
||||
params.set('export', 'excel'); // or 'pdf', 'csv', etc.
|
||||
params.set('page', '1');
|
||||
params.set('limit', '99999999999');
|
||||
|
||||
const res = await httpClient<Blob>(`${this.basePath}?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([res]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `filename-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
```
|
||||
|
||||
- Change `export=excel` → `export=pdf` (and the file extension) for PDF exports.
|
||||
- Add one method per format; keep them side-by-side in the same service class.
|
||||
|
||||
### Component handler (in the page/tab component)
|
||||
|
||||
```ts
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filterParams.foo) params.set('foo', filterParams.foo);
|
||||
// ... map all active filter params ...
|
||||
|
||||
await FeatureApi.exportToExcel(params.toString());
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [filterParams, searchValue]);
|
||||
```
|
||||
|
||||
- Do **not** fetch all rows into the component to build the file — delegate entirely to the service method.
|
||||
- Do **not** import `xlsx`, `@react-pdf/renderer`, `jspdf`, `exceljs` in page/tab components.
|
||||
|
||||
**Reference implementation:** `MarketingReportApiService.exportDailyMarketingToExcel` / `exportDailyMarketingToPDF` in [src/services/api/report/marketing-report.ts](src/services/api/report/marketing-report.ts), consumed by [src/components/pages/report/marketing/tab/DailyMarketingTab.tsx](src/components/pages/report/marketing/tab/DailyMarketingTab.tsx).
|
||||
|
||||
@@ -17,16 +17,10 @@ import {
|
||||
formatVechicleNumber,
|
||||
formatTitleCase,
|
||||
} from '@/lib/helper';
|
||||
import {
|
||||
DailyMarketingRow,
|
||||
DailyMarketingReportResponse,
|
||||
} from '@/types/api/report/marketing';
|
||||
import { DailyMarketingRow } from '@/types/api/report/marketing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
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';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -39,8 +33,6 @@ import Modal, { useModal } from '@/components/Modal';
|
||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.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,
|
||||
@@ -284,68 +276,30 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
[dailyMarketings]
|
||||
);
|
||||
|
||||
// ===== 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('page', '1');
|
||||
params.set('limit', '9999999');
|
||||
|
||||
const queryString = `?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const response = await httpClient<DailyMarketingReportResponse>(
|
||||
`${MarketingReportApi.basePath}${queryString}`
|
||||
);
|
||||
|
||||
if (isResponseError(response)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.data || [];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [filterParams, searchValue]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await dailyMarketingsExport();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (!allDataForExport || allDataForExport.length === 0) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
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);
|
||||
|
||||
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,
|
||||
});
|
||||
await MarketingReportApi.exportDailyMarketingToExcel(params.toString());
|
||||
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
@@ -353,34 +307,39 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [filterParams, dailyMarketingsExport, summaryTotal]);
|
||||
}, [filterParams, searchValue]);
|
||||
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await dailyMarketingsExport();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (!allDataForExport || allDataForExport.length === 0) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
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);
|
||||
|
||||
const dailyMarketingReportPdfBlob = await pdf(
|
||||
<DailyMarketingReportPDF data={allDataForExport} total={summaryTotal} />
|
||||
).toBlob();
|
||||
await MarketingReportApi.exportDailyMarketingToPDF(params.toString());
|
||||
|
||||
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||
dailyMarketingReportPdfBlob
|
||||
);
|
||||
window.open(dailyMarketingReportPdfUrl, '_blank');
|
||||
|
||||
toast.success('PDF berhasil dibuat.');
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [dailyMarketingsExport, summaryTotal]);
|
||||
}, [filterParams, searchValue]);
|
||||
|
||||
// ===== TAB ACTIONS COMPONENT =====
|
||||
const TabActions = useMemo(() => {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { httpClientFetcher } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
import {
|
||||
DailyMarketingReport,
|
||||
DailyMarketingReportResponse,
|
||||
} from '@/types/api/report/marketing';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { formatDate } from '@/lib/helper';
|
||||
|
||||
export class MarketingReportApiService extends BaseApiService<
|
||||
@@ -29,48 +24,53 @@ export class MarketingReportApiService extends BaseApiService<
|
||||
async exportDailyMarketingToExcel(initialQueryString: string) {
|
||||
const params = new URLSearchParams(initialQueryString);
|
||||
|
||||
params.set('limit', '9999999');
|
||||
params.set('export', 'excel');
|
||||
params.set('page', '1');
|
||||
params.set('limit', '99999999999');
|
||||
|
||||
const queryString = `?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const dailyMarketingsReport = await httpClientFetcher<
|
||||
BaseApiResponse<DailyMarketingReport>
|
||||
>(`${this.basePath}${queryString}`);
|
||||
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
if (isResponseError(dailyMarketingsReport)) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
return;
|
||||
}
|
||||
const url = window.URL.createObjectURL(new Blob([res]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
const rows = dailyMarketingsReport.data;
|
||||
const fileName = `laporan-penjualan-harian-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||
link.setAttribute('download', fileName);
|
||||
|
||||
const formattedRows = [];
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
formattedRows.push({
|
||||
...rows[i],
|
||||
// created_user: rows[i].created_user.name,
|
||||
// created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'),
|
||||
// updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'),
|
||||
so_date: formatDate(rows[i].so_date, 'YYYY-MM-DD'),
|
||||
realization_date: formatDate(rows[i].realization_date, 'YYYY-MM-DD'),
|
||||
sales: rows[i].sales.name,
|
||||
warehouse: rows[i].warehouse.name,
|
||||
customer: rows[i].customer.name,
|
||||
product: rows[i].product.name,
|
||||
});
|
||||
}
|
||||
async exportDailyMarketingToPDF(initialQueryString: string) {
|
||||
const params = new URLSearchParams(initialQueryString);
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(formattedRows);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'laporan-penjualan-harian');
|
||||
params.set('export', 'pdf');
|
||||
params.set('page', '1');
|
||||
params.set('limit', '99999999999');
|
||||
|
||||
// triggers download in browser
|
||||
XLSX.writeFile(wb, 'laporan-penjualan-harian.xlsx');
|
||||
} catch {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
}
|
||||
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-penjualan-harian-${formatDate(Date.now(), 'DD-MM-YYYY')}.pdf`;
|
||||
link.setAttribute('download', fileName);
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user