sls/resources/views/client_calls/logs.blade.php
2026-08-21 19:13:29 +05:30

1110 lines
47 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Date-wise Call Logs & Meeting Intelligence | SingleLogin</title>
<!-- Google Fonts: Outfit & Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script>
(function () {
const savedTheme = localStorage.getItem('theme') || 'dark';
if (savedTheme === 'light') {
document.documentElement.classList.add('light-mode');
} else {
document.documentElement.classList.remove('light-mode');
}
})();
</script>
<style>
:root {
--bg-color: #0b0f19;
--card-bg: rgba(17, 24, 39, 0.6);
--card-border: rgba(255, 255, 255, 0.1);
--card-hover-border: rgba(99, 102, 241, 0.45);
--accent-primary: #4f46e5;
--accent-secondary: #06b6d4;
--accent-glow: rgba(99, 102, 241, 0.25);
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--input-bg: rgba(255, 255, 255, 0.05);
--modal-bg: rgba(15, 23, 42, 0.96);
--modal-overlay: rgba(11, 15, 25, 0.85);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
:root.light-mode {
--bg-color: #f0f4f8;
--card-bg: rgba(255, 255, 255, 0.82);
--card-border: rgba(0, 0, 0, 0.08);
--card-hover-border: rgba(79, 70, 229, 0.4);
--accent-primary: #4f46e5;
--accent-secondary: #0284c7;
--accent-glow: rgba(79, 70, 229, 0.15);
--text-main: #1e293b;
--text-muted: #64748b;
--success-color: #059669;
--warning-color: #d97706;
--danger-color: #dc2626;
--input-bg: rgba(0, 0, 0, 0.03);
--modal-bg: rgba(255, 255, 255, 0.96);
--modal-overlay: rgba(240, 244, 248, 0.85);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.08);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
background-image:
radial-gradient(circle at 85% 15%, rgba(99, 102, 241, 0.15) 0%, transparent 45%),
radial-gradient(circle at 15% 85%, rgba(6, 182, 212, 0.12) 0%, transparent 45%);
background-attachment: fixed;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: rgba(11, 15, 25, 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--card-border);
position: sticky;
top: 0;
z-index: 50;
}
:root.light-mode .navbar { background: rgba(255, 255, 255, 0.82); }
.brand-logo {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
color: var(--text-main);
font-family: 'Outfit', sans-serif;
font-size: 1.25rem;
font-weight: 700;
}
.brand-badge {
background: linear-gradient(135deg, #06b6d4, #3b82f6);
color: white;
font-size: 0.75rem;
padding: 3px 10px;
border-radius: 999px;
font-weight: 700;
text-transform: uppercase;
}
.nav-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.nav-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0.55rem 1.1rem;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--card-border);
background: var(--card-bg);
color: var(--text-main);
backdrop-filter: blur(8px);
}
.nav-btn:hover {
transform: translateY(-2px);
border-color: var(--card-hover-border);
box-shadow: 0 4px 12px var(--accent-glow);
}
.nav-btn-primary {
background: linear-gradient(135deg, #4f46e5, #6366f1);
color: white !important;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 14px rgba(79, 70, 229, 0.4);
}
.theme-toggle-btn {
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text-main);
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 1.1rem;
transition: all 0.25s ease;
}
/* Container */
.main-container {
max-width: 1320px;
margin: 2rem auto;
padding: 0 1.5rem;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.25rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1.25rem 1.4rem;
display: flex;
align-items: center;
gap: 1rem;
transition: all 0.3s ease;
box-shadow: var(--glass-shadow);
}
.stat-card:hover {
border-color: var(--card-hover-border);
transform: translateY(-3px);
box-shadow: 0 10px 24px var(--accent-glow);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
background: rgba(99, 102, 241, 0.12);
}
.stat-val {
font-size: 1.6rem;
font-weight: 800;
font-family: 'Outfit', sans-serif;
color: var(--text-main);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
}
/* Section Header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.section-title {
font-family: 'Outfit', sans-serif;
font-size: 1.6rem;
font-weight: 700;
color: var(--text-main);
}
/* Filter Bar */
.filter-bar {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 250px;
padding: 0.75rem 1rem;
background: var(--input-bg);
backdrop-filter: blur(8px);
border: 1px solid var(--card-border);
border-radius: 12px;
color: var(--text-main);
font-size: 0.875rem;
outline: none;
}
.search-input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
.filter-select {
padding: 0.75rem 1rem;
background: var(--input-bg);
backdrop-filter: blur(8px);
border: 1px solid var(--card-border);
border-radius: 12px;
color: var(--text-main);
font-size: 0.875rem;
outline: none;
cursor: pointer;
}
/* Date Group */
.date-group {
margin-bottom: 2.5rem;
}
.date-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 1.25rem;
}
.date-title-badge {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(6, 182, 212, 0.2));
color: #818cf8;
font-family: 'Outfit', sans-serif;
font-size: 1.1rem;
font-weight: 700;
padding: 4px 14px;
border-radius: 999px;
border: 1px solid rgba(99, 102, 241, 0.3);
display: flex;
align-items: center;
gap: 8px;
}
.date-line {
flex: 1;
height: 1px;
background: var(--card-border);
}
/* Call Log Entry Card */
.log-entry-card {
background: var(--card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--card-border);
border-radius: 18px;
padding: 1.6rem;
margin-bottom: 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
transition: all 0.25s ease;
box-shadow: var(--glass-shadow);
}
.log-entry-card:hover {
border-color: var(--card-hover-border);
transform: translateY(-3px);
box-shadow: 0 12px 30px var(--accent-glow);
}
.log-card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 12px;
}
.log-project-title {
font-family: 'Outfit', sans-serif;
font-size: 1.25rem;
font-weight: 700;
color: var(--text-main);
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.project-tag {
font-size: 0.78rem;
font-weight: 600;
color: #818cf8;
background: rgba(99, 102, 241, 0.15);
padding: 2px 10px;
border-radius: 6px;
border: 1px solid rgba(99, 102, 241, 0.3);
}
.log-client-meta {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 4px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
/* Clickable Emotion Badges */
.emotion-clickable {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
text-transform: uppercase;
}
.emotion-clickable:hover {
transform: scale(1.04);
}
.emotion-happy {
background: rgba(16, 185, 129, 0.18);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.4);
}
.emotion-neutral {
background: rgba(59, 130, 246, 0.18);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.4);
}
.emotion-sad {
background: rgba(245, 158, 11, 0.18);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.4);
}
.emotion-angry {
background: rgba(239, 68, 68, 0.22);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.5);
animation: pulse-border 2s infinite;
}
@keyframes pulse-border {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
.summary-preview-box {
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid #6366f1;
padding: 12px 14px;
border-radius: 0 8px 8px 0;
font-size: 0.9rem;
color: #cbd5e1;
line-height: 1.5;
}
.log-actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--card-border);
flex-wrap: wrap;
gap: 10px;
}
.btn-action-small {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
border: 1px solid var(--card-border);
background: var(--input-bg);
color: var(--text-main);
transition: all 0.2s ease;
}
.btn-action-small:hover {
border-color: #6366f1;
color: #818cf8;
transform: translateY(-1px);
}
.btn-download-mom {
background: rgba(99, 102, 241, 0.15);
color: #818cf8;
border-color: rgba(99, 102, 241, 0.35);
}
.btn-download-todos {
background: rgba(16, 185, 129, 0.15);
color: #34d399;
border-color: rgba(16, 185, 129, 0.35);
}
/* Modals */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--modal-overlay);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: none;
justify-content: center;
align-items: center;
z-index: 100;
padding: 1.5rem;
}
.modal-card {
background: var(--modal-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--card-border);
border-radius: 20px;
width: 100%;
max-width: 660px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5);
padding: 2rem;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-title {
font-family: 'Outfit', sans-serif;
font-size: 1.35rem;
font-weight: 700;
}
.modal-close {
background: transparent;
border: none;
color: var(--text-muted);
font-size: 1.5rem;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
}
.modal-close:hover { color: var(--text-main); background: var(--input-bg); }
/* To-Do Checklist */
.todo-item-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 14px;
background: var(--input-bg);
border: 1px solid var(--card-border);
border-radius: 10px;
margin-bottom: 8px;
transition: all 0.2s ease;
}
.todo-item-row.completed {
opacity: 0.55;
text-decoration: line-through;
}
.todo-checkbox {
margin-top: 3px;
cursor: pointer;
width: 17px;
height: 17px;
accent-color: #10b981;
}
.todo-content { flex: 1; }
.todo-text {
font-size: 0.9rem;
color: var(--text-main);
font-weight: 500;
}
.todo-meta {
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 3px;
display: flex;
gap: 12px;
}
</style>
</head>
<body>
<!-- Glass Navbar -->
<nav class="navbar">
<a href="{{ route('client-calls.index') }}" class="brand-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: #06b6d4;"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
SingleLogin <span class="brand-badge">Call Logs</span>
</a>
<div class="nav-actions">
<!-- Theme Toggle -->
<button class="theme-toggle-btn" id="themeToggleBtn" onclick="toggleTheme()" title="Switch Light / Dark Theme">
🌙
</button>
<a href="{{ route('client-calls.index') }}" class="nav-btn">
📁 Client Projects
</a>
<a href="{{ route('dashboard') }}" class="nav-btn">
Dashboard
</a>
</div>
</nav>
<!-- Main Container -->
<main class="main-container">
<!-- Stats Overview -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon" style="color: #818cf8;">📞</div>
<div>
<div class="stat-val">{{ $totalCalls }}</div>
<div class="stat-label">Total Calls Logged</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="color: #34d399;">😊</div>
<div>
<div class="stat-val" style="color: #34d399;">{{ $happyCalls }}</div>
<div class="stat-label">Satisfied / Happy</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="color: #fbbf24;">😟</div>
<div>
<div class="stat-val" style="color: #fbbf24;">{{ $sadCalls }}</div>
<div class="stat-label">Concerned / Sad</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="color: #f87171;">😡</div>
<div>
<div class="stat-val" style="color: #f87171;">{{ $angryCalls }}</div>
<div class="stat-label">Frustrated / Angry</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="color: #22d3ee;">📋</div>
<div>
<div class="stat-val">{{ $totalTodosCount }}</div>
<div class="stat-label">Actionable Tasks</div>
</div>
</div>
</div>
<!-- Section Header -->
<div class="section-header">
<div>
<h1 class="section-title">Date-wise Client Call Logs</h1>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 4px;">
Review date-wise meeting transcripts, inspect emotional sentiment drivers, and export MoMs & actionable tasks in .txt format.
</p>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<input type="text" id="searchInput" class="search-input" placeholder="Search by project, client name, topic, or lead..." onkeyup="filterLogs()">
<select id="sentimentFilter" class="filter-select" onchange="filterLogs()">
<option value="all">All Expressions</option>
<option value="happy">Happy 😊</option>
<option value="neutral">Neutral 😐</option>
<option value="sad">Sad / Concerned 😟</option>
<option value="angry">Angry / Frustrated 😡</option>
</select>
<select id="projectFilter" class="filter-select" onchange="filterLogs()">
<option value="all">All Projects</option>
@foreach($clients as $c)
<option value="{{ strtolower($c->project_name) }}">{{ $c->project_name }}</option>
@endforeach
</select>
</div>
<!-- Grouped Date-wise List -->
@if($groupedNotes->isEmpty())
<div style="text-align: center; padding: 4rem 2rem; background: var(--card-bg); border-radius: 18px; border: 1px dashed var(--card-border);">
<div style="font-size: 3rem; margin-bottom: 1rem;">📞</div>
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 6px;">No Call Logs Recorded Yet</h3>
<p style="color: var(--text-muted); font-size: 0.9rem;">Once client calls are held, full AI transcription, MoM, and To-Do lists will be logged here date-wise.</p>
</div>
@else
@foreach($groupedNotes as $dateKey => $notes)
<div class="date-group" data-date="{{ $dateKey }}">
<div class="date-header">
<div class="date-title-badge">
<span>📅</span> {{ \Carbon\Carbon::parse($dateKey)->format('F j, Y') }}
</div>
<span style="font-size: 0.8rem; color: var(--text-muted); font-weight: 600;">
{{ $notes->count() }} {{ $notes->count() === 1 ? 'call' : 'calls' }}
</span>
<div class="date-line"></div>
</div>
@foreach($notes as $note)
@php
$sentiment = strtolower($note->detected_sentiment ?? 'neutral');
$badgeClass = match($sentiment) {
'happy' => 'emotion-happy',
'sad' => 'emotion-sad',
'angry' => 'emotion-angry',
default => 'emotion-neutral',
};
$sentimentEmoji = match($sentiment) {
'happy' => '😊 Happy',
'sad' => '😟 Concerned',
'angry' => '😡 Frustrated',
default => '😐 Neutral',
};
$todoCount = count($note->todo_items ?? $note->action_items ?? []);
$hasRec = !empty($note->recording_path) || !empty($note->recordings) || (!empty($note->meeting) && (!empty($note->meeting->recording_path) || !empty($note->meeting->recordings)));
$recPath = $note->recording_path ?? ($note->meeting->recording_path ?? null);
if (!$recPath && !empty($note->recordings)) {
$first = $note->recordings[0];
$recPath = is_array($first) ? ($first['url'] ?? $first['full_url'] ?? null) : $first;
}
if (!$recPath && !empty($note->meeting->recordings)) {
$first = $note->meeting->recordings[0];
$recPath = is_array($first) ? ($first['url'] ?? $first['full_url'] ?? null) : $first;
}
@endphp
<div class="log-entry-card"
data-project="{{ strtolower($note->client->project_name ?? '') }}"
data-client="{{ strtolower($note->client->client_name ?? '') }}"
data-sentiment="{{ $sentiment }}"
data-search="{{ strtolower(($note->client->project_name ?? '') . ' ' . ($note->client->client_name ?? '') . ' ' . ($note->meeting->title ?? '') . ' ' . ($note->summary ?? '')) }}">
<div class="log-card-top">
<div>
<div class="log-project-title">
{{ $note->meeting->title ?? 'Client Call' }}
<span class="project-tag">
{{ $note->client->project_name ?? 'Project' }}
</span>
@if($hasRec)
<span style="font-size: 0.72rem; font-weight: 700; background: rgba(99,102,241,0.2); color: #818cf8; padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(99,102,241,0.3);">
🎥 Recorded
</span>
@endif
</div>
<div class="log-client-meta">
<span>👤 <strong>{{ $note->client->client_name ?? 'Client' }}</strong> {{ $note->client->company_name ? "({$note->client->company_name})" : "" }}</span>
<span>🕒 {{ $note->created_at ? $note->created_at->format('h:i A') : '' }}</span>
<span>🎙️ Host: {{ $note->creator->name ?? 'SingleLogin Host' }}</span>
</div>
</div>
<!-- Interactive Emotion / Expression Badge -->
<button class="emotion-clickable {{ $badgeClass }}" onclick="openEmotionModal({{ $note->id }})" title="Click to view why the client felt this way">
<span>{{ $sentimentEmoji }}</span>
<span style="font-size: 0.72rem; text-decoration: underline; margin-left: 4px;">Why? 🔍</span>
</button>
</div>
<!-- Executive Summary -->
<div class="summary-preview-box">
{{ $note->summary ?? 'Meeting concluded with milestone alignment.' }}
</div>
<!-- Action Buttons Bar -->
<div class="log-actions-bar">
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<a href="{{ route('client-calls.download-mom', $note->id) }}" class="btn-action-small btn-download-mom" title="Download formatted MoM in text format">
📥 Download MoM (.txt)
</a>
<button class="btn-action-small btn-download-todos" onclick="openTodoModal({{ $note->id }}, '{{ addslashes($note->client->project_name ?? '') }}')">
📋 Tasks ({{ $todoCount }})
</button>
<a href="{{ route('client-calls.download-todos', $note->id) }}" class="btn-action-small" title="Direct download Tasks in text format">
📄 Tasks (.txt)
</a>
@if($hasRec && $recPath)
<button class="btn-action-small" style="background: rgba(99,102,241,0.15); color: #818cf8; border-color: rgba(99,102,241,0.35);" onclick="openLogVideoModal('{{ asset(ltrim($recPath, '/')) }}', '{{ addslashes($note->meeting->title ?? 'Meeting Recording') }}')">
🎥 Play Video
</button>
<a href="{{ route('client-calls.download-note-recording', $note->id) }}" class="btn-action-small" style="color: #f472b6; border-color: rgba(244,114,182,0.4);" title="Download WebRTC call video recording">
📥 Video
</a>
@endif
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn-action-small" onclick="openFullMomModal({{ $note->id }})">
View Full MoM
</button>
<a href="{{ route('client-calls.show', $note->client_id) }}" target="_blank" class="btn-action-small" style="color: #38bdf8; border-color: rgba(56,189,248,0.3);">
Open Client Hub
</a>
</div>
</div>
</div>
@endforeach
</div>
@endforeach
@endif
</main>
<!-- 1. Interactive Emotion Intelligence Modal -->
<div id="emotionModal" class="modal-overlay">
<div class="modal-card">
<div class="modal-header">
<div>
<h2 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
<span>🧠</span> Emotion Root-Cause Intelligence
</h2>
<p id="emotionModalSubmeta" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;"></p>
</div>
<button class="modal-close" onclick="closeEmotionModal()">&times;</button>
</div>
<div id="emotionModalBody">
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
Loading AI emotion breakdown...
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 1.5rem;">
<button type="button" class="nav-btn" onclick="closeEmotionModal()">Close</button>
</div>
</div>
</div>
<!-- 2. Interactive To-Do Checklist Modal -->
<div id="todoModal" class="modal-overlay">
<div class="modal-card">
<div class="modal-header">
<div>
<h2 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
<span>📋</span> AI Auto-Generated Actionable Tasks
</h2>
<p id="todoModalSubmeta" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;"></p>
</div>
<button class="modal-close" onclick="closeTodoModal()">&times;</button>
</div>
<div id="todoModalList" style="max-height: 380px; overflow-y: auto; margin-bottom: 1.25rem;">
<!-- To-Dos rendered dynamically -->
</div>
<div style="display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--card-border); padding-top: 1rem;">
<a id="btnDownloadTodoFile" href="#" class="nav-btn-primary nav-btn" style="font-size: 0.85rem;">
📥 Download Tasks (.txt)
</a>
<button type="button" class="nav-btn" onclick="closeTodoModal()">Done</button>
</div>
</div>
</div>
<!-- 3. Full MoM Modal -->
<div id="fullMomModal" class="modal-overlay">
<div class="modal-card" style="max-width: 750px;">
<div class="modal-header">
<div>
<h2 class="modal-title">Minutes of Meeting (MoM)</h2>
<p id="fullMomSubmeta" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;"></p>
</div>
<button class="modal-close" onclick="closeFullMomModal()">&times;</button>
</div>
<div id="fullMomContent" style="background: var(--input-bg); border: 1px solid var(--card-border); border-radius: 12px; padding: 1.25rem; font-size: 0.875rem; line-height: 1.6; color: #cbd5e1; max-height: 440px; overflow-y: auto; white-space: pre-wrap;">
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1.25rem;">
<a id="btnDownloadMomFile" href="#" class="nav-btn-primary nav-btn" style="font-size: 0.85rem;">
📥 Download MoM (.txt)
</a>
<button type="button" class="nav-btn" onclick="closeFullMomModal()">Close</button>
</div>
</div>
</div>
<!-- 4. Video Player Modal -->
<div id="logVideoPlayerModal" class="modal-overlay">
<div class="modal-card" style="max-width: 800px; padding: 1.5rem;">
<div class="modal-header">
<div>
<h2 id="logVideoModalTitle" class="modal-title" style="display: flex; align-items: center; gap: 8px;">
<span>🎥</span> Meeting Video Recording
</h2>
</div>
<button class="modal-close" onclick="closeLogVideoModal()">&times;</button>
</div>
<div style="background: #000; border-radius: 12px; overflow: hidden; margin-bottom: 1.25rem;">
<video id="logRecordingVideoPlayer" controls playsinline style="width: 100%; max-height: 460px; display: block; border-radius: 12px;"></video>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
<a id="btnDownloadLogVideoModal" href="#" class="nav-btn-primary nav-btn" style="font-size: 0.85rem;" download>
📥 Download Video Recording
</a>
<button type="button" class="nav-btn" onclick="closeLogVideoModal()">Close</button>
</div>
</div>
</div>
<script>
function toggleTheme() {
const isLight = document.documentElement.classList.toggle('light-mode');
localStorage.setItem('theme', isLight ? 'light' : 'dark');
document.getElementById('themeToggleBtn').innerText = isLight ? '☀️' : '🌙';
}
document.addEventListener('DOMContentLoaded', () => {
const isLight = document.documentElement.classList.contains('light-mode');
const btn = document.getElementById('themeToggleBtn');
if (btn) btn.innerText = isLight ? '☀️' : '🌙';
});
function filterLogs() {
const query = document.getElementById('searchInput').value.toLowerCase();
const sentiment = document.getElementById('sentimentFilter').value;
const project = document.getElementById('projectFilter').value;
const cards = document.querySelectorAll('.log-entry-card');
cards.forEach(card => {
const text = card.dataset.search || '';
const cardSentiment = card.dataset.sentiment || '';
const cardProject = card.dataset.project || '';
const matchesQuery = text.includes(query);
const matchesSentiment = (sentiment === 'all') || (cardSentiment === sentiment);
const matchesProject = (project === 'all') || (cardProject === project);
if (matchesQuery && matchesSentiment && matchesProject) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
document.querySelectorAll('.date-group').forEach(group => {
const visibleCards = group.querySelectorAll('.log-entry-card[style="display: flex;"]');
const allCards = group.querySelectorAll('.log-entry-card');
if (allCards.length > 0 && visibleCards.length === 0 && (query || sentiment !== 'all' || project !== 'all')) {
group.style.display = 'none';
} else {
group.style.display = 'block';
}
});
}
async function openEmotionModal(noteId) {
const modal = document.getElementById('emotionModal');
const body = document.getElementById('emotionModalBody');
const submeta = document.getElementById('emotionModalSubmeta');
modal.style.display = 'flex';
body.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted);">Analyzing client emotional sentiment...</div>';
try {
const res = await fetch(`/client-calls/notes/${noteId}/emotion-details`, {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
if (!res.ok) {
body.innerHTML = `<div style="color:#f87171;padding:1rem;background:rgba(239,68,68,0.1);border-radius:8px;">${data.error || 'Access Restricted.'}</div>`;
return;
}
submeta.innerText = `${data.client_name} • ${data.project_name} (${data.created_at})`;
const emo = data.emotion_details || {};
const sentiment = data.detected_sentiment || 'neutral';
const bullets = emo.bullet_points || [];
const quotes = emo.trigger_quotes || [];
let bannerStyle = 'background: rgba(59,130,246,0.12); border-left: 4px solid #3b82f6; color: #93c5fd;';
let icon = '😐';
if (sentiment === 'happy') {
bannerStyle = 'background: rgba(16,185,129,0.12); border-left: 4px solid #10b981; color: #34d399;';
icon = '😊';
} else if (sentiment === 'sad') {
bannerStyle = 'background: rgba(245,158,11,0.12); border-left: 4px solid #f59e0b; color: #fbbf24;';
icon = '😟';
} else if (sentiment === 'angry') {
bannerStyle = 'background: rgba(239,68,68,0.15); border-left: 4px solid #ef4444; color: #f87171;';
icon = '😡';
}
body.innerHTML = `
<div style="${bannerStyle} padding: 14px; border-radius: 0 10px 10px 0; margin-bottom: 1.25rem;">
<div style="font-weight: 700; font-size: 0.95rem; display: flex; align-items: center; gap: 6px;">
<span>${icon}</span> ${escapeHtml(emo.headline || 'Client Emotional Analysis')}
</div>
</div>
<div style="margin-bottom: 1.25rem;">
<div style="font-size: 0.85rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 8px;">
Key Reasons & Emotional Triggers:
</div>
<ul style="padding-left: 20px; font-size: 0.875rem; color: #cbd5e1; line-height: 1.6;">
${bullets.map(b => `<li style="margin-bottom: 4px;">${escapeHtml(b)}</li>`).join('')}
</ul>
</div>
${quotes.length > 0 ? `
<div style="margin-bottom: 1.25rem;">
<div style="font-size: 0.85rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 8px;">
Spoken Key Phrases / Quotes:
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${quotes.map(q => `<span style="background: var(--input-bg); border: 1px solid var(--card-border); padding: 4px 10px; border-radius: 6px; font-size: 0.8rem; font-style: italic; color: #cbd5e1;">"${escapeHtml(q)}"</span>`).join('')}
</div>
</div>
` : ''}
${emo.suggested_action ? `
<div style="background: rgba(99,102,241,0.1); border: 1px solid rgba(99,102,241,0.3); border-radius: 10px; padding: 12px 14px;">
<div style="font-size: 0.75rem; font-weight: 700; color: #818cf8; text-transform: uppercase; margin-bottom: 4px;">
💡 Recommended Action for Project Team:
</div>
<p style="font-size: 0.85rem; color: #e0e7ff; line-height: 1.5;">${escapeHtml(emo.suggested_action)}</p>
</div>
` : ''}
`;
} catch (e) {
body.innerHTML = `<div style="color:#f87171;padding:1rem;">Failed to load emotion details.</div>`;
}
}
function closeEmotionModal() { document.getElementById('emotionModal').style.display = 'none'; }
async function openTodoModal(noteId, projectName) {
const modal = document.getElementById('todoModal');
const submeta = document.getElementById('todoModalSubmeta');
const listEl = document.getElementById('todoModalList');
const downloadBtn = document.getElementById('btnDownloadTodoFile');
submeta.innerText = projectName ? `Project: ${projectName}` : '';
downloadBtn.href = `/client-calls/notes/${noteId}/download-todos`;
modal.style.display = 'flex';
listEl.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted);">Loading tasks...</div>';
try {
const res = await fetch(`/client-calls/notes/${noteId}/emotion-details`, {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await res.json();
const todos = data.todo_items || [];
if (todos.length === 0) {
listEl.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted);">No specific tasks found for this call.</div>';
return;
}
listEl.innerHTML = todos.map((item, idx) => {
const id = item.id || (idx + 1);
const completed = item.completed ? 'checked' : '';
const completedClass = item.completed ? 'completed' : '';
const taskText = item.task || '';
const owner = item.owner || 'Assigned Lead';
const priority = item.priority || 'Medium';
const deadline = item.deadline || 'TBD';
return `
<div class="todo-item-row ${completedClass}" id="todo_${noteId}_${id}">
<input type="checkbox" class="todo-checkbox" ${completed} onchange="toggleTodoCheck(this, ${noteId}, ${id})">
<div class="todo-content">
<div class="todo-text">${escapeHtml(taskText)}</div>
<div class="todo-meta">
<span>👤 ${escapeHtml(owner)}</span>
<span>⚡ Priority: ${escapeHtml(priority)}</span>
<span>📅 Due: ${escapeHtml(deadline)}</span>
</div>
</div>
</div>
`;
}).join('');
} catch (e) {
listEl.innerHTML = '<div style="color:#f87171;padding:1rem;">Failed to load tasks.</div>';
}
}
function toggleTodoCheck(cb, noteId, todoId) {
const row = document.getElementById(`todo_${noteId}_${todoId}`);
if (row) row.classList.toggle('completed', cb.checked);
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
fetch(`/client-calls/notes/${noteId}/toggle-todo/${todoId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
}).catch(() => {});
}
function closeTodoModal() { document.getElementById('todoModal').style.display = 'none'; }
async function openFullMomModal(noteId) {
const modal = document.getElementById('fullMomModal');
const content = document.getElementById('fullMomContent');
const downloadBtn = document.getElementById('btnDownloadMomFile');
downloadBtn.href = `/client-calls/notes/${noteId}/download-mom`;
modal.style.display = 'flex';
content.innerHTML = 'Loading complete MoM...';
try {
const res = await fetch(`/client-calls/notes/${noteId}/download-mom`);
const text = await res.text();
content.innerText = text;
} catch (e) {
content.innerText = 'Failed to load MoM.';
}
}
function closeFullMomModal() { document.getElementById('fullMomModal').style.display = 'none'; }
function openLogVideoModal(videoUrl, title) {
const modal = document.getElementById('logVideoPlayerModal');
const video = document.getElementById('logRecordingVideoPlayer');
const titleEl = document.getElementById('logVideoModalTitle');
const dlBtn = document.getElementById('btnDownloadLogVideoModal');
if (titleEl && title) {
titleEl.innerHTML = `<span>🎥</span> ${escapeHtml(title)}`;
}
if (video) {
video.src = videoUrl;
video.load();
video.play().catch(() => {});
}
if (dlBtn) {
dlBtn.href = videoUrl;
}
if (modal) {
modal.style.display = 'flex';
}
}
function closeLogVideoModal() {
const modal = document.getElementById('logVideoPlayerModal');
const video = document.getElementById('logRecordingVideoPlayer');
if (video) {
video.pause();
video.src = '';
}
if (modal) {
modal.style.display = 'none';
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/[&<>"']/g, function(m) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[m];
});
}
</script>
</body>
</html>