From e8219f279154d0972de1f5a482b90ba20a6ec950 Mon Sep 17 00:00:00 2001 From: Paras Patel Date: Fri, 22 Aug 2025 15:50:22 -0700 Subject: [PATCH 1/5] feat: Add reusable export utilities for tables - Add exportUtils.ts with CSV, JSON, and clipboard functions - Implement formatFilename utility for consistent naming - Create ExportData interface for type safety - Support tab-separated clipboard format for spreadsheet compatibility - Include fallback clipboard method for older browsers --- app/utils/exportUtils.ts | 115 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 app/utils/exportUtils.ts diff --git a/app/utils/exportUtils.ts b/app/utils/exportUtils.ts new file mode 100644 index 00000000..6246427c --- /dev/null +++ b/app/utils/exportUtils.ts @@ -0,0 +1,115 @@ +export interface ExportData { + [key: string]: string | number | null | undefined; +} + +export function exportToCSV(data: ExportData[], filename: string): void { + if (!data || data.length === 0) { + console.warn('No data to export'); + return; + } + + const headers = Object.keys(data[0] || {}); + const csvContent = [ + headers.join(','), + ...data.map(row => + headers.map(header => { + const value = row[header]; + const stringValue = value === null || value === undefined ? '' : String(value); + return stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n') + ? `"${stringValue.replace(/"/g, '""')}"` + : stringValue; + }).join(',') + ) + ].join('\n'); + + downloadFile(csvContent, filename, 'text/csv;charset=utf-8;'); +} + +export function exportToJSON(data: ExportData[], filename: string): void { + if (!data || data.length === 0) { + console.warn('No data to export'); + return; + } + + const jsonContent = JSON.stringify(data, null, 2); + downloadFile(jsonContent, filename, 'application/json;charset=utf-8;'); +} + + +export async function copyTableToClipboard(data: ExportData[]): Promise { + if (!data || data.length === 0) { + console.warn('No data to copy'); + return; + } + + const headers = Object.keys(data[0] || {}); + + // Create tab-separated values (good for pasting into spreadsheets) + const headerRow = headers.join('\t'); + const dataRows = data.map(row => + headers.map(header => { + const value = row[header]; + return value === null || value === undefined ? '' : String(value); + }).join('\t') + ); + + const clipboardContent = [headerRow, ...dataRows].join('\n'); + + try { + await navigator.clipboard.writeText(clipboardContent); + // You could show a toast notification here + console.log('Table data copied to clipboard'); + } catch (err) { + console.error('Failed to copy to clipboard:', err); + // Fallback for older browsers + fallbackCopyToClipboard(clipboardContent); + } +} + +function fallbackCopyToClipboard(text: string): void { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.width = '2em'; + textArea.style.height = '2em'; + textArea.style.padding = '0'; + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + textArea.style.background = 'transparent'; + + document.body.appendChild(textArea); + textArea.select(); + + try { + document.execCommand('copy'); + console.log('Table data copied to clipboard (fallback)'); + } catch (err) { + console.error('Fallback copy failed:', err); + } + + document.body.removeChild(textArea); +} + +function downloadFile(content: string, filename: string, mimeType: string): void { + const blob = new Blob([content], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.style.display = 'none'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + window.URL.revokeObjectURL(url); +} + +export function formatFilename(baseName: string, extension: string): string { + const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-'); + return `${baseName}_${timestamp}.${extension}`; +} \ No newline at end of file From bda1e47450aadce9612d1b11c0246aec3c64ae31 Mon Sep 17 00:00:00 2001 From: Paras Patel Date: Fri, 22 Aug 2025 15:50:44 -0700 Subject: [PATCH 2/5] feat: Add CSV, JSON, and copy export to Seats Analysis table - Add export dropdown with 3 options positioned in header area - Export includes all seat records regardless of pagination - Support CSV with proper escaping, JSON with camelCase, and clipboard copy - Clean UI with export button showing total record count - Consistent export pattern for future table implementations --- app/components/SeatsAnalysisViewer.vue | 83 +++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/app/components/SeatsAnalysisViewer.vue b/app/components/SeatsAnalysisViewer.vue index 5ce7364d..b3a439eb 100644 --- a/app/components/SeatsAnalysisViewer.vue +++ b/app/components/SeatsAnalysisViewer.vue @@ -87,8 +87,34 @@ elevation="4" color="white" variant="elevated" class="mx-auto my-3"
-

All assigned seats

-
+
+

All assigned seats

+ + + + + Export as CSV + + + Export as JSON + + + Copy to Clipboard + + + +