/** * CSV Export Utilities * * Client-side CSV generation for policy settings export. * * Features: * - RFC 4180 compliant CSV formatting * - Proper escaping (commas, quotes, newlines) * - UTF-8 BOM for Excel compatibility * - Efficient string building */ import type { PolicySettingRow } from '@/lib/types/policy-table'; /** * Escape a CSV field value according to RFC 4180 * - Wrap in quotes if contains comma, quote, or newline * - Double any quotes inside the value */ function escapeCsvField(value: string | null | undefined): string { if (value === null || value === undefined) { return ''; } const stringValue = String(value); // Check if escaping is needed const needsEscaping = stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n') || stringValue.includes('\r'); if (!needsEscaping) { return stringValue; } // Double any quotes and wrap in quotes const escaped = stringValue.replace(/"/g, '""'); return `"${escaped}"`; } /** * Generate CSV content from policy settings * @param rows Array of policy settings to export * @param columns Optional array of column keys to include (default: all) * @returns CSV content as string with UTF-8 BOM */ export function generatePolicySettingsCsv( rows: PolicySettingRow[], columns?: Array ): string { // Default columns in preferred order const defaultColumns: Array = [ 'settingName', 'settingValue', 'policyName', 'policyType', 'graphPolicyId', 'lastSyncedAt', ]; const columnsToExport = columns || defaultColumns; // Column headers (human-readable) const headers: Record = { id: 'ID', tenantId: 'Tenant ID', settingName: 'Setting Name', settingValue: 'Setting Value', policyName: 'Policy Name', policyType: 'Policy Type', graphPolicyId: 'Graph Policy ID', lastSyncedAt: 'Last Synced', createdAt: 'Created At', }; // Build CSV rows const csvLines: string[] = []; // Header row const headerRow = columnsToExport.map(col => escapeCsvField(headers[col])).join(','); csvLines.push(headerRow); // Data rows for (const row of rows) { const dataRow = columnsToExport.map(col => { const value = row[col]; // Format dates if (value instanceof Date) { return escapeCsvField(value.toISOString()); } // Format other values return escapeCsvField(String(value)); }).join(','); csvLines.push(dataRow); } // Join with newlines and add UTF-8 BOM for Excel compatibility const csvContent = csvLines.join('\n'); const utf8Bom = '\uFEFF'; return utf8Bom + csvContent; } /** * Trigger browser download of CSV content * @param csvContent CSV content string * @param filename Suggested filename (e.g., "policy-settings.csv") */ export function downloadCsv(csvContent: string, filename: string): void { // Create blob with proper MIME type const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); // Create download link const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; // Trigger download document.body.appendChild(link); link.click(); // Cleanup document.body.removeChild(link); URL.revokeObjectURL(url); } /** * Generate filename with timestamp * @param prefix Filename prefix (e.g., "policy-settings") * @param selectedCount Optional count of selected rows * @returns Filename with timestamp (e.g., "policy-settings-2025-12-10.csv") */ export function generateCsvFilename(prefix: string, selectedCount?: number): string { const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const suffix = selectedCount ? `-selected-${selectedCount}` : ''; return `${prefix}${suffix}-${date}.csv`; }