Просмотр исходного кода

feat: implement call history export functionality #70

- Created ExportModal component with format selection (CSV, JSON, Excel)
- Implemented export utility functions for all three formats
- Added translation keys in English, Hungarian, and German
- Wired up Export button in CallLogsContent to open modal
- Export includes timestamp in filename
- Disabled export button when no data is available

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 месяцев назад
Родитель
Сommit
b4ed2897ea

+ 29 - 1
shopcall.ai-main/src/components/CallLogsContent.tsx

@@ -4,6 +4,8 @@ import { Card } from "@/components/ui/card";
 import { Search, Download, Calendar, Filter, Play, MoreHorizontal } from "lucide-react";
 import { useState, useEffect } from "react";
 import { CallDetailsModal } from "./CallDetailsModal";
+import { ExportModal, ExportFormat } from "./ExportModal";
+import { exportCallLogs } from "@/lib/exportUtils";
 import { API_URL } from "@/lib/config";
 import { useTranslation } from "react-i18next";
 
@@ -42,6 +44,7 @@ export function CallLogsContent() {
   const { t } = useTranslation();
   const [selectedCall, setSelectedCall] = useState<CallLog | null>(null);
   const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isExportModalOpen, setIsExportModalOpen] = useState(false);
   const [callLogs, setCallLogs] = useState<CallLog[]>([]);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
@@ -90,6 +93,20 @@ export function CallLogsContent() {
     setSelectedCall(null);
   };
 
+  const handleExportClick = () => {
+    setIsExportModalOpen(true);
+  };
+
+  const handleCloseExportModal = () => {
+    setIsExportModalOpen(false);
+  };
+
+  const handleExport = (format: ExportFormat) => {
+    const timestamp = new Date().toISOString().split('T')[0];
+    const filename = `call-logs-${timestamp}`;
+    exportCallLogs(callLogs, format, filename);
+  };
+
   return (
     <>
       <div className="flex-1 bg-slate-900 text-white">
@@ -102,7 +119,11 @@ export function CallLogsContent() {
             </div>
 
             <div className="flex items-center gap-4">
-              <Button className="bg-slate-700 hover:bg-slate-600 text-white">
+              <Button
+                className="bg-slate-700 hover:bg-slate-600 text-white"
+                onClick={handleExportClick}
+                disabled={isLoading || callLogs.length === 0}
+              >
                 <Download className="w-4 h-4 mr-2" />
                 {t('callLogs.export')}
               </Button>
@@ -254,6 +275,13 @@ export function CallLogsContent() {
           call={selectedCall}
         />
       )}
+
+      <ExportModal
+        isOpen={isExportModalOpen}
+        onClose={handleCloseExportModal}
+        data={callLogs}
+        onExport={handleExport}
+      />
     </>
   );
 }

+ 128 - 0
shopcall.ai-main/src/components/ExportModal.tsx

