Wire up the export functionality in the frontend to use the backend IPC handlers. Replace the fake progress with real IPC communication.
Requirements
Pre-Validation (NEW)
API Types
// src/renderer/src/types/export.ts
type DateRange = 'today' | '7days' | '30days' | 'all';
type ExportType = 'employees' | 'attachments' | 'media';
type ExportFormat = 'csv' | 'xlsx' | 'pdf';
type ExportOptions = {
types: ExportType[];
format: ExportFormat;
dateRange: DateRange;
};
type ExportResult = {
success: boolean;
filePath?: string;
recordCount: number;
canceled?: boolean;
error?: string;
};
type ExportProgress = {
current: number;
total: number;
percent: number;
stage: 'query' | 'export' | 'write';
};
type ExportPreview = {
employees: number;
attachments: number;
media: number;
total: number;
};
Preload API
// src/preload/index.ts
export interface ExportAPI {
exportData: (options: ExportOptions) => Promise<ExportResult>;
previewExport: (options: { types: ExportType[]; dateRange: DateRange }) => Promise<ExportPreview>;
onExportProgress: (callback: (progress: ExportProgress) => void) => () => void;
openFile: (filePath: string) => Promise<void>;
openFolder: (folderPath: string) => Promise<void>;
}
IPC Handler - Preview
// src/core/ipc/database/handlers.ts
// Preview: just count records without exporting
ipcMain.handle('preview-export', async (_event, options: { types: ExportType[]; dateRange: DateRange }) => {
const preview: ExportPreview = { employees: 0, attachments: 0, media: 0, total: 0 };
if (options.types.includes('employees')) {
preview.employees = countEmployees(options.dateRange);
preview.total += preview.employees;
}
if (options.types.includes('attachments')) {
preview.attachments = countAttachments(options.dateRange);
preview.total += preview.attachments;
}
if (options.types.includes('media')) {
preview.media = countMedia(options.dateRange);
preview.total += preview.media;
}
return preview;
});
Export Action
// src/renderer/src/actions/database.ts
import { toast } from '@/utils/toast';
import { t } from '@/i18n'; // or your i18n setup
type ExportOptions = {
types: ('employees' | 'attachments' | 'media')[];
format: 'csv' | 'xlsx' | 'pdf';
dateRange: 'today' | '7days' | '30days' | 'all';
};
type ExportResult = {
success: boolean;
filePath?: string;
recordCount: number;
canceled?: boolean;
error?: string;
};
type ExportProgress = {
current: number;
total: number;
percent: number;
stage: 'query' | 'export' | 'write';
};
type ExportPreview = {
employees: number;
attachments: number;
media: number;
total: number;
};
// Preview action - shows count before export
const previewExport = async (
types: ExportType[],
dateRange: DateRange
): Promise<ExportPreview> => {
return window.api.previewExport({ types, dateRange });
};
// Export action
const exportData = async (
options: ExportOptions,
onProgress?: (progress: ExportProgress) => void
): Promise<ExportResult> => {
if (!options.types || options.types.length === 0) {
toast({
title: t('export.error.noTypes.title'),
description: t('export.error.noTypes.description'),
variant: 'destructive',
});
return { success: false, error: 'No types selected', recordCount: 0 };
}
const unsubscribe = onProgress
? window.api.onExportProgress(onProgress)
: undefined;
try {
const result = await window.api.exportData(options);
if (result.canceled) {
return result;
}
if (result.success) {
toast({
title: t('export.success.title'),
description: t('export.success.description', { count: result.recordCount }),
});
} else {
toast({
title: t('export.error.failed.title'),
description: result.error || t('export.error.failed.description'),
variant: 'destructive',
});
}
return result;
} catch (error) {
const message = error instanceof Error ? error.message : t('export.error.unknown');
toast({
title: t('export.error.failed.title'),
description: message,
variant: 'destructive',
});
throw error;
} finally {
if (unsubscribe) {
unsubscribe();
}
}
};
export { exportData, previewExport };
UI - Export Dialog with Preview
// src/renderer/src/components/right-sidebar.tsx
import { exportData, previewExport } from '@/actions/database';
import { t } from '@/i18n';
const [selectedTypes, setSelectedTypes] = useState<ExportType[]>([]);
const [selectedFormat, setSelectedFormat] = useState<ExportFormat>('csv');
const [dateRange, setDateRange] = useState<DateRange>('all');
const [isExporting, setIsExporting] = useState(false);
const [isPreviewing, setIsPreviewing] = useState(false);
const [exportPreview, setExportPreview] = useState<ExportPreview | null>(null);
const [exportProgress, setExportProgress] = useState<ExportProgress | null>(null);
const [exportResult, setExportResult] = useState<ExportResult | null>(null);
// Preview when types or dateRange change
useEffect(() => {
if (selectedTypes.length === 0) {
setExportPreview(null);
return;
}
const timer = setTimeout(async () => {
setIsPreviewing(true);
try {
const preview = await previewExport(selectedTypes, dateRange);
setExportPreview(preview);
} catch (e) {
setExportPreview(null);
} finally {
setIsPreviewing(false);
}
}, 300); // Debounce 300ms
return () => clearTimeout(timer);
}, [selectedTypes, dateRange]);
const handleExport = async () => {
if (selectedTypes.length === 0 || !exportPreview || exportPreview.total === 0) {
toast({
title: t('export.error.noData.title'),
description: t('export.error.noData.description'),
variant: 'destructive',
});
return;
}
setIsExporting(true);
setExportProgress({ current: 0, total: 0, percent: 0, stage: 'query' });
setExportResult(null);
try {
const result = await exportData(
{ types: selectedTypes, format: selectedFormat, dateRange },
(progress) => setExportProgress(progress)
);
if (result.canceled) return;
setExportResult(result);
} finally {
setIsExporting(false);
setExportProgress(null);
}
};
// Preview UI
{selectedTypes.length > 0 && (
<div className="text-sm text-muted-foreground">
{isPreviewing ? (
<span>{t('export.preview.loading')}</span>
) : exportPreview ? (
<span>
{exportPreview.total > 0
? t('export.preview.ready', {
employees: exportPreview.employees,
attachments: exportPreview.attachments,
media: exportPreview.media,
total: exportPreview.total,
})
: t('export.preview.empty')}
</span>
) : null}
</div>
)}
i18n Keys
// src/i18n/locales/fr.json
{
"export": {
"title": "Exporter les donnees",
"preview": {
"loading": "Calcul en cours...",
"ready": "{total} enregistrements a exporter ({employees} employes, {attachments} pieces jointes, {media} medias)",
"empty": "Aucune donnee a exporter"
},
"success": {
"title": "Export reussi",
"description": "{count} enregistrements exportes"
},
"error": {
"noTypes": {
"title": "Export echoue",
"description": "Selectionnez au moins un type de donnee"
},
"noData": {
"title": "Aucune donnee",
"description": "Aucune donnee a exporter avec ces filtres"
},
"failed": {
"title": "Export echoue",
"description": "Une erreur est survenue"
},
"unknown": "Erreur inconnue"
},
"actions": {
"openFile": "Ouvrir le fichier",
"openFolder": "Ouvrir le dossier",
"close": "Fermer"
}
}
}
Dialog States
1. Initial State
- Form with types, format, date range
- Preview shows: "45 enregistrements a exporter"
- Export button (enabled if records > 0)
2. Loading Preview
- Form visible
- Preview shows: "Calcul en cours..."
- Export button disabled
3. No Data
- Preview shows: "Aucune donnee a exporter"
- Export button disabled
4. Exporting
- Progress bar visible
- Form disabled
5. Success
- Success message + file path
- "Ouvrir le fichier" button
- "Ouvrir le dossier" button
- "Fermer" button
6. Error
- Error message
- "Fermer" button (dialog stays open)
IPC Handler with File Operations
// src/core/ipc/database/handlers.ts
import { ipcMain, shell } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { app } from 'electron';
import { csvExporter } from '@/lib/exporters/csv-exporter';
import { excelExporter } from '@/lib/exporters/excel-exporter';
import { pdfExporter } from '@/lib/exporters/pdf-exporter';
import { getEmployeesForExport, getAttachmentsForExport, getMediaForExport } from '@/db/queries/export-queries';
const getExportDir = (): string => {
const documentsPath = app.getPath('documents');
const exportDir = path.join(documentsPath, 'WEMS', 'exports');
if (!fs.existsSync(exportDir)) {
fs.mkdirSync(exportDir, { recursive: true });
}
return exportDir;
};
const exportDataHandler = async (event: Electron.IpcMainInvokeEvent, options: ExportOptions) => {
const sender = event.sender;
try {
if (!options.types || options.types.length === 0) {
return { success: false, error: 'No export types selected', recordCount: 0 };
}
const exporter = options.format === 'csv'
? csvExporter
: options.format === 'xlsx'
? excelExporter
: pdfExporter;
const ext = exporter.getExtension();
const exportDir = getExportDir();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `export-${timestamp}.${ext}`;
const filePath = path.join(exportDir, fileName);
// Query data
sendProgress(sender, { current: 0, total: 0, percent: 0, stage: 'query' });
const data: Record<string, unknown[]> = {};
let totalRecords = 0;
for (let i = 0; i < options.types.length; i++) {
const type = options.types[i];
if (type === 'employees') {
data.employees = getEmployeesForExport(options.dateRange);
totalRecords += data.employees.length;
} else if (type === 'attachments') {
data.attachments = getAttachmentsForExport(options.dateRange);
totalRecords += data.attachments.length;
} else if (type === 'media') {
data.media = getMediaForExport(options.dateRange);
totalRecords += data.media.length;
}
sendProgress(sender, {
current: i + 1,
total: options.types.length,
percent: Math.round(((i + 1) / options.types.length) * 50),
stage: 'query',
});
}
// Generate file
sendProgress(sender, { current: 0, total: totalRecords, percent: 50, stage: 'export' });
const buffer = await exporter.generate(data);
// Write to disk
sendProgress(sender, { current: 0, total: totalRecords, percent: 80, stage: 'write' });
await fs.promises.writeFile(filePath, buffer);
sendProgress(sender, { current: totalRecords, total: totalRecords, percent: 100, stage: 'write' });
return { success: true, filePath, recordCount: totalRecords };
} catch (error) {
const message = error instanceof Error ? error.message : 'Erreur inconnue';
return { success: false, error: message, recordCount: 0 };
}
};
const openFileHandler = async (_event: Electron.IpcMainInvokeEvent, filePath: string) => {
await shell.openPath(filePath);
};
const openFolderHandler = async (_event: Electron.IpcMainInvokeEvent, folderPath: string) => {
shell.showItemInFolder(folderPath);
};
ipcMain.handle('export-data', exportDataHandler);
ipcMain.handle('open-file', openFileHandler);
ipcMain.handle('open-folder', openFolderHandler);
Testing Checklist
Related
Wire up the export functionality in the frontend to use the backend IPC handlers. Replace the fake progress with real IPC communication.
Requirements
Pre-Validation (NEW)
API Types
Preload API
IPC Handler - Preview
Export Action
UI - Export Dialog with Preview
i18n Keys
Dialog States
IPC Handler with File Operations
Testing Checklist
Related