|
@@ -1,77 +1,72 @@
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Button } from "@/components/ui/button";
|
|
|
import { Input } from "@/components/ui/input";
|
|
import { Input } from "@/components/ui/input";
|
|
|
import { Card } from "@/components/ui/card";
|
|
import { Card } from "@/components/ui/card";
|
|
|
-import { Search, Download, Calendar, Filter, Play, MoreHorizontal } from "lucide-react";
|
|
|
|
|
|
|
+import { Search, Download, Calendar, Filter, ChevronRight } from "lucide-react";
|
|
|
import { useState, useEffect } from "react";
|
|
import { useState, useEffect } from "react";
|
|
|
-import { CallDetailsModal } from "./CallDetailsModal";
|
|
|
|
|
|
|
+import { useNavigate } from "react-router-dom";
|
|
|
import { ExportModal, ExportFormat } from "./ExportModal";
|
|
import { ExportModal, ExportFormat } from "./ExportModal";
|
|
|
import { exportCallLogs } from "@/lib/exportUtils";
|
|
import { exportCallLogs } from "@/lib/exportUtils";
|
|
|
-import { API_URL } from "@/lib/config";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
+import { supabase } from "@/integrations/supabase/client";
|
|
|
|
|
|
|
|
interface CallLog {
|
|
interface CallLog {
|
|
|
- time: string;
|
|
|
|
|
- customer: string;
|
|
|
|
|
- intent: string;
|
|
|
|
|
- outcome: string;
|
|
|
|
|
- duration: string;
|
|
|
|
|
- sentiment: string;
|
|
|
|
|
- cost: string;
|
|
|
|
|
- outcomeColor: string;
|
|
|
|
|
- sentimentColor: string;
|
|
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ store_id: string;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+ started_at: string | null;
|
|
|
|
|
+ ended_at: string | null;
|
|
|
|
|
+ duration: number | null;
|
|
|
|
|
+ caller: string | null;
|
|
|
|
|
+ transcript: string | null;
|
|
|
|
|
+ recording_url: string | null;
|
|
|
|
|
+ costs: any | null;
|
|
|
|
|
+ cost_total: number | null;
|
|
|
|
|
+ payload: any;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const maskPhoneNumber = (phone: string | undefined ) => {
|
|
|
|
|
- if (!phone) return "";
|
|
|
|
|
- const last4 = phone.slice(-4);
|
|
|
|
|
- return `...xxx-${last4}`;
|
|
|
|
|
|
|
+const formatDuration = (seconds: number | null): string => {
|
|
|
|
|
+ if (!seconds) return "-";
|
|
|
|
|
+ const mins = Math.floor(seconds / 60);
|
|
|
|
|
+ const secs = Math.round(seconds % 60);
|
|
|
|
|
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const formatDateTime = (dateStr: string | null): string => {
|
|
|
|
|
+ if (!dateStr) return "-";
|
|
|
|
|
+ const date = new Date(dateStr);
|
|
|
|
|
+ return date.toLocaleString();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const getSentimentEmoji = (sentiment: string) => {
|
|
|
|
|
- switch (sentiment) {
|
|
|
|
|
- case "Positive":
|
|
|
|
|
- return "😊";
|
|
|
|
|
- case "Negative":
|
|
|
|
|
- return "😠";
|
|
|
|
|
- case "Neutral":
|
|
|
|
|
- return "😐";
|
|
|
|
|
- default:
|
|
|
|
|
- return "😐";
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const maskPhoneNumber = (phone: string | null): string => {
|
|
|
|
|
+ if (!phone) return "-";
|
|
|
|
|
+ if (phone.length <= 4) return phone;
|
|
|
|
|
+ const last4 = phone.slice(-4);
|
|
|
|
|
+ return `***${last4}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
export function CallLogsContent() {
|
|
export function CallLogsContent() {
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
- const [selectedCall, setSelectedCall] = useState<CallLog | null>(null);
|
|
|
|
|
- const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
|
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
|
|
const [callLogs, setCallLogs] = useState<CallLog[]>([]);
|
|
const [callLogs, setCallLogs] = useState<CallLog[]>([]);
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
+ const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
|
|
|
|
const fetchCallLogs = async () => {
|
|
const fetchCallLogs = async () => {
|
|
|
setIsLoading(true);
|
|
setIsLoading(true);
|
|
|
setError(null);
|
|
setError(null);
|
|
|
try {
|
|
try {
|
|
|
- const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
- if (!sessionData) {
|
|
|
|
|
- throw new Error('No session data found');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { access_token } = JSON.parse(sessionData).session;
|
|
|
|
|
- const response = await fetch(`${API_URL}/api/call-logs`, {
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Authorization': `Bearer ${access_token}`,
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const { data, error: fetchError } = await supabase
|
|
|
|
|
+ .from('call_logs')
|
|
|
|
|
+ .select('*')
|
|
|
|
|
+ .order('created_at', { ascending: false });
|
|
|
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error('Failed to fetch call logs');
|
|
|
|
|
|
|
+ if (fetchError) {
|
|
|
|
|
+ throw fetchError;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const data = await response.json();
|
|
|
|
|
- setCallLogs(data.call_logs);
|
|
|
|
|
|
|
+ setCallLogs(data || []);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
|
} finally {
|
|
} finally {
|
|
@@ -83,14 +78,8 @@ export function CallLogsContent() {
|
|
|
fetchCallLogs();
|
|
fetchCallLogs();
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
- const handleDetailsClick = (call: CallLog) => {
|
|
|
|
|
- setSelectedCall(call);
|
|
|
|
|
- setIsModalOpen(true);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const handleCloseModal = () => {
|
|
|
|
|
- setIsModalOpen(false);
|
|
|
|
|
- setSelectedCall(null);
|
|
|
|
|
|
|
+ const handleRowClick = (callId: string) => {
|
|
|
|
|
+ navigate(`/call-logs/${callId}`);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleExportClick = () => {
|
|
const handleExportClick = () => {
|
|
@@ -104,9 +93,24 @@ export function CallLogsContent() {
|
|
|
const handleExport = (format: ExportFormat) => {
|
|
const handleExport = (format: ExportFormat) => {
|
|
|
const timestamp = new Date().toISOString().split('T')[0];
|
|
const timestamp = new Date().toISOString().split('T')[0];
|
|
|
const filename = `call-logs-${timestamp}`;
|
|
const filename = `call-logs-${timestamp}`;
|
|
|
- exportCallLogs(callLogs, format, filename);
|
|
|
|
|
|
|
+ const exportData = callLogs.map(log => ({
|
|
|
|
|
+ time: log.started_at || log.created_at,
|
|
|
|
|
+ caller: log.caller || '',
|
|
|
|
|
+ duration: formatDuration(log.duration),
|
|
|
|
|
+ cost: log.cost_total ? `$${log.cost_total.toFixed(4)}` : '',
|
|
|
|
|
+ }));
|
|
|
|
|
+ exportCallLogs(exportData, format, filename);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const filteredLogs = callLogs.filter(log => {
|
|
|
|
|
+ if (!searchQuery) return true;
|
|
|
|
|
+ const searchLower = searchQuery.toLowerCase();
|
|
|
|
|
+ return (
|
|
|
|
|
+ (log.caller && log.caller.toLowerCase().includes(searchLower)) ||
|
|
|
|
|
+ (log.transcript && log.transcript.toLowerCase().includes(searchLower))
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
|
<div className="flex-1 bg-slate-900 text-white">
|
|
<div className="flex-1 bg-slate-900 text-white">
|
|
@@ -144,6 +148,8 @@ export function CallLogsContent() {
|
|
|
<Input
|
|
<Input
|
|
|
placeholder={t('callLogs.searchPlaceholder')}
|
|
placeholder={t('callLogs.searchPlaceholder')}
|
|
|
className="pl-10 bg-slate-800 border-slate-700 text-white placeholder-slate-400"
|
|
className="pl-10 bg-slate-800 border-slate-700 text-white placeholder-slate-400"
|
|
|
|
|
+ value={searchQuery}
|
|
|
|
|
+ onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -206,56 +212,39 @@ export function CallLogsContent() {
|
|
|
{t('common.tryAgain')}
|
|
{t('common.tryAgain')}
|
|
|
</Button>
|
|
</Button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ ) : filteredLogs.length === 0 ? (
|
|
|
|
|
+ <div className="flex flex-col items-center justify-center py-12">
|
|
|
|
|
+ <p className="text-slate-400">{t('callLogs.noCalls')}</p>
|
|
|
|
|
+ </div>
|
|
|
) : (
|
|
) : (
|
|
|
<div className="overflow-x-auto">
|
|
<div className="overflow-x-auto">
|
|
|
<table className="w-full">
|
|
<table className="w-full">
|
|
|
<thead>
|
|
<thead>
|
|
|
<tr className="border-b border-slate-700">
|
|
<tr className="border-b border-slate-700">
|
|
|
- <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.time')}</th>
|
|
|
|
|
- <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.customer')}</th>
|
|
|
|
|
- <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.intent')}</th>
|
|
|
|
|
- <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.outcome')}</th>
|
|
|
|
|
|
|
+ <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.startedAt')}</th>
|
|
|
|
|
+ <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.endedAt')}</th>
|
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.duration')}</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.duration')}</th>
|
|
|
- <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.sentiment')}</th>
|
|
|
|
|
|
|
+ <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.caller')}</th>
|
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.cost')}</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.cost')}</th>
|
|
|
- <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.actions')}</th>
|
|
|
|
|
|
|
+ <th className="text-right py-3 px-4 text-sm font-medium text-slate-400"></th>
|
|
|
</tr>
|
|
</tr>
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody>
|
|
<tbody>
|
|
|
- {callLogs.map((call, index) => (
|
|
|
|
|
- <tr key={index} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
|
|
|
|
- <td className="py-3 px-4 text-sm text-slate-300">{call.time}</td>
|
|
|
|
|
- <td className="py-3 px-4 text-sm text-white">{maskPhoneNumber(call.customer)}</td>
|
|
|
|
|
- <td className="py-3 px-4 text-sm text-slate-300">{call.intent}</td>
|
|
|
|
|
- <td className="py-3 px-4">
|
|
|
|
|
- <span className={`text-sm font-medium ${call.outcomeColor}`}>
|
|
|
|
|
- {call.outcome}
|
|
|
|
|
- </span>
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="py-3 px-4 text-sm text-slate-300">{call.duration}</td>
|
|
|
|
|
- <td className="py-3 px-4">
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <span className="text-sm">{getSentimentEmoji(call.sentiment)}</span>
|
|
|
|
|
- <span className={`text-sm ${call.sentimentColor}`}>
|
|
|
|
|
- {call.sentiment}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {filteredLogs.map((call) => (
|
|
|
|
|
+ <tr
|
|
|
|
|
+ key={call.id}
|
|
|
|
|
+ className="border-b border-slate-700/50 hover:bg-slate-700/30 cursor-pointer transition-colors"
|
|
|
|
|
+ onClick={() => handleRowClick(call.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <td className="py-3 px-4 text-sm text-slate-300">{formatDateTime(call.started_at)}</td>
|
|
|
|
|
+ <td className="py-3 px-4 text-sm text-slate-300">{formatDateTime(call.ended_at)}</td>
|
|
|
|
|
+ <td className="py-3 px-4 text-sm text-slate-300">{formatDuration(call.duration)}</td>
|
|
|
|
|
+ <td className="py-3 px-4 text-sm text-white">{maskPhoneNumber(call.caller)}</td>
|
|
|
|
|
+ <td className="py-3 px-4 text-sm text-slate-300">
|
|
|
|
|
+ {call.cost_total ? `$${call.cost_total.toFixed(4)}` : '-'}
|
|
|
</td>
|
|
</td>
|
|
|
- <td className="py-3 px-4 text-sm text-slate-300">{call.cost}</td>
|
|
|
|
|
- <td className="py-3 px-4">
|
|
|
|
|
- <div className="flex items-center gap-2">
|
|
|
|
|
- <Button variant="ghost" size="sm" className="text-slate-400 hover:text-white">
|
|
|
|
|
- <Play className="w-4 h-4" />
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="sm"
|
|
|
|
|
- className="text-slate-400 hover:text-white"
|
|
|
|
|
- onClick={() => handleDetailsClick(call)}
|
|
|
|
|
- >
|
|
|
|
|
- <MoreHorizontal className="w-4 h-4" />
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <td className="py-3 px-4 text-right">
|
|
|
|
|
+ <ChevronRight className="w-4 h-4 text-slate-400 inline-block" />
|
|
|
</td>
|
|
</td>
|
|
|
</tr>
|
|
</tr>
|
|
|
))}
|
|
))}
|
|
@@ -268,14 +257,6 @@ export function CallLogsContent() {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {selectedCall && (
|
|
|
|
|
- <CallDetailsModal
|
|
|
|
|
- isOpen={isModalOpen}
|
|
|
|
|
- onClose={handleCloseModal}
|
|
|
|
|
- call={selectedCall}
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
<ExportModal
|
|
<ExportModal
|
|
|
isOpen={isExportModalOpen}
|
|
isOpen={isExportModalOpen}
|
|
|
onClose={handleCloseExportModal}
|
|
onClose={handleCloseExportModal}
|