@@ -0,0 +1,128 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Label } from "@/components/ui/label";
+import { Download, FileJson, FileSpreadsheet, FileText } from "lucide-react";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+
+interface ExportModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  data: any[];
+  onExport: (format: ExportFormat) => void;
+}
+
+export type ExportFormat = "csv" | "json" | "excel";
+
+export function ExportModal({ isOpen, onClose, data, onExport }: ExportModalProps) {
+  const { t } = useTranslation();
+  const [selectedFormat, setSelectedFormat] = useState<ExportFormat>("csv");
+  const [isExporting, setIsExporting] = useState(false);
+
+  const handleExport = async () => {
+    setIsExporting(true);
+    try {
+      await onExport(selectedFormat);
+      // Close modal after successful export
+      setTimeout(() => {
+        onClose();
+        setIsExporting(false);
+      }, 500);
+    } catch (error) {
+      console.error("Export failed:", error);
+      setIsExporting(false);
+    }
+  };
+
+  const formatOptions = [
+    {
+      value: "csv",
+      label: t("callLogs.export.formats.csv"),
+      description: t("callLogs.export.formats.csvDescription"),
+      icon: FileText,
+    },
+    {
+      value: "json",
+      label: t("callLogs.export.formats.json"),
+      description: t("callLogs.export.formats.jsonDescription"),
+      icon: FileJson,
+    },
+    {
+      value: "excel",
+      label: t("callLogs.export.formats.excel"),
+      description: t("callLogs.export.formats.excelDescription"),
+      icon: FileSpreadsheet,
+    },
+  ];
+
+  return (
+    <Dialog open={isOpen} onOpenChange={onClose}>
+      <DialogContent className="max-w-md bg-slate-800 border-slate-700 text-white">
+        <DialogHeader>
+          <DialogTitle className="text-xl font-semibold text-white">
+            {t("callLogs.export.title")}
+          </DialogTitle>
+        </DialogHeader>
+
+        <div className="py-4">
+          <p className="text-slate-400 text-sm mb-4">
+            {t("callLogs.export.description", { count: data.length })}
+          </p>
+
+          <RadioGroup
+            value={selectedFormat}
+            onValueChange={(value) => setSelectedFormat(value as ExportFormat)}
+            className="space-y-3"
+          >
+            {formatOptions.map((option) => {
+              const Icon = option.icon;
+              return (
+                <div
+                  key={option.value}
+                  className={`flex items-start space-x-3 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
+                    selectedFormat === option.value
+                      ? "border-cyan-500 bg-cyan-500/10"
+                      : "border-slate-700 bg-slate-700/30 hover:border-slate-600"
+                  }`}
+                  onClick={() => setSelectedFormat(option.value as ExportFormat)}
+                >
+                  <RadioGroupItem value={option.value} id={option.value} className="mt-1" />
+                  <div className="flex-1">
+                    <Label
+                      htmlFor={option.value}
+                      className="flex items-center gap-2 cursor-pointer"
+                    >
+                      <Icon className="w-5 h-5 text-slate-400" />
+                      <span className="font-medium text-white">{option.label}</span>
+                    </Label>
+                    <p className="text-sm text-slate-400 mt-1">{option.description}</p>
+                  </div>
+                </div>
+              );
+            })}
+          </RadioGroup>
+        </div>
+
+        <DialogFooter className="flex gap-2 sm:gap-2">
+          <Button
+            variant="outline"
+            onClick={onClose}
+            className="border-slate-700 text-slate-300 hover:text-white hover:bg-slate-700"
+            disabled={isExporting}
+          >
+            {t("common.cancel")}
+          </Button>
+          <Button
+            onClick={handleExport}
+            className="bg-cyan-600 hover:bg-cyan-700 text-white"
+            disabled={isExporting}
+          >
+            <Download className="w-4 h-4 mr-2" />
+            {isExporting ? t("callLogs.export.downloading") : t("callLogs.export.download")}
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 14 - 0
shopcall.ai-main/src/i18n/locales/de.json

@@ -256,6 +256,20 @@
       "sentiment": "Stimmung",
       "cost": "Kosten",
       "actions": "Aktionen"
+    },
+    "export": {
+      "title": "Anrufprotokolle Exportieren",
+      "description": "{{count}} Anrufprotokolle in Ihrem bevorzugten Format exportieren",
+      "download": "Herunterladen",
+      "downloading": "Wird heruntergeladen...",
+      "formats": {
+        "csv": "CSV",
+        "csvDescription": "Komma-getrennte Werte, kompatibel mit Excel und Tabellenkalkulationen",
+        "json": "JSON",
+        "jsonDescription": "Strukturiertes Datenformat, ideal für Entwickler und Datenverarbeitung",
+        "excel": "Excel",
+        "excelDescription": "Microsoft Excel-Format, bereit zum Öffnen in Excel oder ähnlichen Apps"
+      }
     }
   },
   "analytics": {

+ 14 - 0
shopcall.ai-main/src/i18n/locales/en.json

@@ -256,6 +256,20 @@
       "sentiment": "Sentiment",
       "cost": "Cost",
       "actions": "Actions"
+    },
+    "export": {
+      "title": "Export Call Logs",
+      "description": "Export {{count}} call logs in your preferred format",
+      "download": "Download",
+      "downloading": "Downloading...",
+      "formats": {
+        "csv": "CSV",
+        "csvDescription": "Comma-separated values, compatible with Excel and spreadsheet apps",
+        "json": "JSON",
+        "jsonDescription": "Structured data format, ideal for developers and data processing",
+        "excel": "Excel",
+        "excelDescription": "Microsoft Excel format, ready to open in Excel or similar apps"
+      }
     }
   },
   "analytics": {

+ 14 - 0
shopcall.ai-main/src/i18n/locales/hu.json

@@ -256,6 +256,20 @@
       "sentiment": "Hangulat",
       "cost": "Költség",
       "actions": "Műveletek"
+    },
+    "export": {
+      "title": "Hívásnapló Exportálása",
+      "description": "{{count}} hívásnapló exportálása a választott formátumban",
+      "download": "Letöltés",
+      "downloading": "Letöltés...",
+      "formats": {
+        "csv": "CSV",
+        "csvDescription": "Vesszővel elválasztott értékek, kompatibilis Excellel és táblázatkezelőkkel",
+        "json": "JSON",
+        "jsonDescription": "Strukturált adatformátum, ideális fejlesztőknek és adatfeldolgozáshoz",
+        "excel": "Excel",
+        "excelDescription": "Microsoft Excel formátum, készen az Excelben vagy hasonló alkalmazásokban való megnyitásra"
+      }
     }
   },
   "analytics": {

+ 153 - 0
shopcall.ai-main/src/lib/exportUtils.ts

@@ -0,0 +1,153 @@
+import { ExportFormat } from "@/components/ExportModal";
+
+interface CallLog {
+  time: string;
+  customer: string;
+  intent: string;
+  outcome: string;
+  duration: string;
+  sentiment: string;
+  cost: string;
+  outcomeColor?: string;
+  sentimentColor?: string;
+}
+
+/**
+ * Export call logs to CSV format
+ */
+export function exportToCSV(data: CallLog[], filename: string = "call-logs"): void {
+  // Define CSV headers
+  const headers = ["Time", "Customer", "Intent", "Outcome", "Duration", "Sentiment", "Cost"];
+
+  // Convert data to CSV rows
+  const rows = data.map((log) => [
+    log.time,
+    log.customer,
+    log.intent,
+    log.outcome,
+    log.duration,
+    log.sentiment,
+    log.cost,
+  ]);
+
+  // Combine headers and rows
+  const csvContent = [
+    headers.join(","),
+    ...rows.map((row) => row.map((cell) => `"${cell}"`).join(",")),
+  ].join("\n");
+
+  // Create and download file
+  downloadFile(csvContent, `${filename}.csv`, "text/csv;charset=utf-8;");
+}
+
+/**
+ * Export call logs to JSON format
+ */
+export function exportToJSON(data: CallLog[], filename: string = "call-logs"): void {
+  // Clean data by removing color properties (outcomeColor, sentimentColor)
+  const cleanData = data.map(({ outcomeColor, sentimentColor, ...rest }) => rest);
+
+  const jsonContent = JSON.stringify(cleanData, null, 2);
+
+  downloadFile(jsonContent, `${filename}.json`, "application/json;charset=utf-8;");
+}
+
+/**
+ * Export call logs to Excel format (XLSX)
+ * Note: This creates a simple XML-based Excel file format
+ */
+export function exportToExcel(data: CallLog[], filename: string = "call-logs"): void {
+  // Define headers
+  const headers = ["Time", "Customer", "Intent", "Outcome", "Duration", "Sentiment", "Cost"];
+
+  // Create Excel XML content
+  let excelContent = `<?xml version="1.0"?>
+<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
+ xmlns:o="urn:schemas-microsoft-com:office:office"
+ xmlns:x="urn:schemas-microsoft-com:office:excel"
+ xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
+ xmlns:html="http://www.w3.org/TR/REC-html40">
+ <Worksheet ss:Name="Call Logs">
+  <Table>
+   <Row>`;
+
+  // Add headers
+  headers.forEach((header) => {
+    excelContent += `<Cell><Data ss:Type="String">${escapeXML(header)}</Data></Cell>`;
+  });
+  excelContent += `</Row>`;
+
+  // Add data rows
+  data.forEach((log) => {
+    excelContent += `<Row>
+     <Cell><Data ss:Type="String">${escapeXML(log.time)}</Data></Cell>
+     <Cell><Data ss:Type="String">${escapeXML(log.customer)}</Data></Cell>
+     <Cell><Data ss:Type="String">${escapeXML(log.intent)}</Data></Cell>
+     <Cell><Data ss:Type="String">${escapeXML(log.outcome)}</Data></Cell>
+     <Cell><Data ss:Type="String">${escapeXML(log.duration)}</Data></Cell>
+     <Cell><Data ss:Type="String">${escapeXML(log.sentiment)}</Data></Cell>
+     <Cell><Data ss:Type="String">${escapeXML(log.cost)}</Data></Cell>
+    </Row>`;
+  });
+
+  excelContent += `
+  </Table>
+ </Worksheet>
+</Workbook>`;
+
+  downloadFile(
+    excelContent,
+    `${filename}.xls`,
+    "application/vnd.ms-excel;charset=utf-8;"
+  );
+}
+
+/**
+ * Main export function that delegates to format-specific functions
+ */
+export function exportCallLogs(
+  data: CallLog[],
+  format: ExportFormat,
+  filename: string = "call-logs"
+): void {
+  switch (format) {
+    case "csv":
+      exportToCSV(data, filename);
+      break;
+    case "json":
+      exportToJSON(data, filename);
+      break;
+    case "excel":
+      exportToExcel(data, filename);
+      break;
+    default:
+      throw new Error(`Unsupported export format: ${format}`);
+  }
+}
+
+/**
+ * Helper function to download a file
+ */
+function downloadFile(content: string, filename: string, mimeType: string): void {
+  const blob = new Blob([content], { type: mimeType });
+  const url = URL.createObjectURL(blob);
+  const link = document.createElement("a");
+  link.href = url;
+  link.download = filename;
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  URL.revokeObjectURL(url);
+}
+
+/**
+ * Helper function to escape XML special characters
+ */
+function escapeXML(str: string): string {
+  return str
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&apos;");
+}