548 lines
22 KiB
Plaintext
548 lines
22 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en" data-theme="light">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DocVA Client | Dashboard</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
|
</head>
|
|
|
|
<style>
|
|
.void-btn-a {
|
|
background: #394059;
|
|
text-decoration: none;
|
|
color: #2b8efc !important;
|
|
border: none;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 5px;
|
|
font-weight: bold;
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
|
|
<body class="dashboard-body">
|
|
<div id="root">
|
|
<nav class="dashboard-header">
|
|
<div class="header-content">
|
|
<div class="header-logo">
|
|
<div class="logo-container">
|
|
<h2 class="sidebar-title">DocVA Client Portal</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-nav">
|
|
<ul class="nav-list">
|
|
<li class="nav-item">
|
|
<a href="/client/dashboard" class="nav-button">
|
|
<i class="fas fa-chart-bar"></i>Dashboard
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/client/virtual-assistants" class="nav-button active">
|
|
<i class="fas fa-users"></i>Virtual Assistants
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/client/projects" class="nav-button">
|
|
<i class="fas fa-file-invoice"></i>Projects
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<button class="mobile-menu-button">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div class="dashboard-container">
|
|
<aside class="dashboard-sidebar">
|
|
<nav class="sidebar-nav">
|
|
<ul class="sidebar-list">
|
|
<li class="sidebar-item">
|
|
<a href="/client/profile" class="sidebar-link">
|
|
<i class="fas fa-user sidebar-icon"></i>
|
|
<span>Profile</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="/client/dashboard" class="sidebar-link">
|
|
<i class="fas fa-chart-bar sidebar-icon"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="/client/virtual-assistants" class="sidebar-link active">
|
|
<i class="fas fa-users sidebar-icon"></i>
|
|
<span>Virtual Assistants</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="/client/projects" class="sidebar-link">
|
|
<i class="fas fa-file-invoice sidebar-icon"></i>
|
|
<span>Projects</span>
|
|
</a>
|
|
</li>
|
|
<li class="sidebar-item">
|
|
<a href="javascript:void(0);" class="sidebar-link" id="logoutButton">
|
|
<i class="fas fa-sign-out-alt sidebar-icon"></i>
|
|
<span>Logout</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
<main class="dashboard-main">
|
|
<div class="dashboard-content">
|
|
<!-- Stats Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-content">
|
|
<div class="stat-info">
|
|
<h3>Unbilled Tips</h3>
|
|
<div class="stat-value" id="vaUnbilledTips">$0</div>
|
|
</div>
|
|
<div class="stat-icon blue">
|
|
<i class="fas fa-file-invoice fa-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-content">
|
|
<div class="stat-info">
|
|
<h3>Total Tips</h3>
|
|
<div class="stat-value" id="vaTotalTips">$0</div>
|
|
</div>
|
|
<div class="stat-icon blue">
|
|
<i class="fas fa-dollar fa-xl"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Virtual Assistants List -->
|
|
<div class="content-section">
|
|
<div class="section-header" style="justify-content: space-between;align-items: center;">
|
|
<a href="/client/virtual-assistants" class="btn btn-secondary">Back</a>
|
|
<span class="btn btn-primary" id="tipVaAddBtn">Tip Virtual Assistant</span>
|
|
</div>
|
|
<div class="table-container">
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Void</th>
|
|
<th>Status</th>
|
|
<th>Virtual Assistant Name</th>
|
|
<th>Description</th>
|
|
<th>Date Added</th>
|
|
<th>Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="vaTipTableBody">
|
|
<!-- Data will be loaded here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="vaEmptyTipState" style="display: none;">
|
|
<div class="empty-state">
|
|
<i class="fas fa-users"></i>
|
|
<h3>No Tip Found</h3>
|
|
<p>You haven't gave any Tip yet.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<div id="tipModal" class="modal-overlay">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Tip Virtual Assistant</h2>
|
|
<button class="modal-close" onclick="closeTipModal()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form class="modal-form" id="tipForm">
|
|
<div class="form-group">
|
|
<label class="form-label">Select Virtual Assistant</label>
|
|
<select name="vaForTip" class="form-input" id="vaForTip" required>
|
|
<option value="">-- Select Virtual Assistant --</option>
|
|
<!-- Options will be populated dynamically -->
|
|
</select>
|
|
<div class="error-message" id="vaForTipError"></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Tip Amount</label>
|
|
<input type="text" class="form-input" id="vaTipAmount" required>
|
|
<div class="error-message" id="vaTipAmountError"></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Tip Description</label>
|
|
<textarea name="vaTipDescription" class="form-input" id="vaTipDescription"></textarea>
|
|
<div class="error-message" id="vaTipDescriptionError"></div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
|
|
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="logoutModal" class="modal-overlay">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">Confirm Logout</h2>
|
|
<button class="modal-close" onclick="closeLogoutModal()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="logout-modal-body">
|
|
<div class="logout-icon">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
</div>
|
|
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
|
<p class="logout-message">You will be logged out of your account and redirected to the login page.
|
|
</p>
|
|
<div class="logout-actions">
|
|
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
|
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// Theme management
|
|
function setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
function getSystemTheme() {
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
// Initialize theme
|
|
const savedTheme = getSystemTheme();
|
|
setTheme(savedTheme);
|
|
|
|
// API base URL
|
|
const API_BASE = '/api/v1/client';
|
|
|
|
// Auth token management
|
|
function getAuthToken() {
|
|
return localStorage.getItem('knoptpq');
|
|
}
|
|
|
|
async function fetchWithAuth(url, options = {}) {
|
|
const token = getAuthToken();
|
|
if (!token) {
|
|
window.location.href = '/client/login';
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'token': `${token}`,
|
|
'type': 'client',
|
|
'Content-Type': 'application/json',
|
|
'ngrok-skip-browser-warning': 69420,
|
|
...options.headers,
|
|
}
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
localStorage.removeItem('knoptpq');
|
|
localStorage.removeItem('refreshToken');
|
|
window.location.href = '/client/login';
|
|
return;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
// Call logout API
|
|
await fetchWithAuth(`${API_BASE}/logout`, {
|
|
method: 'POST'
|
|
});
|
|
} catch (error) {
|
|
console.error('Logout API error:', error);
|
|
} finally {
|
|
// Clear local storage and redirect
|
|
localStorage.removeItem('knoptpq');
|
|
localStorage.removeItem('refreshToken');
|
|
window.location.href = '/client/login';
|
|
}
|
|
}
|
|
|
|
// Logout Modal Functions
|
|
function openLogoutModal() {
|
|
const modal = document.getElementById('logoutModal');
|
|
modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeLogoutModal() {
|
|
const modal = document.getElementById('logoutModal');
|
|
modal.style.display = 'none';
|
|
document.body.style.overflow = 'auto';
|
|
}
|
|
|
|
function openTipModal() {
|
|
const modal = document.getElementById('tipModal');
|
|
modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// Clear form
|
|
document.getElementById('tipForm').reset();
|
|
clearErrorMessages();
|
|
}
|
|
|
|
function closeTipModal() {
|
|
const modal = document.getElementById('tipModal');
|
|
modal.style.display = 'none';
|
|
document.body.style.overflow = 'auto';
|
|
|
|
// Clear form and errors
|
|
document.getElementById('tipForm').reset();
|
|
clearErrorMessages();
|
|
}
|
|
|
|
function clearErrorMessages() {
|
|
document.getElementById('vaForTipError').textContent = '';
|
|
document.getElementById('vaTipAmountError').textContent = '';
|
|
document.getElementById('vaTipDescriptionError').textContent = '';
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
// Mobile menu toggle
|
|
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
const sidebar = document.querySelector('.dashboard-sidebar');
|
|
menuToggle.addEventListener('click', () => {
|
|
sidebar.classList.toggle('active');
|
|
});
|
|
|
|
document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
|
|
|
|
document.getElementById('tipModal').addEventListener('click', function (e) {
|
|
if (e.target === this) {
|
|
closeTipModal();
|
|
}
|
|
});
|
|
|
|
// Close modal with escape key
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') {
|
|
closeLogoutModal();
|
|
closeTipModal();
|
|
}
|
|
});
|
|
|
|
document.getElementById('logoutModal').addEventListener('click', function (e) {
|
|
if (e.target === this) {
|
|
closeLogoutModal();
|
|
}
|
|
});
|
|
|
|
// Logout button - opens confirmation modal instead of directly logging out
|
|
document.getElementById('logoutButton').addEventListener('click', openLogoutModal);
|
|
|
|
// Confirm logout button
|
|
document.getElementById('confirmLogoutBtn').addEventListener('click', handleLogout);
|
|
});
|
|
|
|
// ------------------------------
|
|
// TIP FORM SUBMISSION HANDLER
|
|
// ------------------------------
|
|
|
|
document.getElementById('tipForm').addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
|
|
clearErrorMessages();
|
|
|
|
const vaId = document.getElementById('vaForTip').value;
|
|
const amount = document.getElementById('vaTipAmount').value.trim();
|
|
const description = document.getElementById('vaTipDescription').value.trim();
|
|
|
|
let isValid = true;
|
|
|
|
// Validation
|
|
if (!vaId) {
|
|
document.getElementById('vaForTipError').textContent = "Please select a virtual assistant.";
|
|
isValid = false;
|
|
}
|
|
|
|
if (!amount || isNaN(amount) || Number(amount) <= 0) {
|
|
document.getElementById('vaTipAmountError').textContent = "Enter a valid tip amount.";
|
|
isValid = false;
|
|
}
|
|
|
|
if (!isValid) return;
|
|
|
|
// Prepare API body
|
|
const payload = {
|
|
vaId,
|
|
amount: Number(amount),
|
|
description
|
|
};
|
|
|
|
try {
|
|
document.getElementById('submitTipBtn').disabled = true;
|
|
document.getElementById('submitTipBtn').textContent = "Submitting...";
|
|
|
|
const response = await fetchWithAuth(`/api/v1/client/tips`, {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
alert(data.message || "Failed to submit tip.");
|
|
return;
|
|
}
|
|
|
|
alert("Tip submitted successfully!");
|
|
closeTipModal();
|
|
|
|
// Optionally refresh page or reload tips list
|
|
window.location.reload();
|
|
|
|
} catch (err) {
|
|
console.error("Tip submit error:", err);
|
|
alert("Something went wrong while submitting tip.");
|
|
} finally {
|
|
document.getElementById('submitTipBtn').disabled = false;
|
|
document.getElementById('submitTipBtn').textContent = "Submit Tip";
|
|
}
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Load VAs for tip selection
|
|
async function loadVAs() {
|
|
try {
|
|
const response = await fetchWithAuth(`${API_BASE}/dashboard`);
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
console.error("Failed to load VAs:", data.message);
|
|
return;
|
|
}
|
|
|
|
const vaSelect = document.getElementById('vaForTip');
|
|
data.data.allVAs.forEach(va => {
|
|
const option = document.createElement('option');
|
|
option.value = va.id;
|
|
option.textContent = va.name;
|
|
vaSelect.appendChild(option);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("Error loading VAs:", error);
|
|
}
|
|
}
|
|
|
|
loadVAs();
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Load VAs for tip selection
|
|
async function loadTips() {
|
|
try {
|
|
const response = await fetchWithAuth(`${API_BASE}/tips`);
|
|
|
|
if (!response.ok) {
|
|
console.error("Failed to load VAs:", data.message);
|
|
return;
|
|
}
|
|
const data = await response.json();
|
|
|
|
const vaSelect = document.getElementById('vaTipTableBody');
|
|
data.data.forEach(tip => {
|
|
const row = document.createElement('tr');
|
|
const cellVoid = document.createElement('td');
|
|
const cellStatus = document.createElement('td');
|
|
const cellName = document.createElement('td');
|
|
const cellDescription = document.createElement('td');
|
|
const cellDate = document.createElement('td');
|
|
const cellAmount = document.createElement('td');
|
|
|
|
cellVoid.innerHTML = `<button id='voidBtn' class='void-btn-a' data-id='${tip._id}'>Void</button>`;
|
|
cellVoid.classList.add('void-btn');
|
|
|
|
cellStatus.textContent = tip.status;
|
|
cellName.textContent = tip.vaName;
|
|
cellDescription.textContent = tip.description;
|
|
cellDate.textContent = tip.createdAt ? new Date(tip.createdAt).toLocaleDateString() : '';
|
|
cellAmount.textContent = `$${parseFloat(tip.amount).toFixed(2)}`;
|
|
|
|
row.appendChild(cellVoid);
|
|
row.appendChild(cellStatus);
|
|
row.appendChild(cellName);
|
|
row.appendChild(cellDescription);
|
|
row.appendChild(cellDate);
|
|
row.appendChild(cellAmount);
|
|
|
|
vaSelect.appendChild(row);
|
|
});
|
|
|
|
document.querySelectorAll('.void-btn-a').forEach(button => {
|
|
button.addEventListener('click', async function () {
|
|
const tipId = this.getAttribute('data-id');
|
|
if (confirm('Are you sure you want to void this tip?')) {
|
|
try {
|
|
const response = await fetchWithAuth(`${API_BASE}/tips/${tipId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (!response.ok) {
|
|
alert(result.message || 'Failed to void tip.');
|
|
return;
|
|
}
|
|
|
|
alert('Tip voided successfully!');
|
|
window.location.reload();
|
|
} catch (error) {
|
|
console.error('Error voiding tip:', error);
|
|
alert('Something went wrong while voiding the tip.');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("Error loading VAs:", error);
|
|
}
|
|
}
|
|
|
|
loadTips();
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |