1692 lines
82 KiB
PHP
1692 lines
82 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>{{ $client->project_name }} | Client Call Hub | 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;
|
|
}
|
|
|
|
/* Glass 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, #4f46e5, #06b6d4);
|
|
color: white;
|
|
font-size: 0.75rem;
|
|
padding: 3px 10px;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.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 {
|
|
border-color: var(--card-hover-border);
|
|
transform: translateY(-2px);
|
|
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);
|
|
}
|
|
.nav-btn-instant {
|
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
color: white !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
box-shadow: 0 4px 14px rgba(245, 158, 11, 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;
|
|
}
|
|
|
|
/* Alert */
|
|
.alert {
|
|
padding: 1rem 1.25rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-size: 0.9rem;
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
.alert-success {
|
|
background: rgba(16, 185, 129, 0.15);
|
|
border: 1px solid rgba(16, 185, 129, 0.35);
|
|
color: #34d399;
|
|
}
|
|
|
|
/* Profile Header Card */
|
|
.profile-header-card {
|
|
background: var(--card-bg);
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 20px;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
flex-wrap: wrap;
|
|
gap: 1.5rem;
|
|
box-shadow: var(--glass-shadow);
|
|
}
|
|
.project-headline {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1.85rem;
|
|
font-weight: 800;
|
|
color: var(--text-main);
|
|
margin-bottom: 6px;
|
|
line-height: 1.2;
|
|
}
|
|
.client-submeta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
flex-wrap: wrap;
|
|
margin-top: 8px;
|
|
}
|
|
.client-meta-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* Action Buttons Bar */
|
|
.hub-actions-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Tab Navigation */
|
|
.tabs-nav {
|
|
display: flex;
|
|
gap: 10px;
|
|
border-bottom: 1px solid var(--card-border);
|
|
margin-bottom: 1.75rem;
|
|
overflow-x: auto;
|
|
padding-bottom: 2px;
|
|
}
|
|
.tab-btn {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-muted);
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
padding: 0.75rem 1.25rem;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.2s ease;
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.tab-btn:hover { color: var(--text-main); }
|
|
.tab-btn.active {
|
|
color: #818cf8;
|
|
font-weight: 700;
|
|
}
|
|
.tab-btn.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -2px;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, #4f46e5, #06b6d4);
|
|
border-radius: 3px 3px 0 0;
|
|
}
|
|
|
|
.tab-content { display: none; }
|
|
.tab-content.active { display: block; }
|
|
|
|
/* Date Group Header */
|
|
.date-section-group {
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
.date-section-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-divider-line {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--card-border);
|
|
}
|
|
|
|
/* Call Detail Glass Card */
|
|
.call-detail-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.75rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: var(--glass-shadow);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
.call-detail-card:hover {
|
|
border-color: var(--card-hover-border);
|
|
}
|
|
|
|
.call-header-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
.call-title {
|
|
font-family: 'Outfit', sans-serif;
|
|
font-size: 1.35rem;
|
|
font-weight: 700;
|
|
color: var(--text-main);
|
|
}
|
|
.call-meta-line {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
margin-top: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Emotion Badges inside Call Details */
|
|
.call-emotion-badge {
|
|
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;
|
|
}
|
|
.call-emotion-badge:hover {
|
|
transform: scale(1.04);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
.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 Box */
|
|
.summary-box {
|
|
background: rgba(99, 102, 241, 0.08);
|
|
border-left: 4px solid #6366f1;
|
|
padding: 1rem 1.25rem;
|
|
border-radius: 0 10px 10px 0;
|
|
color: #cbd5e1;
|
|
font-size: 0.92rem;
|
|
line-height: 1.6;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
/* Section Headings */
|
|
.detail-subheading {
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
color: #818cf8;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin: 1.5rem 0 0.75rem 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/* Actionable Tasks List */
|
|
.tasks-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.task-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--card-border);
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.task-row:hover {
|
|
border-color: rgba(99, 102, 241, 0.3);
|
|
}
|
|
.task-checkbox {
|
|
margin-top: 3px;
|
|
accent-color: #10b981;
|
|
cursor: pointer;
|
|
width: 17px;
|
|
height: 17px;
|
|
}
|
|
.task-content { flex: 1; }
|
|
.task-title {
|
|
font-size: 0.9rem;
|
|
color: var(--text-main);
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.task-title.completed {
|
|
text-decoration: line-through;
|
|
opacity: 0.55;
|
|
}
|
|
.task-meta {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.priority-badge {
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
}
|
|
.priority-High { background: rgba(239, 68, 68, 0.2); color: #f87171; }
|
|
.priority-Medium { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
|
|
.priority-Low { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
|
|
|
|
.btn-delete-task {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 0.85rem;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.btn-delete-task:hover {
|
|
color: #ef4444;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
}
|
|
|
|
/* Action items table */
|
|
.action-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 1rem 0 1.5rem 0;
|
|
font-size: 0.875rem;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.action-table th {
|
|
text-align: left;
|
|
padding: 10px 14px;
|
|
background: var(--input-bg);
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
border-bottom: 1px solid var(--card-border);
|
|
}
|
|
.action-table td {
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid var(--card-border);
|
|
color: var(--text-main);
|
|
}
|
|
|
|
/* MoM Full Text Box */
|
|
.mom-content-box {
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
font-size: 0.875rem;
|
|
line-height: 1.65;
|
|
color: #d1d5db;
|
|
white-space: pre-wrap;
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
/* Call Footer Action Bar */
|
|
.call-actions-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-top: 1.25rem;
|
|
border-top: 1px solid var(--card-border);
|
|
margin-top: 1.5rem;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Meetings List */
|
|
.meetings-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.25rem;
|
|
}
|
|
.meeting-item-card {
|
|
background: var(--card-bg);
|
|
backdrop-filter: blur(16px);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 1.25rem;
|
|
transition: all 0.25s ease;
|
|
box-shadow: var(--glass-shadow);
|
|
}
|
|
.meeting-item-card:hover {
|
|
border-color: var(--card-hover-border);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* 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: 640px;
|
|
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.4rem;
|
|
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;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.modal-close:hover { color: var(--text-main); background: var(--input-bg); }
|
|
|
|
.form-group { margin-bottom: 1.25rem; }
|
|
.form-label {
|
|
display: block;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin-bottom: 6px;
|
|
}
|
|
.form-control {
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 10px;
|
|
color: var(--text-main);
|
|
font-size: 0.9rem;
|
|
outline: none;
|
|
}
|
|
.form-control:focus {
|
|
border-color: #6366f1;
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
|
}
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.share-link-box {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 10px;
|
|
padding: 10px 14px;
|
|
margin: 1rem 0;
|
|
}
|
|
.share-link-text {
|
|
flex: 1;
|
|
font-family: monospace;
|
|
font-size: 0.875rem;
|
|
color: #38bdf8;
|
|
word-break: break-all;
|
|
}
|
|
.btn-copy {
|
|
background: rgba(99, 102, 241, 0.2);
|
|
color: #818cf8;
|
|
border: 1px solid rgba(99, 102, 241, 0.4);
|
|
padding: 6px 12px;
|
|
border-radius: 8px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
.btn-copy:hover {
|
|
background: #4f46e5;
|
|
color: white;
|
|
}
|
|
</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: #6366f1;"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
|
Client Hub <span class="brand-badge">{{ $client->project_name }}</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.logs') }}" class="nav-btn" style="border-color: rgba(6, 182, 212, 0.4); color: #22d3ee;">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
|
Date-wise Call Logs
|
|
</a>
|
|
<a href="{{ route('client-calls.index') }}" class="nav-btn">
|
|
All Clients
|
|
</a>
|
|
<a href="{{ route('dashboard') }}" class="nav-btn">
|
|
Dashboard
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Container -->
|
|
<main class="main-container">
|
|
|
|
@if(session('success'))
|
|
<div class="alert alert-success">
|
|
<span>{{ session('success') }}</span>
|
|
<button style="background:transparent;border:none;color:inherit;cursor:pointer;font-size:1.1rem;" onclick="this.parentElement.remove()">✕</button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Profile Header -->
|
|
<div class="profile-header-card">
|
|
<div>
|
|
<div style="font-size: 0.8rem; color: #818cf8; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">
|
|
Client Profile & Project Space
|
|
</div>
|
|
<h1 class="project-headline">{{ $client->project_name }}</h1>
|
|
|
|
<div class="client-submeta">
|
|
<div class="client-meta-item">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
|
|
<strong>{{ $client->client_name }}</strong> {{ $client->company_name ? "({$client->company_name})" : "" }}
|
|
</div>
|
|
<div class="client-meta-item">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
|
{{ $client->client_email }}
|
|
</div>
|
|
@if($client->client_phone)
|
|
<div class="client-meta-item">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"></path></svg>
|
|
{{ $client->client_phone }}
|
|
</div>
|
|
@endif
|
|
<div class="client-meta-item">
|
|
<span style="color: var(--text-muted);">Lead in Charge:</span>
|
|
<span style="color: #818cf8; font-weight: 600;">{{ $client->responsibleUser->name ?? 'Unassigned' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hub-actions-bar">
|
|
<button class="nav-btn-instant nav-btn" onclick="openInstantMeetingModal()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
|
|
⚡ Start Instant Meeting
|
|
</button>
|
|
<button class="nav-btn-primary nav-btn" onclick="openScheduleModal()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
|
📅 Schedule Meeting
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs Navigation -->
|
|
<div class="tabs-nav">
|
|
<button class="tab-btn active" onclick="switchTab('callHistoryTab', this)">
|
|
<span>📅</span> Date-wise Call History & MoM Details ({{ $client->callNotes->count() }})
|
|
</button>
|
|
<button class="tab-btn" onclick="switchTab('meetingsTab', this)">
|
|
<span>⏰</span> Scheduled & Active Meetings ({{ $client->meetings->count() }})
|
|
</button>
|
|
<button class="tab-btn" onclick="switchTab('detailsTab', this)">
|
|
<span>💼</span> Project Specifications & Assigned Team
|
|
</button>
|
|
</div>
|
|
|
|
<!-- TAB 1: Date-wise Previous Calls & Intelligence -->
|
|
<div id="callHistoryTab" class="tab-content active">
|
|
@if($groupedCallNotes->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: 12px;">📝</div>
|
|
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 6px;">No Previous Calls Recorded Yet</h3>
|
|
<p style="color: var(--text-muted); font-size: 0.9rem; max-width: 500px; margin: 0 auto 1.5rem auto;">
|
|
When you conduct meetings with this client, AI will transcribe the audio, detect expressions, convert discussion into actionable tasks, and archive them date-wise here.
|
|
</p>
|
|
<button class="nav-btn-instant nav-btn" onclick="openInstantMeetingModal()">⚡ Launch First Call</button>
|
|
</div>
|
|
@else
|
|
@foreach($groupedCallNotes as $dateKey => $notes)
|
|
<div class="date-section-group">
|
|
<div class="date-section-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-divider-line"></div>
|
|
</div>
|
|
|
|
@foreach($notes as $note)
|
|
@php
|
|
$noteSentiment = strtolower($note->detected_sentiment ?? 'neutral');
|
|
$noteBadgeClass = match($noteSentiment) {
|
|
'happy' => 'emotion-happy',
|
|
'sad' => 'emotion-sad',
|
|
'angry' => 'emotion-angry',
|
|
default => 'emotion-neutral',
|
|
};
|
|
$noteEmoji = match($noteSentiment) {
|
|
'happy' => '😊 Happy / Satisfied',
|
|
'sad' => '😟 Concerned / Sad',
|
|
'angry' => '😡 Frustrated / Angry',
|
|
default => '😐 Neutral / Professional',
|
|
};
|
|
$todos = $note->todo_items ?? [];
|
|
if (empty($todos) && !empty($note->action_items)) {
|
|
$todos = $note->action_items;
|
|
}
|
|
@endphp
|
|
|
|
<div class="call-detail-card" id="call_card_{{ $note->id }}">
|
|
<div class="call-header-top">
|
|
<div>
|
|
<div class="call-title">{{ $note->meeting->title ?? 'Client Call Session' }}</div>
|
|
<div class="call-meta-line">
|
|
<span>🕒 {{ $note->created_at ? $note->created_at->format('h:i A') : '' }}</span>
|
|
<span>🎙️ Host: {{ $note->creator->name ?? 'SingleLogin Host' }}</span>
|
|
@if($note->emailed_at)
|
|
<span style="color: #34d399;">✉️ MoM Emailed ({{ $note->emailed_at->format('M d, h:i A') }})</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clickable Client Expression Badge -->
|
|
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
|
<button class="call-emotion-badge {{ $noteBadgeClass }}" onclick="openEmotionBreakdownModal({{ $note->id }})" title="Click to view why client felt this way">
|
|
<span>{{ $noteEmoji }}</span>
|
|
<span style="font-size: 0.72rem; text-decoration: underline; margin-left: 4px;">Why? 🔍</span>
|
|
</button>
|
|
|
|
@if($noteSentiment === 'angry' && $canViewAngry)
|
|
<button class="nav-btn" style="background: #ef4444; color: white; border: none; font-size: 0.78rem; padding: 5px 12px; font-weight: 700;" onclick="openWhyAngryModal({{ $note->id }})">
|
|
Why is he angry?
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Executive Summary -->
|
|
<div class="detail-subheading">
|
|
<span>Executive Summary</span>
|
|
</div>
|
|
<div class="summary-box">
|
|
{{ $note->summary ?? 'Meeting completed with deliverables walkthrough and milestone planning.' }}
|
|
</div>
|
|
|
|
<!-- Converted Actionable Tasks (To-Do List) -->
|
|
<div class="detail-subheading">
|
|
<span style="color: #34d399;">📋 Actionable Tasks Converted from Meeting (<span id="task_count_{{ $note->id }}">{{ count($todos) }}</span>)</span>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button class="nav-btn" style="font-size: 0.75rem; padding: 3px 8px;" onclick="openAddTaskModal({{ $note->id }})">
|
|
+ Add Task
|
|
</button>
|
|
<a href="{{ route('client-calls.download-todos', $note->id) }}" class="nav-btn" style="font-size: 0.75rem; padding: 3px 8px; color: #34d399; border-color: rgba(16,185,129,0.4);">
|
|
📥 Download Tasks (.txt)
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tasks-container" id="tasks_list_{{ $note->id }}">
|
|
@if(empty($todos))
|
|
<div style="font-size: 0.85rem; color: var(--text-muted); font-style: italic; padding: 8px 12px; background: var(--input-bg); border-radius: 8px;">
|
|
No actionable tasks logged for this call yet.
|
|
</div>
|
|
@else
|
|
@foreach($todos as $todo)
|
|
@php
|
|
$todoId = $todo['id'] ?? $loop->iteration;
|
|
$isCompleted = (bool)($todo['completed'] ?? false);
|
|
$priority = $todo['priority'] ?? 'Medium';
|
|
@endphp
|
|
<div class="task-row" id="todorow_{{ $note->id }}_{{ $todoId }}">
|
|
<input type="checkbox" class="task-checkbox" {{ $isCompleted ? 'checked' : '' }} onchange="toggleTodoItem(this, {{ $note->id }}, {{ $todoId }})">
|
|
<div class="task-content">
|
|
<div class="task-title {{ $isCompleted ? 'completed' : '' }}" id="todotitle_{{ $note->id }}_{{ $todoId }}">
|
|
{{ $todo['task'] ?? '' }}
|
|
</div>
|
|
<div class="task-meta">
|
|
<span>👤 {{ $todo['owner'] ?? 'Assigned Lead' }}</span>
|
|
<span class="priority-badge priority-{{ $priority }}">{{ $priority }} Priority</span>
|
|
<span>📅 Due: {{ $todo['deadline'] ?? 'TBD' }}</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn-delete-task" onclick="deleteTodoItem({{ $note->id }}, {{ $todoId }})" title="Delete Task">✕</button>
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Action Items Table -->
|
|
@if(!empty($note->action_items) && count($note->action_items) > 0)
|
|
<div class="detail-subheading">Action Items & Deliverables</div>
|
|
<table class="action-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Task</th>
|
|
<th>Owner</th>
|
|
<th>Deadline</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($note->action_items as $act)
|
|
<tr>
|
|
<td>{{ is_array($act) ? ($act['task'] ?? json_encode($act)) : $act }}</td>
|
|
<td>{{ is_array($act) ? ($act['owner'] ?? 'Team') : 'Team' }}</td>
|
|
<td>{{ is_array($act) ? ($act['deadline'] ?? '-') : '-' }}</td>
|
|
<td><span style="color: #34d399;">{{ is_array($act) ? ($act['status'] ?? 'Pending') : 'Pending' }}</span></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
@endif
|
|
|
|
<!-- Key Decisions Made -->
|
|
@if(!empty($note->key_decisions) && count($note->key_decisions) > 0)
|
|
<div class="detail-subheading">Key Decisions Made</div>
|
|
<ul style="padding-left: 20px; font-size: 0.875rem; color: #cbd5e1; line-height: 1.6; margin-bottom: 1.25rem;">
|
|
@foreach($note->key_decisions as $dec)
|
|
<li>{{ is_array($dec) ? ($dec['decision'] ?? json_encode($dec)) : $dec }}</li>
|
|
@endforeach
|
|
</ul>
|
|
@endif
|
|
|
|
<!-- Full MoM Text (Collapsible) -->
|
|
<details style="margin-top: 1rem; cursor: pointer;">
|
|
<summary style="font-size: 0.85rem; font-weight: 600; color: #818cf8; outline: none;">
|
|
📄 View Complete Minutes of Meeting (MoM)
|
|
</summary>
|
|
<div class="mom-content-box">{!! nl2br(e($note->mom_content)) !!}</div>
|
|
</details>
|
|
|
|
<!-- Raw Transcript (Collapsible) -->
|
|
@if($note->raw_transcript)
|
|
<details style="margin-top: 0.75rem; cursor: pointer;">
|
|
<summary style="font-size: 0.85rem; font-weight: 600; color: var(--text-muted); outline: none;">
|
|
🎙️ View Spoken Call Speech Transcript
|
|
</summary>
|
|
<div class="mom-content-box" style="color: #9ca3af; font-family: monospace; font-size: 0.8rem;">{!! nl2br(e($note->raw_transcript)) !!}</div>
|
|
</details>
|
|
@endif
|
|
|
|
@php
|
|
$hasRecording = !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)) {
|
|
$firstRec = $note->recordings[0];
|
|
$recPath = is_array($firstRec) ? ($firstRec['url'] ?? $firstRec['full_url'] ?? null) : $firstRec;
|
|
}
|
|
if (!$recPath && !empty($note->meeting->recordings)) {
|
|
$firstRec = $note->meeting->recordings[0];
|
|
$recPath = is_array($firstRec) ? ($firstRec['url'] ?? $firstRec['full_url'] ?? null) : $firstRec;
|
|
}
|
|
@endphp
|
|
|
|
@if($hasRecording && $recPath)
|
|
<div style="background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.25); border-radius: 12px; padding: 12px 16px; margin: 1.25rem 0; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
|
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
<span style="font-size: 1.4rem;">🎥</span>
|
|
<div>
|
|
<div style="font-weight: 700; font-size: 0.9rem; color: var(--text-main);">Video Call Recording</div>
|
|
<div style="font-size: 0.78rem; color: var(--text-muted);">High-fidelity WebRTC meeting recording</div>
|
|
</div>
|
|
</div>
|
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
|
<button type="button" class="nav-btn" style="color: #38bdf8; border-color: rgba(56,189,248,0.4); font-size: 0.8rem; padding: 0.45rem 0.9rem;" onclick="openVideoPlayerModal('{{ asset(ltrim($recPath, '/')) }}', '{{ addslashes($note->meeting->title ?? 'Meeting Recording') }}')">
|
|
▶️ Watch Recording
|
|
</button>
|
|
<a href="{{ route('client-calls.download-note-recording', $note->id) }}" class="nav-btn" style="color: #818cf8; border-color: rgba(99,102,241,0.4); font-size: 0.8rem; padding: 0.45rem 0.9rem;">
|
|
📥 Download Video
|
|
</a>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Card Footer Action Bar -->
|
|
<div class="call-actions-bar">
|
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
|
<a href="{{ route('client-calls.download-mom', $note->id) }}" class="nav-btn" style="color: #818cf8; border-color: rgba(99,102,241,0.4); font-size: 0.8rem; padding: 0.45rem 0.9rem;">
|
|
📥 Download MoM (.txt)
|
|
</a>
|
|
<a href="{{ route('client-calls.download-todos', $note->id) }}" class="nav-btn" style="color: #34d399; border-color: rgba(16,185,129,0.4); font-size: 0.8rem; padding: 0.45rem 0.9rem;">
|
|
📋 Download Tasks (.txt)
|
|
</a>
|
|
@if($hasRecording)
|
|
<a href="{{ route('client-calls.download-note-recording', $note->id) }}" class="nav-btn" style="color: #f472b6; border-color: rgba(244,114,182,0.4); font-size: 0.8rem; padding: 0.45rem 0.9rem;">
|
|
🎥 Download Video
|
|
</a>
|
|
@endif
|
|
</div>
|
|
<button class="nav-btn" style="font-size: 0.8rem; padding: 0.45rem 0.9rem;" onclick="resendEmail({{ $note->id }})">
|
|
✉️ Resend MoM Email
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
|
|
<!-- TAB 2: Scheduled & Active Meetings -->
|
|
<div id="meetingsTab" class="tab-content">
|
|
@if($client->meetings->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: 12px;">⏰</div>
|
|
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 6px;">No Upcoming Meetings Scheduled</h3>
|
|
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">Schedule a future call or launch an instant video meeting room.</p>
|
|
<button class="nav-btn-primary nav-btn" onclick="openScheduleModal()">+ Schedule Meeting</button>
|
|
</div>
|
|
@else
|
|
<div class="meetings-list">
|
|
@foreach($client->meetings as $meeting)
|
|
@php
|
|
$meetingHasRec = !empty($meeting->recording_path) || !empty($meeting->recordings);
|
|
$meetingRecPath = $meeting->recording_path;
|
|
if (!$meetingRecPath && !empty($meeting->recordings)) {
|
|
$firstRec = $meeting->recordings[0];
|
|
$meetingRecPath = is_array($firstRec) ? ($firstRec['url'] ?? $firstRec['full_url'] ?? null) : $firstRec;
|
|
}
|
|
@endphp
|
|
<div class="meeting-item-card">
|
|
<div>
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px; flex-wrap: wrap;">
|
|
<span style="font-size: 0.75rem; font-weight: 700; padding: 2px 8px; border-radius: 999px; {{ $meeting->meeting_type === 'instant' ? 'background: rgba(245,158,11,0.2); color: #fbbf24;' : 'background: rgba(99,102,241,0.2); color: #818cf8;' }}">
|
|
{{ $meeting->meeting_type === 'instant' ? '⚡ Instant' : '📅 Scheduled' }}
|
|
</span>
|
|
<span style="font-size: 0.75rem; font-weight: 600; color: {{ $meeting->status === 'completed' ? '#34d399' : '#38bdf8' }}; text-transform: uppercase;">
|
|
• {{ $meeting->status }}
|
|
</span>
|
|
@if($meetingHasRec)
|
|
<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);">
|
|
🎥 Recording Available
|
|
</span>
|
|
@endif
|
|
</div>
|
|
|
|
<div style="font-family: 'Outfit', sans-serif; font-size: 1.2rem; font-weight: 700;">{{ $meeting->title }}</div>
|
|
|
|
<div style="display: flex; gap: 14px; font-size: 0.85rem; color: var(--text-muted); margin-top: 6px; flex-wrap: wrap;">
|
|
@if($meeting->scheduled_at)
|
|
<div>🕒 {{ $meeting->scheduled_at->format('M d, Y - h:i A') }} ({{ $meeting->duration_minutes }}m)</div>
|
|
@else
|
|
<div>🕒 Created: {{ $meeting->created_at->format('M d, Y - h:i A') }}</div>
|
|
@endif
|
|
<div>👤 Host: {{ $meeting->host->name ?? 'SingleLogin Host' }}</div>
|
|
@if(!empty($meeting->client_emails))
|
|
<div>✉️ Client Invites: {{ count($meeting->client_emails) }}</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if($meeting->agenda)
|
|
<div style="font-size: 0.85rem; color: #cbd5e1; margin-top: 6px;">
|
|
<em>Agenda:</em> {{ $meeting->agenda }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
|
|
@if($meetingHasRec && $meetingRecPath)
|
|
<button type="button" class="btn-copy" style="color: #38bdf8; border-color: rgba(56,189,248,0.4);" onclick="openVideoPlayerModal('{{ asset(ltrim($meetingRecPath, '/')) }}', '{{ addslashes($meeting->title) }}')">
|
|
▶️ Watch
|
|
</button>
|
|
<a href="{{ route('client-calls.download-meeting-recording', $meeting->id) }}" class="btn-copy" style="color: #818cf8; text-decoration: none;">
|
|
📥 Video
|
|
</a>
|
|
@endif
|
|
<button class="btn-copy" onclick="navigator.clipboard.writeText('{{ $meeting->join_url }}'); alert('Meeting link copied to clipboard!');">
|
|
📋 Copy Link
|
|
</button>
|
|
<a href="{{ $meeting->join_url }}" target="_blank" class="nav-btn-primary nav-btn" style="font-size: 0.85rem; padding: 0.55rem 1.1rem;">
|
|
👉 Join Room
|
|
</a>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- TAB 3: Project Specifications & Team -->
|
|
<div id="detailsTab" class="tab-content">
|
|
<div class="call-detail-card">
|
|
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem;">Project Scope & Specifications</h3>
|
|
<div style="font-size: 0.92rem; color: #cbd5e1; line-height: 1.6; background: var(--input-bg); padding: 1.25rem; border-radius: 12px; border: 1px solid var(--card-border); margin-bottom: 1.75rem;">
|
|
{{ $client->project_details ?: 'No detailed scope description provided.' }}
|
|
</div>
|
|
|
|
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem;">Assigned Internal Project Team</h3>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem;">
|
|
<div style="background: var(--input-bg); border: 1px solid var(--card-border); border-radius: 12px; padding: 1.2rem;">
|
|
<div style="font-size: 0.75rem; color: #818cf8; font-weight: 700; text-transform: uppercase;">Responsible Project Lead</div>
|
|
<div style="font-weight: 700; font-size: 1.05rem; margin-top: 4px;">{{ $client->responsibleUser->name ?? 'Unassigned' }}</div>
|
|
<div style="font-size: 0.82rem; color: var(--text-muted);">{{ $client->responsibleUser->email ?? '' }}</div>
|
|
</div>
|
|
|
|
@foreach($client->getTeamMembers() as $member)
|
|
<div style="background: var(--input-bg); border: 1px solid var(--card-border); border-radius: 12px; padding: 1.2rem;">
|
|
<div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; text-transform: uppercase;">Team Member</div>
|
|
<div style="font-weight: 700; font-size: 1.05rem; margin-top: 4px;">{{ $member->name }}</div>
|
|
<div style="font-size: 0.82rem; color: var(--text-muted);">{{ $member->email }}</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- 1. Schedule Meeting Modal -->
|
|
<div id="scheduleModal" class="modal-overlay">
|
|
<div class="modal-card">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Schedule Future Client Meeting</h2>
|
|
<button class="modal-close" onclick="closeScheduleModal()">×</button>
|
|
</div>
|
|
<form action="{{ route('client-calls.schedule-meeting', $client->id) }}" method="POST">
|
|
@csrf
|
|
<div class="form-group">
|
|
<label class="form-label">Meeting Title *</label>
|
|
<input type="text" name="title" class="form-control" placeholder="e.g. Sprint 4 Review & Roadmap Sync" required>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Scheduled Date & Time *</label>
|
|
<input type="datetime-local" name="scheduled_at" class="form-control" required value="{{ now()->addHour()->format('Y-m-d\TH:i') }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Estimated Duration (Minutes)</label>
|
|
<input type="number" name="duration_minutes" class="form-control" value="30" min="10" max="240" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Meeting Agenda / Discussion Points</label>
|
|
<textarea name="agenda" class="form-control" rows="2" placeholder="Topics to cover, questions for client, demo milestones..."></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Client Email(s) for Invitation (Comma separated)</label>
|
|
<input type="text" name="client_emails" class="form-control" value="{{ $client->client_email }}" placeholder="e.g. client@acme.com, director@acme.com">
|
|
<span style="font-size: 0.75rem; color: var(--text-muted);">External clients will receive an automated invitation email containing their direct video join link.</span>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Invite Internal Team Members</label>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; max-height: 120px; overflow-y: auto; background: var(--input-bg); border: 1px solid var(--card-border); border-radius: 10px; padding: 10px;">
|
|
@foreach($allUsers as $u)
|
|
<label style="display: flex; align-items: center; gap: 6px; font-size: 0.82rem; cursor: pointer;">
|
|
<input type="checkbox" name="team_attendees[]" value="{{ $u->id }}" {{ $u->id === Auth::id() || $u->id === $client->responsible_user_id ? 'checked' : '' }}>
|
|
<span>{{ $u->name }}</span>
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
|
|
<button type="button" class="nav-btn" onclick="closeScheduleModal()">Cancel</button>
|
|
<button type="submit" class="nav-btn-primary nav-btn">Schedule & Send Invitations</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Instant Meeting Modal -->
|
|
<div id="instantMeetingModal" class="modal-overlay">
|
|
<div class="modal-card">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title" style="display: flex; align-items: center; gap: 8px;">
|
|
<span>⚡</span> Start Instant Meeting
|
|
</h2>
|
|
<button class="modal-close" onclick="closeInstantMeetingModal()">×</button>
|
|
</div>
|
|
|
|
<p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 1.25rem;">
|
|
Create an ad-hoc instant meeting room immediately. Share the link with team members and client to join instantly like Google Meet.
|
|
</p>
|
|
|
|
<form id="instantMeetingForm" onsubmit="handleStartInstantMeeting(event)">
|
|
@csrf
|
|
<div class="form-group">
|
|
<label class="form-label">Instant Meeting Title</label>
|
|
<input type="text" id="instantTitle" name="title" class="form-control" value="Instant Sync - {{ $client->project_name }}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Client Email(s) to Invite Immediately</label>
|
|
<input type="text" id="instantEmails" name="client_emails" class="form-control" value="{{ $client->client_email }}">
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
|
|
<button type="button" class="nav-btn" onclick="closeInstantMeetingModal()">Cancel</button>
|
|
<button type="submit" id="btnLaunchInstant" class="nav-btn-instant nav-btn">Launch Call Now</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="instantResultBox" style="display: none; margin-top: 1.5rem; border-top: 1px solid var(--card-border); padding-top: 1.25rem;">
|
|
<div style="font-weight: 700; font-size: 0.9rem; color: #34d399; margin-bottom: 6px;">🎉 Instant Room Ready!</div>
|
|
<div class="share-link-box">
|
|
<span id="instantLinkText" class="share-link-text"></span>
|
|
<button class="btn-copy" onclick="copyInstantLink()">Copy Link</button>
|
|
</div>
|
|
<div style="text-align: center; margin-top: 1rem;">
|
|
<a id="btnJoinInstant" href="#" target="_blank" class="nav-btn-primary nav-btn" style="width: 100%; justify-content: center;">
|
|
👉 Enter Instant Call Room
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Add Custom Task Modal -->
|
|
<div id="addTaskModal" class="modal-overlay">
|
|
<div class="modal-card" style="max-width: 520px;">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Add Actionable Task</h2>
|
|
<button class="modal-close" onclick="closeAddTaskModal()">×</button>
|
|
</div>
|
|
<form id="addTaskForm" onsubmit="submitNewTask(event)">
|
|
<input type="hidden" id="addTaskNoteId" value="">
|
|
<div class="form-group">
|
|
<label class="form-label">Task Description *</label>
|
|
<input type="text" id="addTaskInput" class="form-control" placeholder="e.g. Deploy API contracts to staging" required>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Assignee / Owner</label>
|
|
<input type="text" id="addTaskOwner" class="form-control" placeholder="{{ Auth::user()->name }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Priority</label>
|
|
<select id="addTaskPriority" class="form-control">
|
|
<option value="High">High 🔴</option>
|
|
<option value="Medium" selected>Medium 🟡</option>
|
|
<option value="Low">Low 🔵</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Due Date</label>
|
|
<input type="text" id="addTaskDeadline" class="form-control" placeholder="e.g. Aug 25, 2026">
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.25rem;">
|
|
<button type="button" class="nav-btn" onclick="closeAddTaskModal()">Cancel</button>
|
|
<button type="submit" class="nav-btn-primary nav-btn">Save Task</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 4. Emotion Intelligence Breakdown Modal (Why happy, Why sad, Why angry) -->
|
|
<div id="emotionBreakdownModal" class="modal-overlay">
|
|
<div class="modal-card" style="max-width: 600px;">
|
|
<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="emotionBreakdownSubmeta" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;"></p>
|
|
</div>
|
|
<button class="modal-close" onclick="closeEmotionBreakdownModal()">×</button>
|
|
</div>
|
|
|
|
<div id="emotionBreakdownContent" style="padding: 10px 0;">
|
|
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">Analyzing emotion breakdown...</div>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: flex-end; margin-top: 1.25rem;">
|
|
<button type="button" class="nav-btn" onclick="closeEmotionBreakdownModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 5. Why Is He Angry Modal (Confidential to Admin & Responsible Person) -->
|
|
<div id="whyAngryModal" class="modal-overlay">
|
|
<div class="modal-card" style="max-width: 650px; border-color: rgba(239, 68, 68, 0.4);">
|
|
<div class="modal-header">
|
|
<div>
|
|
<h2 class="modal-title" style="color: #f87171; display: flex; align-items: center; gap: 8px;">
|
|
<span>😡</span> Managerial Analysis: Why is the client angry?
|
|
</h2>
|
|
<p style="font-size: 0.75rem; color: #fca5a5; margin-top: 4px;">Strictly Confidential • Restricted to Admin and Assigned Lead</p>
|
|
</div>
|
|
<button class="modal-close" onclick="closeWhyAngryModal()">×</button>
|
|
</div>
|
|
|
|
<div id="whyAngryContent">
|
|
<div style="text-align: center; padding: 2rem; color: var(--text-muted);">
|
|
Loading AI Analysis...
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: flex-end; margin-top: 1.5rem;">
|
|
<button type="button" class="nav-btn" onclick="closeWhyAngryModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Player Modal -->
|
|
<div id="videoPlayerModal" class="modal-overlay">
|
|
<div class="modal-card" style="max-width: 800px; padding: 1.5rem;">
|
|
<div class="modal-header">
|
|
<h2 id="videoModalTitle" class="modal-title" style="display: flex; align-items: center; gap: 8px;">
|
|
<span>🎥</span> Meeting Video Recording
|
|
</h2>
|
|
<button class="modal-close" onclick="closeVideoPlayerModal()">×</button>
|
|
</div>
|
|
<div style="background: #000; border-radius: 12px; overflow: hidden; margin-bottom: 1.25rem;">
|
|
<video id="recordingVideoPlayer" controls playsinline style="width: 100%; max-height: 480px; 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="btnDownloadVideoModal" href="#" class="nav-btn-primary nav-btn" style="font-size: 0.85rem;" download>
|
|
📥 Download Recording
|
|
</a>
|
|
<button type="button" class="nav-btn" onclick="closeVideoPlayerModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openVideoPlayerModal(videoUrl, title) {
|
|
const modal = document.getElementById('videoPlayerModal');
|
|
const video = document.getElementById('recordingVideoPlayer');
|
|
const titleEl = document.getElementById('videoModalTitle');
|
|
const dlBtn = document.getElementById('btnDownloadVideoModal');
|
|
|
|
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 closeVideoPlayerModal() {
|
|
const modal = document.getElementById('videoPlayerModal');
|
|
const video = document.getElementById('recordingVideoPlayer');
|
|
if (video) {
|
|
video.pause();
|
|
video.src = '';
|
|
}
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function switchTab(tabId, el) {
|
|
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
|
|
document.querySelectorAll('.tab-btn').forEach(tb => tb.classList.remove('active'));
|
|
document.getElementById(tabId).classList.add('active');
|
|
el.classList.add('active');
|
|
}
|
|
|
|
function openScheduleModal() { document.getElementById('scheduleModal').style.display = 'flex'; }
|
|
function closeScheduleModal() { document.getElementById('scheduleModal').style.display = 'none'; }
|
|
|
|
function openInstantMeetingModal() {
|
|
document.getElementById('instantMeetingModal').style.display = 'flex';
|
|
document.getElementById('instantResultBox').style.display = 'none';
|
|
}
|
|
function closeInstantMeetingModal() { document.getElementById('instantMeetingModal').style.display = 'none'; }
|
|
|
|
// Interactive Tasks Checkbox Toggle
|
|
function toggleTodoItem(cb, noteId, todoId) {
|
|
const titleEl = document.getElementById(`todotitle_${noteId}_${todoId}`);
|
|
if (titleEl) {
|
|
if (cb.checked) {
|
|
titleEl.classList.add('completed');
|
|
} else {
|
|
titleEl.classList.remove('completed');
|
|
}
|
|
}
|
|
fetch(`/client-calls/notes/${noteId}/toggle-todo/${todoId}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
}
|
|
}).catch(() => {});
|
|
}
|
|
|
|
// Add Task Modal
|
|
function openAddTaskModal(noteId) {
|
|
document.getElementById('addTaskNoteId').value = noteId;
|
|
document.getElementById('addTaskInput').value = '';
|
|
document.getElementById('addTaskDeadline').value = '';
|
|
document.getElementById('addTaskModal').style.display = 'flex';
|
|
}
|
|
function closeAddTaskModal() {
|
|
document.getElementById('addTaskModal').style.display = 'none';
|
|
}
|
|
|
|
async function submitNewTask(e) {
|
|
e.preventDefault();
|
|
const noteId = document.getElementById('addTaskNoteId').value;
|
|
const taskText = document.getElementById('addTaskInput').value.trim();
|
|
const owner = document.getElementById('addTaskOwner').value.trim();
|
|
const priority = document.getElementById('addTaskPriority').value;
|
|
const deadline = document.getElementById('addTaskDeadline').value.trim();
|
|
|
|
if (!taskText) return;
|
|
|
|
try {
|
|
const res = await fetch(`/client-calls/notes/${noteId}/add-todo`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
task: taskText,
|
|
owner: owner,
|
|
priority: priority,
|
|
deadline: deadline
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.success && data.todo) {
|
|
const container = document.getElementById(`tasks_list_${noteId}`);
|
|
const newDiv = document.createElement('div');
|
|
newDiv.className = 'task-row';
|
|
newDiv.id = `todorow_${noteId}_${data.todo.id}`;
|
|
newDiv.innerHTML = `
|
|
<input type="checkbox" class="task-checkbox" onchange="toggleTodoItem(this, ${noteId}, ${data.todo.id})">
|
|
<div class="task-content">
|
|
<div class="task-title" id="todotitle_${noteId}_${data.todo.id}">
|
|
${escapeHtml(data.todo.task)}
|
|
</div>
|
|
<div class="task-meta">
|
|
<span>👤 ${escapeHtml(data.todo.owner)}</span>
|
|
<span class="priority-badge priority-${data.todo.priority}">${data.todo.priority} Priority</span>
|
|
<span>📅 Due: ${escapeHtml(data.todo.deadline)}</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn-delete-task" onclick="deleteTodoItem(${noteId}, ${data.todo.id})" title="Delete Task">✕</button>
|
|
`;
|
|
container.appendChild(newDiv);
|
|
|
|
const countEl = document.getElementById(`task_count_${noteId}`);
|
|
if (countEl && data.todo_items) {
|
|
countEl.innerText = data.todo_items.length;
|
|
}
|
|
closeAddTaskModal();
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to add task.');
|
|
}
|
|
}
|
|
|
|
async function deleteTodoItem(noteId, todoId) {
|
|
if (!confirm('Delete this task?')) return;
|
|
try {
|
|
const res = await fetch(`/client-calls/notes/${noteId}/delete-todo/${todoId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
}
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
const row = document.getElementById(`todorow_${noteId}_${todoId}`);
|
|
if (row) row.remove();
|
|
const countEl = document.getElementById(`task_count_${noteId}`);
|
|
if (countEl && data.todo_items) {
|
|
countEl.innerText = data.todo_items.length;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to delete task.');
|
|
}
|
|
}
|
|
|
|
async function handleStartInstantMeeting(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('btnLaunchInstant');
|
|
btn.disabled = true;
|
|
btn.innerText = 'Creating Room...';
|
|
|
|
const title = document.getElementById('instantTitle').value;
|
|
const emails = document.getElementById('instantEmails').value;
|
|
|
|
try {
|
|
const res = await fetch(`{{ route('client-calls.instant-meeting', $client->id) }}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify({ title: title, client_emails: emails, send_email_now: true })
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
document.getElementById('instantResultBox').style.display = 'block';
|
|
document.getElementById('instantLinkText').innerText = data.join_url;
|
|
document.getElementById('btnJoinInstant').href = data.join_url;
|
|
btn.innerText = 'Created!';
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to launch instant meeting. Please try again.');
|
|
btn.disabled = false;
|
|
btn.innerText = 'Launch Call Now';
|
|
}
|
|
}
|
|
|
|
function copyInstantLink() {
|
|
const link = document.getElementById('instantLinkText').innerText;
|
|
navigator.clipboard.writeText(link);
|
|
alert('Instant meeting link copied to clipboard!');
|
|
}
|
|
|
|
async function openEmotionBreakdownModal(noteId) {
|
|
const modal = document.getElementById('emotionBreakdownModal');
|
|
const content = document.getElementById('emotionBreakdownContent');
|
|
const submeta = document.getElementById('emotionBreakdownSubmeta');
|
|
|
|
modal.style.display = 'flex';
|
|
content.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted);">Analyzing emotion breakdown...</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) {
|
|
content.innerHTML = `<div style="color:#f87171;padding:1rem;">${data.error || 'Access Restricted.'}</div>`;
|
|
return;
|
|
}
|
|
|
|
submeta.innerText = `${data.client_name} • ${data.project_name} (${data.created_at})`;
|
|
const emo = data.emotion_details || {};
|
|
const bullets = emo.bullet_points || [];
|
|
const quotes = emo.trigger_quotes || [];
|
|
|
|
content.innerHTML = `
|
|
<div style="background: rgba(99,102,241,0.12); border-left: 4px solid #6366f1; padding: 12px 14px; border-radius: 0 8px 8px 0; margin-bottom: 1.25rem;">
|
|
<div style="font-weight: 700; color: #818cf8; font-size: 1rem;">${emo.headline || 'Client Emotion Analysis'}</div>
|
|
</div>
|
|
<div style="margin-bottom: 1.25rem;">
|
|
<div style="font-size: 0.8rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 8px;">Key Reasons & Observations:</div>
|
|
<ul style="padding-left: 20px; font-size: 0.875rem; color: #cbd5e1; line-height: 1.6;">
|
|
${bullets.map(b => `<li>${escapeHtml(b)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
${quotes.length > 0 ? `
|
|
<div style="margin-bottom: 1.25rem;">
|
|
<div style="font-size: 0.8rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; margin-bottom: 8px;">Client Spoken Phrases:</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;">"${escapeHtml(q)}"</span>`).join('')}
|
|
</div>
|
|
</div>` : ''}
|
|
${emo.suggested_action ? `
|
|
<div style="background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 10px; padding: 12px 14px;">
|
|
<div style="font-size: 0.78rem; font-weight: 700; color: #34d399; text-transform: uppercase; margin-bottom: 4px;">💡 Recommended Action for PM & TL:</div>
|
|
<p style="font-size: 0.875rem; color: #d1fae5; line-height: 1.5;">${escapeHtml(emo.suggested_action)}</p>
|
|
</div>` : ''}
|
|
`;
|
|
} catch (e) {
|
|
content.innerHTML = '<div style="color:#f87171;padding:1rem;">Failed to load emotion breakdown.</div>';
|
|
}
|
|
}
|
|
function closeEmotionBreakdownModal() { document.getElementById('emotionBreakdownModal').style.display = 'none'; }
|
|
|
|
async function openWhyAngryModal(noteId) {
|
|
const modal = document.getElementById('whyAngryModal');
|
|
const content = document.getElementById('whyAngryContent');
|
|
modal.style.display = 'flex';
|
|
content.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-muted);">Analyzing transcript and emotion triggers...</div>';
|
|
|
|
try {
|
|
const res = await fetch(`/client-calls/notes/${noteId}/angry-analysis`, {
|
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
content.innerHTML = `<div style="color:#f87171;padding:1rem;background:rgba(239,68,68,0.1);border-radius:8px;">${data.error || 'Access Denied.'}</div>`;
|
|
return;
|
|
}
|
|
|
|
const analysis = data.angry_analysis || {};
|
|
const triggers = analysis.trigger_words || [];
|
|
const grievances = analysis.grievances || [];
|
|
|
|
content.innerHTML = `
|
|
<div style="margin-bottom: 1.25rem;">
|
|
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 4px;">Client & Project</div>
|
|
<div style="font-weight: 700; font-size: 1.1rem; color: var(--text-main);">${escapeHtml(data.client_name)} • ${escapeHtml(data.project_name)}</div>
|
|
</div>
|
|
|
|
<div style="background: rgba(239,68,68,0.12); border-left: 4px solid #ef4444; padding: 14px; border-radius: 0 10px 10px 0; margin-bottom: 1.25rem;">
|
|
<div style="font-weight: 700; font-size: 0.85rem; color: #fca5a5; margin-bottom: 6px; text-transform: uppercase;">AI Executive Breakdown</div>
|
|
<p style="font-size: 0.9rem; color: #fee2e2; line-height: 1.5;">${escapeHtml(analysis.summary_why_angry || 'Client showed frustration during the meeting.')}</p>
|
|
</div>
|
|
|
|
${triggers.length > 0 ? `
|
|
<div style="margin-bottom: 1.25rem;">
|
|
<div style="font-weight: 600; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 6px;">Identified Angry / Negative Keywords from Call:</div>
|
|
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
|
${triggers.map(t => `<span style="background: rgba(239,68,68,0.25); color: #fca5a5; font-size: 0.78rem; padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(239,68,68,0.4); font-weight: 600;">"${escapeHtml(t)}"</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
${grievances.length > 0 ? `
|
|
<div style="margin-bottom: 1.25rem;">
|
|
<div style="font-weight: 600; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 6px;">Key Complaints / Friction Points:</div>
|
|
<ul style="padding-left: 20px; font-size: 0.875rem; color: #cbd5e1; line-height: 1.6;">
|
|
${grievances.map(g => `<li>${escapeHtml(g)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : ''}
|
|
|
|
${analysis.suggested_remedy ? `
|
|
<div style="background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 10px; padding: 12px 14px;">
|
|
<div style="font-weight: 700; font-size: 0.8rem; color: #34d399; margin-bottom: 4px; text-transform: uppercase;">💡 Recommended Remedial Action for PM & Director:</div>
|
|
<p style="font-size: 0.875rem; color: #d1fae5; line-height: 1.5;">${escapeHtml(analysis.suggested_remedy)}</p>
|
|
</div>
|
|
` : ''}
|
|
`;
|
|
} catch (err) {
|
|
content.innerHTML = `<div style="color:#f87171;padding:1rem;">Failed to load analysis.</div>`;
|
|
}
|
|
}
|
|
|
|
function closeWhyAngryModal() {
|
|
document.getElementById('whyAngryModal').style.display = 'none';
|
|
}
|
|
|
|
async function resendEmail(noteId) {
|
|
if (!confirm('Re-send MoM and Notes email to TL, Director, PM, and Admin?')) return;
|
|
try {
|
|
const res = await fetch(`/client-calls/notes/${noteId}/resend-email`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
});
|
|
const data = await res.json();
|
|
alert(data.message || 'MoM Email dispatched!');
|
|
} catch (e) {
|
|
alert('Failed to send email.');
|
|
}
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/[&<>"']/g, function(m) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m];
|
|
});
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const isLight = document.documentElement.classList.toggle('light-mode');
|
|
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
|
const btn = document.getElementById('themeToggleBtn');
|
|
if (btn) btn.innerText = isLight ? '☀️' : '🌙';
|
|
}
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const isLight = document.documentElement.classList.contains('light-mode');
|
|
const btn = document.getElementById('themeToggleBtn');
|
|
if (btn) btn.innerText = isLight ? '☀️' : '🌙';
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|