|
@@ -2,10 +2,12 @@ import { useState, useEffect, useRef } from "react";
|
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Button } from "@/components/ui/button";
|
|
|
import { Card } from "@/components/ui/card";
|
|
import { Card } from "@/components/ui/card";
|
|
|
-import { ArrowLeft, Phone, Clock, Calendar, Bot, User, Play, Pause } from "lucide-react";
|
|
|
|
|
|
|
+import { ArrowLeft, Phone, Clock, Calendar, X, Play, Pause, Volume2, RefreshCw, User, ShoppingCart, Package, AlertCircle, CheckCircle, Loader2 } from "lucide-react";
|
|
|
import { useTranslation } from "react-i18next";
|
|
import { useTranslation } from "react-i18next";
|
|
|
import { supabase } from "@/lib/supabase";
|
|
import { supabase } from "@/lib/supabase";
|
|
|
-import { API_URL } from "@/lib/config";
|
|
|
|
|
|
|
+import { API_URL, ENABLE_MANUAL_ANALYTICS } from "@/lib/config";
|
|
|
|
|
+import ReactMarkdown from 'react-markdown';
|
|
|
|
|
+import { AnalyticsStatus, ANALYTICS_STATUS, RelatedCustomer, RelatedOrder, RelatedProduct } from "@/types/database.types";
|
|
|
|
|
|
|
|
interface CallLog {
|
|
interface CallLog {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -20,12 +22,26 @@ interface CallLog {
|
|
|
costs: any | null;
|
|
costs: any | null;
|
|
|
cost_total: number | null;
|
|
cost_total: number | null;
|
|
|
payload: any;
|
|
payload: any;
|
|
|
|
|
+ outcome: string | null;
|
|
|
|
|
+ summary: string | null;
|
|
|
|
|
+ satisfaction_score: number | null;
|
|
|
|
|
+ related_customers: RelatedCustomer[] | null;
|
|
|
|
|
+ related_orders: RelatedOrder[] | null;
|
|
|
|
|
+ related_products: RelatedProduct[] | null;
|
|
|
|
|
+ analytics_status: AnalyticsStatus | null;
|
|
|
|
|
+ analytics_error: string | null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-interface TranscriptMessage {
|
|
|
|
|
- role: 'ai' | 'user';
|
|
|
|
|
- content: string;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// Helper function to get satisfaction emoji and color based on score
|
|
|
|
|
+const getSatisfactionDisplay = (score: number | null): { emoji: string; label: string; color: string } => {
|
|
|
|
|
+ if (score === null) return { emoji: '❓', label: 'Unknown', color: 'text-slate-400' };
|
|
|
|
|
+
|
|
|
|
|
+ if (score >= 81) return { emoji: '😍', label: 'Very Satisfied', color: 'text-green-400' };
|
|
|
|
|
+ if (score >= 61) return { emoji: '😊', label: 'Satisfied', color: 'text-lime-400' };
|
|
|
|
|
+ if (score >= 41) return { emoji: '😐', label: 'Neutral', color: 'text-yellow-400' };
|
|
|
|
|
+ if (score >= 21) return { emoji: '😞', label: 'Dissatisfied', color: 'text-orange-400' };
|
|
|
|
|
+ return { emoji: '😠', label: 'Very Dissatisfied', color: 'text-red-400' };
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
const formatDuration = (seconds: number | null): string => {
|
|
const formatDuration = (seconds: number | null): string => {
|
|
|
if (!seconds) return "-";
|
|
if (!seconds) return "-";
|
|
@@ -34,41 +50,78 @@ const formatDuration = (seconds: number | null): string => {
|
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const formatDateTime = (dateStr: string | null): string => {
|
|
|
|
|
|
|
+const getRelativeTime = (dateStr: string | null, t: (key: string, options?: any) => string): string => {
|
|
|
if (!dateStr) return "-";
|
|
if (!dateStr) return "-";
|
|
|
const date = new Date(dateStr);
|
|
const date = new Date(dateStr);
|
|
|
- return date.toLocaleString();
|
|
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const diffMs = now.getTime() - date.getTime();
|
|
|
|
|
+ const diffMins = Math.floor(diffMs / 60000);
|
|
|
|
|
+ const diffHours = Math.floor(diffMins / 60);
|
|
|
|
|
+ const diffDays = Math.floor(diffHours / 24);
|
|
|
|
|
+
|
|
|
|
|
+ if (diffMins < 1) return t('common.justNow');
|
|
|
|
|
+ if (diffMins < 60) return t('common.minsAgo', { count: diffMins });
|
|
|
|
|
+ if (diffHours < 24) return t('common.hoursAgo', { count: diffHours });
|
|
|
|
|
+ return t('common.daysAgo', { count: diffDays });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const parseTranscript = (transcript: string | null): TranscriptMessage[] => {
|
|
|
|
|
- if (!transcript) return [];
|
|
|
|
|
|
|
+const getOutcomeColor = (outcome: string | null): string => {
|
|
|
|
|
+ if (!outcome) return "bg-slate-600";
|
|
|
|
|
+
|
|
|
|
|
+ const colorMap: Record<string, string> = {
|
|
|
|
|
+ resolved: "bg-green-600",
|
|
|
|
|
+ order_placed: "bg-green-600",
|
|
|
|
|
+ order_inquiry: "bg-blue-600",
|
|
|
|
|
+ product_inquiry: "bg-blue-600",
|
|
|
|
|
+ general_question: "bg-blue-600",
|
|
|
|
|
+ complaint: "bg-red-600",
|
|
|
|
|
+ return_request: "bg-orange-600",
|
|
|
|
|
+ callback_requested: "bg-purple-600",
|
|
|
|
|
+ escalated: "bg-yellow-600",
|
|
|
|
|
+ no_answer: "bg-slate-500",
|
|
|
|
|
+ voicemail: "bg-slate-500",
|
|
|
|
|
+ wrong_number: "bg-slate-500",
|
|
|
|
|
+ abandoned: "bg-slate-500",
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- const messages: TranscriptMessage[] = [];
|
|
|
|
|
- const lines = transcript.split('\n');
|
|
|
|
|
|
|
+ return colorMap[outcome] || "bg-slate-600";
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- for (const line of lines) {
|
|
|
|
|
- const trimmedLine = line.trim();
|
|
|
|
|
- if (!trimmedLine) continue;
|
|
|
|
|
|
|
+const formatOutcome = (outcome: string | null): string => {
|
|
|
|
|
+ if (!outcome) return "-";
|
|
|
|
|
+ return outcome.split('_').map(word =>
|
|
|
|
|
+ word.charAt(0).toUpperCase() + word.slice(1)
|
|
|
|
|
+ ).join(' ');
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- if (trimmedLine.startsWith('AI:')) {
|
|
|
|
|
- messages.push({
|
|
|
|
|
- role: 'ai',
|
|
|
|
|
- content: trimmedLine.substring(3).trim()
|
|
|
|
|
- });
|
|
|
|
|
- } else if (trimmedLine.startsWith('User:')) {
|
|
|
|
|
- messages.push({
|
|
|
|
|
- role: 'user',
|
|
|
|
|
- content: trimmedLine.substring(5).trim()
|
|
|
|
|
- });
|
|
|
|
|
- } else {
|
|
|
|
|
- // If line doesn't have a prefix, append to last message or create new one
|
|
|
|
|
- if (messages.length > 0) {
|
|
|
|
|
- messages[messages.length - 1].content += ' ' + trimmedLine;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const formatTranscript = (transcript: string | null): React.ReactNode => {
|
|
|
|
|
+ if (!transcript) return null;
|
|
|
|
|
+
|
|
|
|
|
+ // Split by newlines and process each line
|
|
|
|
|
+ const lines = transcript.split('\n').filter(line => line.trim());
|
|
|
|
|
+
|
|
|
|
|
+ return lines.map((line, index) => {
|
|
|
|
|
+ // Remove AI:, User:, Assistant:, Customer: prefixes (case insensitive)
|
|
|
|
|
+ let cleanLine = line.replace(/^(AI|User|Assistant|Customer|Bot|Agent):\s*/i, '').trim();
|
|
|
|
|
+
|
|
|
|
|
+ // Determine if this was an AI/assistant line or user/customer line
|
|
|
|
|
+ const isAI = /^(AI|Assistant|Bot|Agent):/i.test(line);
|
|
|
|
|
+ const isUser = /^(User|Customer):/i.test(line);
|
|
|
|
|
+
|
|
|
|
|
+ // Add emoji based on speaker
|
|
|
|
|
+ let emoji = '';
|
|
|
|
|
+ if (isAI) {
|
|
|
|
|
+ emoji = '🤖 ';
|
|
|
|
|
+ } else if (isUser) {
|
|
|
|
|
+ emoji = '👤 ';
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- return messages;
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <p key={index} className={`text-slate-300 text-sm ${index > 0 ? 'mt-2' : ''}`}>
|
|
|
|
|
+ {emoji}{cleanLine}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
export function CallLogDetailContent() {
|
|
export function CallLogDetailContent() {
|
|
@@ -79,8 +132,75 @@ export function CallLogDetailContent() {
|
|
|
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 [isPlaying, setIsPlaying] = useState(false);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
|
+ const [currentTime, setCurrentTime] = useState(0);
|
|
|
|
|
+ const [isAnalyticsTriggering, setIsAnalyticsTriggering] = useState(false);
|
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
|
|
|
|
|
|
|
+ const handleTriggerAnalytics = async () => {
|
|
|
|
|
+ if (!id || !callLog) return;
|
|
|
|
|
+
|
|
|
|
|
+ setIsAnalyticsTriggering(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
|
+ if (!session) {
|
|
|
|
|
+ throw new Error('Not authenticated');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(`${API_URL}/api/call-logs/${id}/trigger-analytics`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ const errorData = await response.json();
|
|
|
|
|
+ throw new Error(errorData.error || 'Failed to trigger analytics');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update local state to show running status
|
|
|
|
|
+ setCallLog({
|
|
|
|
|
+ ...callLog,
|
|
|
|
|
+ analytics_status: ANALYTICS_STATUS.RUNNING,
|
|
|
|
|
+ analytics_error: null,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Start polling for updates
|
|
|
|
|
+ const pollInterval = setInterval(async () => {
|
|
|
|
|
+ const pollResponse = await fetch(`${API_URL}/api/call-logs/${id}`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (pollResponse.ok) {
|
|
|
|
|
+ const data = await pollResponse.json();
|
|
|
|
|
+ setCallLog(data.call_log);
|
|
|
|
|
+
|
|
|
|
|
+ // Stop polling if completed or failed
|
|
|
|
|
+ if (data.call_log.analytics_status !== ANALYTICS_STATUS.RUNNING) {
|
|
|
|
|
+ clearInterval(pollInterval);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 3000); // Poll every 3 seconds
|
|
|
|
|
+
|
|
|
|
|
+ // Stop polling after 2 minutes max
|
|
|
|
|
+ setTimeout(() => clearInterval(pollInterval), 120000);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to trigger analytics:', err);
|
|
|
|
|
+ setCallLog({
|
|
|
|
|
+ ...callLog,
|
|
|
|
|
+ analytics_status: ANALYTICS_STATUS.FAILED,
|
|
|
|
|
+ analytics_error: err instanceof Error ? err.message : 'Failed to trigger analytics',
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsAnalyticsTriggering(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const fetchCallLog = async () => {
|
|
const fetchCallLog = async () => {
|
|
|
if (!id) return;
|
|
if (!id) return;
|
|
@@ -135,9 +255,14 @@ export function CallLogDetailContent() {
|
|
|
|
|
|
|
|
const handleAudioEnded = () => {
|
|
const handleAudioEnded = () => {
|
|
|
setIsPlaying(false);
|
|
setIsPlaying(false);
|
|
|
|
|
+ setCurrentTime(0);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const messages = parseTranscript(callLog?.transcript || null);
|
|
|
|
|
|
|
+ const handleTimeUpdate = () => {
|
|
|
|
|
+ if (audioRef.current) {
|
|
|
|
|
+ setCurrentTime(audioRef.current.currentTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
if (isLoading) {
|
|
|
return (
|
|
return (
|
|
@@ -179,202 +304,397 @@ export function CallLogDetailContent() {
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="flex-1 bg-slate-900 text-white overflow-hidden flex flex-col">
|
|
<div className="flex-1 bg-slate-900 text-white overflow-hidden flex flex-col">
|
|
|
- {/* Header */}
|
|
|
|
|
- <div className="border-b border-slate-800 bg-slate-900">
|
|
|
|
|
- <div className="flex items-center gap-4 p-6">
|
|
|
|
|
|
|
+ {/* Modal Header */}
|
|
|
|
|
+ <div className="border-b border-slate-800 bg-slate-900 p-6">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <h1 className="text-xl font-semibold text-white">{t('callLogDetail.title')}</h1>
|
|
|
<Button
|
|
<Button
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
size="sm"
|
|
size="sm"
|
|
|
className="text-slate-400 hover:text-white"
|
|
className="text-slate-400 hover:text-white"
|
|
|
onClick={handleBack}
|
|
onClick={handleBack}
|
|
|
>
|
|
>
|
|
|
- <ArrowLeft className="w-4 h-4 mr-2" />
|
|
|
|
|
- {t('common.back')}
|
|
|
|
|
|
|
+ <X className="w-5 h-5" />
|
|
|
</Button>
|
|
</Button>
|
|
|
- <div>
|
|
|
|
|
- <h1 className="text-2xl font-semibold text-white">
|
|
|
|
|
- {t('callLogDetail.title')}{callLog.caller ? ` - ${callLog.caller}` : ''}
|
|
|
|
|
- </h1>
|
|
|
|
|
- <p className="text-slate-400">{formatDateTime(callLog.started_at)}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Content - Two Column Layout */}
|
|
{/* Content - Two Column Layout */}
|
|
|
- <div className="flex-1 overflow-hidden p-6">
|
|
|
|
|
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full">
|
|
|
|
|
- {/* Left Column - Call Statistics */}
|
|
|
|
|
- <div className="lg:col-span-1 space-y-6">
|
|
|
|
|
- {/* Call Info Card */}
|
|
|
|
|
- <Card className="bg-slate-800 border-slate-700 p-6">
|
|
|
|
|
- <h3 className="text-lg font-semibold text-white mb-4">{t('callLogDetail.callInfo')}</h3>
|
|
|
|
|
-
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <div className="w-10 h-10 bg-cyan-500/10 rounded-lg flex items-center justify-center">
|
|
|
|
|
- <Calendar className="w-5 h-5 text-cyan-400" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm text-slate-400">{t('callLogDetail.startedAt')}</p>
|
|
|
|
|
- <p className="text-white">{formatDateTime(callLog.started_at)}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div className="flex-1 overflow-y-auto p-6">
|
|
|
|
|
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
|
+ {/* Left Column - Contact Information */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700 p-6">
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-6">
|
|
|
|
|
+ <Phone className="w-5 h-5 text-slate-400" />
|
|
|
|
|
+ <h3 className="text-lg font-semibold text-white">{t('callLogDetail.contactInfo')}</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Phone Number with Badge */}
|
|
|
|
|
+ <div className="mb-4">
|
|
|
|
|
+ <p className="text-slate-400 text-sm mb-1">{t('callLogDetail.caller')}</p>
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <span className="text-white font-medium">{callLog.caller || '-'}</span>
|
|
|
|
|
+ {callLog.outcome && (
|
|
|
|
|
+ <span className={`px-2 py-0.5 rounded text-xs font-medium text-white ${getOutcomeColor(callLog.outcome)}`}>
|
|
|
|
|
+ {formatOutcome(callLog.outcome)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Phone with icon */}
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <Phone className="w-4 h-4 text-slate-400" />
|
|
|
|
|
+ <span className="text-slate-300">{callLog.caller || '-'}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Added time */}
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-6">
|
|
|
|
|
+ <Calendar className="w-4 h-4 text-slate-400" />
|
|
|
|
|
+ <span className="text-slate-300">{getRelativeTime(callLog.created_at, t)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Stats Grid - 2x3 */}
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-4 mb-6">
|
|
|
|
|
+ {/* Total Calls */}
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-4 text-center">
|
|
|
|
|
+ <div className="text-2xl font-bold text-white">1</div>
|
|
|
|
|
+ <div className="text-sm text-slate-400">{t('callLogDetail.totalCalls')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <div className="w-10 h-10 bg-cyan-500/10 rounded-lg flex items-center justify-center">
|
|
|
|
|
- <Calendar className="w-5 h-5 text-cyan-400" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm text-slate-400">{t('callLogDetail.endedAt')}</p>
|
|
|
|
|
- <p className="text-white">{formatDateTime(callLog.ended_at)}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* Total Duration */}
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-4 text-center">
|
|
|
|
|
+ <div className="text-2xl font-bold text-white">{formatDuration(callLog.duration)}</div>
|
|
|
|
|
+ <div className="text-sm text-slate-400">{t('callLogDetail.totalDuration')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <div className="w-10 h-10 bg-green-500/10 rounded-lg flex items-center justify-center">
|
|
|
|
|
- <Phone className="w-5 h-5 text-green-400" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm text-slate-400">{t('callLogDetail.caller')}</p>
|
|
|
|
|
- <p className="text-white">{callLog.caller || '-'}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* Total Cost */}
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-4 text-center">
|
|
|
|
|
+ <div className="text-2xl font-bold text-white">
|
|
|
|
|
+ {callLog.cost_total ? `$${callLog.cost_total.toFixed(2)}` : '-'}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div className="text-sm text-slate-400">{t('callLogDetail.totalCost')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <div className="w-10 h-10 bg-purple-500/10 rounded-lg flex items-center justify-center">
|
|
|
|
|
- <Clock className="w-5 h-5 text-purple-400" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm text-slate-400">{t('callLogDetail.duration')}</p>
|
|
|
|
|
- <p className="text-white">{formatDuration(callLog.duration)}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* Outcome in grid */}
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-4 text-center">
|
|
|
|
|
+ <div className="text-lg font-bold text-white">{formatOutcome(callLog.outcome)}</div>
|
|
|
|
|
+ <div className="text-sm text-slate-400">{t('callLogDetail.callOutcome')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- {/* Cost item hidden - cost calculation not fully implemented
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <div className="w-10 h-10 bg-yellow-500/10 rounded-lg flex items-center justify-center">
|
|
|
|
|
- <DollarSign className="w-5 h-5 text-yellow-400" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div>
|
|
|
|
|
- <p className="text-sm text-slate-400">{t('callLogDetail.cost')}</p>
|
|
|
|
|
- <p className="text-white">
|
|
|
|
|
- {callLog.cost_total ? `$${callLog.cost_total.toFixed(4)}` : '-'}
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ {/* Customer Satisfaction */}
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-4 text-center col-span-2">
|
|
|
|
|
+ {(() => {
|
|
|
|
|
+ const satisfaction = getSatisfactionDisplay(callLog.satisfaction_score);
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div className="flex items-center justify-center gap-2">
|
|
|
|
|
+ <span className="text-4xl">{satisfaction.emoji}</span>
|
|
|
|
|
+ <div className="text-left">
|
|
|
|
|
+ <div className={`text-2xl font-bold ${satisfaction.color}`}>
|
|
|
|
|
+ {callLog.satisfaction_score !== null ? `${callLog.satisfaction_score}%` : '-'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className={`text-sm ${satisfaction.color}`}>{satisfaction.label}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-sm text-slate-400 mt-1">{t('callLogDetail.satisfaction')}</div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+ })()}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Call Outcome Box */}
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-4 text-center">
|
|
|
|
|
+ <div className={`inline-block px-4 py-2 rounded-lg text-lg font-bold text-white ${getOutcomeColor(callLog.outcome)}`}>
|
|
|
|
|
+ {formatOutcome(callLog.outcome)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-sm text-slate-400 mt-2">{t('callLogDetail.callOutcome')}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Right Column - Call History */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700 p-6">
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-6">
|
|
|
|
|
+ <Calendar className="w-5 h-5 text-slate-400" />
|
|
|
|
|
+ <h3 className="text-lg font-semibold text-white">{t('callLogDetail.callHistory')}</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Call Header */}
|
|
|
|
|
+ <div className="flex items-center justify-between mb-6">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Calendar className="w-4 h-4 text-slate-400" />
|
|
|
|
|
+ <span className="text-slate-300">{getRelativeTime(callLog.started_at, t)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ {callLog.outcome && (
|
|
|
|
|
+ <span className={`px-2 py-0.5 rounded text-xs font-medium text-white ${getOutcomeColor(callLog.outcome)}`}>
|
|
|
|
|
+ {formatOutcome(callLog.outcome)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <span className="text-slate-300 flex items-center gap-1">
|
|
|
|
|
+ <Clock className="w-4 h-4" />
|
|
|
|
|
+ {formatDuration(callLog.duration)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Transcript */}
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <h4 className="text-white font-medium mb-2">{t('callLogDetail.transcript')}</h4>
|
|
|
|
|
+ <div className="bg-slate-700/30 rounded-lg p-4 max-h-48 overflow-y-auto">
|
|
|
|
|
+ {callLog.transcript ? (
|
|
|
|
|
+ formatTranscript(callLog.transcript)
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-slate-400 text-sm">{t('callLogDetail.noTranscript')}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Summary */}
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <h4 className="text-white font-medium mb-2">{t('callLogDetail.summary')}</h4>
|
|
|
|
|
+ <div className="bg-teal-900/30 border border-teal-700/50 rounded-lg p-4">
|
|
|
|
|
+ {callLog.summary ? (
|
|
|
|
|
+ <div className="text-slate-200 text-sm prose prose-sm prose-invert max-w-none prose-p:my-1 prose-ul:my-1 prose-li:my-0">
|
|
|
|
|
+ <ReactMarkdown>{callLog.summary}</ReactMarkdown>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- */}
|
|
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <p className="text-slate-400 text-sm">-</p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- </Card>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Call Outcome */}
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <span className="text-slate-400">{t('callLogDetail.callOutcome')}:</span>
|
|
|
|
|
+ <span className={`px-2 py-0.5 rounded text-sm font-medium text-white ${getOutcomeColor(callLog.outcome)}`}>
|
|
|
|
|
+ {formatOutcome(callLog.outcome)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- {/* Audio Player Card */}
|
|
|
|
|
|
|
+ {/* Call Recording */}
|
|
|
{callLog.recording_url && (
|
|
{callLog.recording_url && (
|
|
|
- <Card className="bg-slate-800 border-slate-700 p-6">
|
|
|
|
|
- <h3 className="text-lg font-semibold text-white mb-4">{t('callLogDetail.recording')}</h3>
|
|
|
|
|
-
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <h4 className="text-white font-medium mb-2">{t('callLogDetail.recording')}</h4>
|
|
|
|
|
+ <div className="bg-slate-700/30 rounded-lg p-4">
|
|
|
<audio
|
|
<audio
|
|
|
ref={audioRef}
|
|
ref={audioRef}
|
|
|
src={callLog.recording_url}
|
|
src={callLog.recording_url}
|
|
|
onEnded={handleAudioEnded}
|
|
onEnded={handleAudioEnded}
|
|
|
|
|
+ onTimeUpdate={handleTimeUpdate}
|
|
|
className="hidden"
|
|
className="hidden"
|
|
|
/>
|
|
/>
|
|
|
-
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-4">
|
|
|
<Button
|
|
<Button
|
|
|
onClick={togglePlayPause}
|
|
onClick={togglePlayPause}
|
|
|
- className="w-12 h-12 rounded-full bg-cyan-600 hover:bg-cyan-500 flex items-center justify-center"
|
|
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ className="w-10 h-10 rounded-full bg-slate-600 hover:bg-slate-500 flex items-center justify-center p-0"
|
|
|
>
|
|
>
|
|
|
{isPlaying ? (
|
|
{isPlaying ? (
|
|
|
- <Pause className="w-5 h-5" />
|
|
|
|
|
|
|
+ <Pause className="w-4 h-4" />
|
|
|
) : (
|
|
) : (
|
|
|
- <Play className="w-5 h-5 ml-0.5" />
|
|
|
|
|
|
|
+ <Play className="w-4 h-4 ml-0.5" />
|
|
|
)}
|
|
)}
|
|
|
</Button>
|
|
</Button>
|
|
|
<div className="flex-1">
|
|
<div className="flex-1">
|
|
|
- <p className="text-sm text-slate-400">{t('callLogDetail.clickToPlay')}</p>
|
|
|
|
|
- <p className="text-white text-sm">{formatDuration(callLog.duration)}</p>
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2 text-sm text-slate-400">
|
|
|
|
|
+ <span>{formatDuration(Math.floor(currentTime))}</span>
|
|
|
|
|
+ <div className="flex-1 h-1 bg-slate-600 rounded-full overflow-hidden">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="h-full bg-cyan-500 transition-all"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: callLog.duration
|
|
|
|
|
+ ? `${(currentTime / callLog.duration) * 100}%`
|
|
|
|
|
+ : '0%'
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span>{formatDuration(callLog.duration)}</span>
|
|
|
|
|
+ <Volume2 className="w-4 h-4" />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {/* Native audio controls as fallback */}
|
|
|
|
|
- <audio
|
|
|
|
|
- src={callLog.recording_url}
|
|
|
|
|
- controls
|
|
|
|
|
- className="w-full mt-2"
|
|
|
|
|
- />
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- </Card>
|
|
|
|
|
|
|
+ </div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {/* Costs Breakdown Card - hidden, cost calculation not fully implemented
|
|
|
|
|
- {callLog.costs && Array.isArray(callLog.costs) && callLog.costs.length > 0 && (
|
|
|
|
|
- <Card className="bg-slate-800 border-slate-700 p-6">
|
|
|
|
|
- <h3 className="text-lg font-semibold text-white mb-4">{t('callLogDetail.costsBreakdown')}</h3>
|
|
|
|
|
-
|
|
|
|
|
- <div className="space-y-2">
|
|
|
|
|
- {callLog.costs.map((cost: any, index: number) => (
|
|
|
|
|
- <div key={index} className="flex justify-between items-center py-2 border-b border-slate-700 last:border-0">
|
|
|
|
|
- <span className="text-slate-400 text-sm capitalize">{cost.type}</span>
|
|
|
|
|
- <span className="text-white text-sm">${cost.cost?.toFixed(4) || '0.0000'}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- <div className="flex justify-between items-center pt-2 border-t border-slate-600">
|
|
|
|
|
- <span className="text-white font-medium">{t('callLogDetail.total')}</span>
|
|
|
|
|
- <span className="text-cyan-400 font-medium">
|
|
|
|
|
- ${callLog.cost_total?.toFixed(4) || '0.0000'}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* Analytics Status Banner */}
|
|
|
|
|
+ {callLog.analytics_status && callLog.analytics_status !== ANALYTICS_STATUS.COMPLETED && (
|
|
|
|
|
+ <div className={`mb-6 p-4 rounded-lg flex items-center justify-between ${
|
|
|
|
|
+ callLog.analytics_status === ANALYTICS_STATUS.RUNNING
|
|
|
|
|
+ ? 'bg-blue-900/30 border border-blue-700/50'
|
|
|
|
|
+ : callLog.analytics_status === ANALYTICS_STATUS.FAILED
|
|
|
|
|
+ ? 'bg-red-900/30 border border-red-700/50'
|
|
|
|
|
+ : 'bg-slate-700/30 border border-slate-600/50'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ {callLog.analytics_status === ANALYTICS_STATUS.RUNNING && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="w-5 h-5 text-blue-400 animate-spin" />
|
|
|
|
|
+ <span className="text-blue-300">{t('callLogs.analyticsStatus.running')}</span>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {callLog.analytics_status === ANALYTICS_STATUS.FAILED && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <AlertCircle className="w-5 h-5 text-red-400" />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span className="text-red-300">{t('callLogs.analyticsStatus.failed')}</span>
|
|
|
|
|
+ {callLog.analytics_error && (
|
|
|
|
|
+ <p className="text-xs text-red-400 mt-1">{callLog.analytics_error}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {callLog.analytics_status === ANALYTICS_STATUS.PENDING && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Clock className="w-5 h-5 text-slate-400" />
|
|
|
|
|
+ <span className="text-slate-300">{t('callLogs.analyticsStatus.pending')}</span>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- </Card>
|
|
|
|
|
- )}
|
|
|
|
|
- */}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- {/* Right Column - Transcript Chat */}
|
|
|
|
|
- <div className="lg:col-span-2 h-full">
|
|
|
|
|
- <Card className="bg-slate-800 border-slate-700 h-full flex flex-col">
|
|
|
|
|
- <div className="p-4 border-b border-slate-700">
|
|
|
|
|
- <h3 className="text-lg font-semibold text-white">{t('callLogDetail.transcript')}</h3>
|
|
|
|
|
|
|
+ {/* Retry/Run Analytics Button */}
|
|
|
|
|
+ {ENABLE_MANUAL_ANALYTICS && callLog.transcript && (
|
|
|
|
|
+ callLog.analytics_status === ANALYTICS_STATUS.FAILED ||
|
|
|
|
|
+ callLog.analytics_status === ANALYTICS_STATUS.PENDING
|
|
|
|
|
+ ) && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleTriggerAnalytics}
|
|
|
|
|
+ disabled={isAnalyticsTriggering}
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ className="bg-slate-600 hover:bg-slate-500"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isAnalyticsTriggering ? (
|
|
|
|
|
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <RefreshCw className="w-4 h-4 mr-2" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ {callLog.analytics_status === ANALYTICS_STATUS.FAILED
|
|
|
|
|
+ ? t('callLogs.retryAnalytics')
|
|
|
|
|
+ : t('callLogs.runAnalytics')
|
|
|
|
|
+ }
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
|
|
|
- {messages.length === 0 ? (
|
|
|
|
|
- <div className="flex items-center justify-center h-full">
|
|
|
|
|
- <p className="text-slate-400">{t('callLogDetail.noTranscript')}</p>
|
|
|
|
|
|
|
+ {/* Related Entities */}
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ {/* Related Customers */}
|
|
|
|
|
+ {callLog.related_customers && callLog.related_customers.length > 0 && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h4 className="text-white font-medium mb-2 flex items-center gap-2">
|
|
|
|
|
+ <User className="w-4 h-4 text-slate-400" />
|
|
|
|
|
+ {t('callLogDetail.relatedCustomers')} ({callLog.related_customers.length})
|
|
|
|
|
+ </h4>
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ {callLog.related_customers.map((customer, idx) => (
|
|
|
|
|
+ <div key={idx} className="bg-slate-700/30 rounded-lg p-3">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ {customer.name && (
|
|
|
|
|
+ <p className="text-white font-medium">{customer.name}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {customer.email && (
|
|
|
|
|
+ <p className="text-slate-300 text-sm">{customer.email}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {customer.phone && (
|
|
|
|
|
+ <p className="text-slate-400 text-sm">{customer.phone}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-slate-400 text-xs mt-2 italic">{customer.match_reason}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
</div>
|
|
</div>
|
|
|
- ) : (
|
|
|
|
|
- messages.map((message, index) => (
|
|
|
|
|
- <div
|
|
|
|
|
- key={index}
|
|
|
|
|
- className={`flex gap-3 ${message.role === 'ai' ? 'justify-start' : 'justify-end'}`}
|
|
|
|
|
- >
|
|
|
|
|
- {message.role === 'ai' && (
|
|
|
|
|
- <div className="w-8 h-8 bg-cyan-500/20 rounded-full flex items-center justify-center flex-shrink-0">
|
|
|
|
|
- <Bot className="w-4 h-4 text-cyan-400" />
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Related Orders */}
|
|
|
|
|
+ {callLog.related_orders && callLog.related_orders.length > 0 && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h4 className="text-white font-medium mb-2 flex items-center gap-2">
|
|
|
|
|
+ <ShoppingCart className="w-4 h-4 text-slate-400" />
|
|
|
|
|
+ {t('callLogDetail.relatedOrders')} ({callLog.related_orders.length})
|
|
|
|
|
+ </h4>
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ {callLog.related_orders.map((order, idx) => (
|
|
|
|
|
+ <div key={idx} className="bg-slate-700/30 rounded-lg p-3">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-white font-medium">#{order.order_number}</p>
|
|
|
|
|
+ {order.status && (
|
|
|
|
|
+ <span className="text-xs px-2 py-0.5 bg-slate-600 rounded text-slate-300">
|
|
|
|
|
+ {order.status}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {order.total && (
|
|
|
|
|
+ <p className="text-green-400 font-medium">{order.total}</p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- <div
|
|
|
|
|
- className={`max-w-[80%] rounded-2xl px-4 py-3 ${
|
|
|
|
|
- message.role === 'ai'
|
|
|
|
|
- ? 'bg-slate-700 text-white rounded-tl-none'
|
|
|
|
|
- : 'bg-cyan-600 text-white rounded-tr-none'
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- <p className="text-sm leading-relaxed">{message.content}</p>
|
|
|
|
|
|
|
+ <p className="text-slate-400 text-xs mt-2 italic">{order.match_reason}</p>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {message.role === 'user' && (
|
|
|
|
|
- <div className="w-8 h-8 bg-green-500/20 rounded-full flex items-center justify-center flex-shrink-0">
|
|
|
|
|
- <User className="w-4 h-4 text-green-400" />
|
|
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Related Products */}
|
|
|
|
|
+ {callLog.related_products && callLog.related_products.length > 0 && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h4 className="text-white font-medium mb-2 flex items-center gap-2">
|
|
|
|
|
+ <Package className="w-4 h-4 text-slate-400" />
|
|
|
|
|
+ {t('callLogDetail.relatedProducts')} ({callLog.related_products.length})
|
|
|
|
|
+ </h4>
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ {callLog.related_products.map((product, idx) => (
|
|
|
|
|
+ <div key={idx} className="bg-slate-700/30 rounded-lg p-3">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-white font-medium">{product.name}</p>
|
|
|
|
|
+ {product.sku && (
|
|
|
|
|
+ <p className="text-slate-400 text-sm">SKU: {product.sku}</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {product.price && (
|
|
|
|
|
+ <p className="text-green-400 font-medium">{product.price}</p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- ))
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- </Card>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <p className="text-slate-400 text-xs mt-2 italic">{product.match_reason}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Show "Run Analytics" button if no related data and analytics not completed */}
|
|
|
|
|
+ {ENABLE_MANUAL_ANALYTICS && callLog.transcript &&
|
|
|
|
|
+ !callLog.related_customers?.length &&
|
|
|
|
|
+ !callLog.related_orders?.length &&
|
|
|
|
|
+ !callLog.related_products?.length &&
|
|
|
|
|
+ callLog.analytics_status !== ANALYTICS_STATUS.RUNNING && (
|
|
|
|
|
+ <div className="text-center py-4">
|
|
|
|
|
+ <p className="text-slate-400 text-sm mb-3">{t('callLogDetail.noRelatedData')}</p>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleTriggerAnalytics}
|
|
|
|
|
+ disabled={isAnalyticsTriggering}
|
|
|
|
|
+ className="bg-cyan-600 hover:bg-cyan-500"
|
|
|
|
|
+ >
|
|
|
|
|
+ {isAnalyticsTriggering ? (
|
|
|
|
|
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <RefreshCw className="w-4 h-4 mr-2" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ {t('callLogs.runAnalytics')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Card>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|