|
|
@@ -0,0 +1,368 @@
|
|
|
+import { useState, useEffect, useRef } from "react";
|
|
|
+import { useParams, useNavigate } from "react-router-dom";
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
+import { Card } from "@/components/ui/card";
|
|
|
+import { ArrowLeft, Phone, Clock, DollarSign, Calendar, Bot, User, Play, Pause } from "lucide-react";
|
|
|
+import { useTranslation } from "react-i18next";
|
|
|
+import { supabase } from "@/integrations/supabase/client";
|
|
|
+
|
|
|
+interface CallLog {
|
|
|
+ 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;
|
|
|
+}
|
|
|
+
|
|
|
+interface TranscriptMessage {
|
|
|
+ role: 'ai' | 'user';
|
|
|
+ content: string;
|
|
|
+}
|
|
|
+
|
|
|
+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 parseTranscript = (transcript: string | null): TranscriptMessage[] => {
|
|
|
+ if (!transcript) return [];
|
|
|
+
|
|
|
+ const messages: TranscriptMessage[] = [];
|
|
|
+ const lines = transcript.split('\n');
|
|
|
+
|
|
|
+ for (const line of lines) {
|
|
|
+ const trimmedLine = line.trim();
|
|
|
+ if (!trimmedLine) continue;
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return messages;
|
|
|
+};
|
|
|
+
|
|
|
+export function CallLogDetailContent() {
|
|
|
+ const { t } = useTranslation();
|
|
|
+ const { id } = useParams<{ id: string }>();
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const [callLog, setCallLog] = useState<CallLog | null>(null);
|
|
|
+ const [isLoading, setIsLoading] = useState(true);
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
+ const [isPlaying, setIsPlaying] = useState(false);
|
|
|
+ const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchCallLog = async () => {
|
|
|
+ if (!id) return;
|
|
|
+
|
|
|
+ setIsLoading(true);
|
|
|
+ setError(null);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const { data, error: fetchError } = await supabase
|
|
|
+ .from('call_logs')
|
|
|
+ .select('*')
|
|
|
+ .eq('id', id)
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (fetchError) {
|
|
|
+ throw fetchError;
|
|
|
+ }
|
|
|
+
|
|
|
+ setCallLog(data);
|
|
|
+ } catch (err) {
|
|
|
+ setError(err instanceof Error ? err.message : 'An error occurred');
|
|
|
+ } finally {
|
|
|
+ setIsLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fetchCallLog();
|
|
|
+ }, [id]);
|
|
|
+
|
|
|
+ const handleBack = () => {
|
|
|
+ navigate('/call-logs');
|
|
|
+ };
|
|
|
+
|
|
|
+ const togglePlayPause = () => {
|
|
|
+ if (audioRef.current) {
|
|
|
+ if (isPlaying) {
|
|
|
+ audioRef.current.pause();
|
|
|
+ } else {
|
|
|
+ audioRef.current.play();
|
|
|
+ }
|
|
|
+ setIsPlaying(!isPlaying);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleAudioEnded = () => {
|
|
|
+ setIsPlaying(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ const messages = parseTranscript(callLog?.transcript || null);
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <div className="flex-1 bg-slate-900 text-white flex items-center justify-center">
|
|
|
+ <div className="flex flex-col items-center">
|
|
|
+ <div className="relative">
|
|
|
+ <div className="w-12 h-12 border-4 border-slate-700 rounded-full"></div>
|
|
|
+ <div className="w-12 h-12 border-4 border-white border-t-transparent rounded-full animate-spin absolute top-0"></div>
|
|
|
+ </div>
|
|
|
+ <p className="text-slate-400 mt-4 font-medium">{t('common.loading')}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error || !callLog) {
|
|
|
+ return (
|
|
|
+ <div className="flex-1 bg-slate-900 text-white flex items-center justify-center">
|
|
|
+ <div className="flex flex-col items-center">
|
|
|
+ <div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mb-4">
|
|
|
+ <svg className="w-8 h-8 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
+ <circle cx="12" cy="12" r="10"/>
|
|
|
+ <line x1="12" y1="8" x2="12" y2="12"/>
|
|
|
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <p className="text-red-500 font-medium mb-2">{t('callLogDetail.notFound')}</p>
|
|
|
+ <Button
|
|
|
+ className="bg-slate-700 hover:bg-slate-600 text-white mt-4"
|
|
|
+ onClick={handleBack}
|
|
|
+ >
|
|
|
+ <ArrowLeft className="w-4 h-4 mr-2" />
|
|
|
+ {t('callLogDetail.backToList')}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <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">
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ className="text-slate-400 hover:text-white"
|
|
|
+ onClick={handleBack}
|
|
|
+ >
|
|
|
+ <ArrowLeft className="w-4 h-4 mr-2" />
|
|
|
+ {t('common.back')}
|
|
|
+ </Button>
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-semibold text-white">{t('callLogDetail.title')}</h1>
|
|
|
+ <p className="text-slate-400">{formatDateTime(callLog.started_at)}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 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 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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+ </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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Audio Player Card */}
|
|
|
+ {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">
|
|
|
+ <audio
|
|
|
+ ref={audioRef}
|
|
|
+ src={callLog.recording_url}
|
|
|
+ onEnded={handleAudioEnded}
|
|
|
+ className="hidden"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
+ <Button
|
|
|
+ onClick={togglePlayPause}
|
|
|
+ className="w-12 h-12 rounded-full bg-cyan-600 hover:bg-cyan-500 flex items-center justify-center"
|
|
|
+ >
|
|
|
+ {isPlaying ? (
|
|
|
+ <Pause className="w-5 h-5" />
|
|
|
+ ) : (
|
|
|
+ <Play className="w-5 h-5 ml-0.5" />
|
|
|
+ )}
|
|
|
+ </Button>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Native audio controls as fallback */}
|
|
|
+ <audio
|
|
|
+ src={callLog.recording_url}
|
|
|
+ controls
|
|
|
+ className="w-full mt-2"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Costs Breakdown Card */}
|
|
|
+ {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>
|
|
|
+ </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>
|
|
|
+ </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>
|
|
|
+ </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>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <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>
|
|
|
+ </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>
|
|
|
+ ))
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|