Added credits tab in client dashboard

This commit is contained in:
Aakash Mittal 2025-12-22 13:56:02 +05:30
parent 41f5eff4d7
commit bc3b81e12f
11 changed files with 266 additions and 9 deletions

View File

@ -165,7 +165,6 @@ body {
} }
.mobile-menu-button { .mobile-menu-button {
display: none;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
align-items: center; align-items: center;
@ -201,6 +200,9 @@ body {
.dashboard-sidebar { .dashboard-sidebar {
display: block; display: block;
} }
.mobile-menu-button {
display: none;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -217,6 +219,10 @@ body {
.dashboard-sidebar.active { .dashboard-sidebar.active {
transform: translateX(0); transform: translateX(0);
} }
.nav-list {
display: none;
}
} }
.sidebar-header { .sidebar-header {
@ -2204,3 +2210,19 @@ tr[data-vaid]:hover {
transform: none; transform: none;
color: var(--accent-light); color: var(--accent-light);
} }
.data-table #vaCreditTableBody .status{
text-transform: capitalize;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
padding: 4px 10px;
border-radius: 20px;
display: inline-block;
background-color: var(--color-gray-200);
}
.data-table #vaCreditTableBody .status.invoiced {
background-color: rgba(16, 185, 129, 0.1);
color: var(--color-success);
}

View File

@ -27,3 +27,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}); });
document.addEventListener("DOMContentLoaded", () => {
const mobileMenuButton = document.querySelector(".mobile-menu-button");
const sidebar = document.querySelector(".dashboard-sidebar");
mobileMenuButton.addEventListener("click", () => {
sidebar.classList.toggle("active");
}
);
});

View File

@ -0,0 +1,61 @@
// API base URL
const API_BASE = '/api/v1/client';
const vaCreditsTableBody = document.getElementById("vaCreditTableBody");
const vaCreditsEmptyState = document.getElementById("vaEmptyCreditState");
function createCreditRow(credit) {
return `
<tr data-credit-id="${credit.id}">
<td><span class="status ${credit.status.toLowerCase()}">${credit.status}</span></td>
<td>${credit.date}</td>
<td>${credit.comment}</td>
<td>$${credit.creditAmount}</td>
</tr>
`;
}
async function loadCreditsData() {
const res = await fetchWithAuth(`${API_BASE}/credits`);
if (!res) return;
const result = await res.json();
if (!result.success) return;
console.log("Credits Result:", result);
const { data } = result;
console.log("Credits Data:", data);
const allCredits = data || [];
if (allCredits.length === 0) {
vaCreditsTableBody.innerHTML = "";
vaCreditsEmptyState.style.display = "block";
return;
}
vaCreditsEmptyState.style.display = "none";
vaCreditsTableBody.innerHTML = allCredits
.map((credit) => createCreditRow(credit))
.join("");
let totalCredits = 0;
let availableCredits = 0;
allCredits.forEach(credit => {
totalCredits += parseFloat(credit.creditAmount);
if (credit.status.toLowerCase() === 'pending') {
availableCredits += parseFloat(credit.creditAmount);
}
});
document.getElementById("vaTotalCredits").textContent = `$${totalCredits}`;
document.getElementById("vaAvailableCredits").textContent = `$${availableCredits}`;
}
// Initial load
loadCreditsData();

View File

@ -621,12 +621,18 @@
if (!payoneerAccount) { if (!payoneerAccount) {
showError('Please enter your Payoneer account number', 'payoneerAccount', step); showError('Please enter your Payoneer account number', 'payoneerAccount', step);
isValid = false; isValid = false;
} else if (!validateEmail(payoneerAccount)) {
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
isValid = false;
} }
} else if (paymentMethod === 'veem') { } else if (paymentMethod === 'veem') {
const veemAccount = document.getElementById('veemAccount').value; const veemAccount = document.getElementById('veemAccount').value;
if (!veemAccount) { if (!veemAccount) {
showError('Please enter your Veem account number', 'veemAccount', step); showError('Please enter your Veem account number', 'veemAccount', step);
isValid = false; isValid = false;
} else if (!validateEmail(veemAccount)) {
showError('Please enter a valid email address for Veem', 'veemAccount', step);
isValid = false;
} }
} else if (paymentMethod === 'wise') { } else if (paymentMethod === 'wise') {
const wiseEmail = document.getElementById('wiseEmail').value; const wiseEmail = document.getElementById('wiseEmail').value;
@ -763,6 +769,48 @@
return true; return true;
} }
function validateVeemEmail(input) {
const wiseError = document.getElementById('veem-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
wiseError.style.display = 'none';
if (value.length > 0) {
// Check if it's a valid email
if (!validateEmail(value)) {
wiseError.textContent = 'Please enter a valid email address for Veem';
wiseError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
}
}
return true;
}
function validatePayoneerEmail(input) {
const wiseError = document.getElementById('payoneer-error');
const value = input.value.trim();
// Remove any validation styling first
input.style.border = '';
wiseError.style.display = 'none';
if (value.length > 0) {
// Check if it's a valid email
if (!validateEmail(value)) {
wiseError.textContent = 'Please enter a valid email address for Payoneer';
wiseError.style.display = 'block';
input.style.border = '1px solid #ef4444';
return false;
}
}
return true;
}
// Function to generate a strong password // Function to generate a strong password
function generatePassword() { function generatePassword() {
const length = 12; const length = 12;

View File

@ -0,0 +1,40 @@
import authFetch from "../../../utils/authFetch.js";
async function va_credits_get(req, res) {
try {
const clientId = req.body.clientId;
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/credit/${clientId}`;
const response = await authFetch(salesforceUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Add any required headers for Salesforce authentication
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
}
const result = await response.json();
// console.log("✅ Successfully synced VA data to Salesforce:", result);
res.status(201).json({
success: true,
data: result.data
});
} catch (error) {
console.error("Failed to sync with Salesforce:", error.message);
console.log(error)
// Create a custom error to distinguish Salesforce errors
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
salesforceError.isSalesforceError = true;
throw salesforceError;
}
}
export { va_credits_get };

View File

@ -15,6 +15,7 @@ import { client_dashboard_projects } from "../../controllers/client/dashboard/cl
import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js"; import { client_logout_callback } from "../../controllers/client/dashboard/clientLogout.controller.js";
import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js"; import { va_tips_delete, va_tips_get, va_tips_manage_function, va_tips_update } from "../../controllers/va/vaTips/vaTipsManage.controller.js";
import { clientDashboardGetVaLogsController } from "../../controllers/client/dashboard/clientDashboardGetVaLogs.controller.js"; import { clientDashboardGetVaLogsController } from "../../controllers/client/dashboard/clientDashboardGetVaLogs.controller.js";
import { va_credits_get } from "../../controllers/client/dashboard/clientDashboardCredits.controller.js";
// clients routes // clients routes
clientRoutes.post("/register", va_register_client); // Register a new VA clientRoutes.post("/register", va_register_client); // Register a new VA
@ -29,6 +30,8 @@ clientRoutes.get("/tips", clientAuthValidator, va_tips_get);
clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update); clientRoutes.patch("/tips/:tipId", clientAuthValidator, va_tips_update);
clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete); clientRoutes.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
clientRoutes.get("/credits", clientAuthValidator, va_credits_get);
clientRoutes.get("/vaLogsData/:vaId", clientAuthValidator, clientDashboardGetVaLogsController); clientRoutes.get("/vaLogsData/:vaId", clientAuthValidator, clientDashboardGetVaLogsController);
// clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC) // clientRoutes.get("/:clientID?", va_clients_callback); // VA Clients Callback for getting all VA's or specific client by ID (NC)

View File

@ -248,6 +248,19 @@ app.get('/client/virtual-assistants/view-logs', async (req, res) => {
} }
}); });
app.get('/client/credits', async (req, res) => {
try {
// res.render('client/dashboard-va-logs');
res.render('client/dashboard-credits', {
title: 'Credits',
pageScript: 'va-credits.js'
});
} catch (error) {
console.error('Projects error:', error);
res.status(500).render('client/dashboard-va-logs', { error: 'Failed to load profile' });
}
});
app.get("/live/:projectId/:vaId", async (req, res) => { app.get("/live/:projectId/:vaId", async (req, res) => {
const { projectId, vaId } = req.params; const { projectId, vaId } = req.params;

View File

@ -0,0 +1,52 @@
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Total Credits</h3>
<div class="stat-value" id="vaTotalCredits">$0</div>
</div>
<div class="stat-icon blue">
<i class="fas fa-dollar fa-xl"></i>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<h3>Available Credits</h3>
<div class="stat-value" id="vaAvailableCredits">$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="table-container">
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Status</th>
<th>Date</th>
<th>Description</th>
<th>Amount</th>
</tr>
</thead>
<tbody id="vaCreditTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<div id="vaEmptyCreditState" style="display: none;">
<div class="empty-state">
<i class="fas fa-users"></i>
<h3>No Credit Found</h3>
<p>You haven't received any credit yet.</p>
</div>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title> <title>
<%= title || 'Dashboard' %> <%= title || 'Dashboard' %>
</title> </title>

View File

@ -33,6 +33,13 @@
</li> </li>
<li>
<a href="/client/credits" class="sidebar-link">
<i class="fas fa-wallet"></i>
<span>Credits</span>
</a>
</li>
<li> <li>
<a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout"> <a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout">

View File

@ -248,11 +248,11 @@
<div id="veem-details" class="payment-details"> <div id="veem-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="veemAccount" class="input-field" placeholder=" " minlength="8" maxlength="25" oninput="validateVeemAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required> <input type="email" id="veemAccount" class="input-field" placeholder=" " oninput="validateVeemEmail(this)" required>
<label for="veemAccount" class="input-label">Veem Account Number *</label> <label for="veemAccount" class="input-label">Veem Email Address *</label>
</div> </div>
<div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;"> <div class="invalid-feedback" id="veem-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 8-25 characters Invalid Veem email address
</div> </div>
</div> </div>
</div> </div>
@ -281,11 +281,11 @@
<div id="payoneer-details" class="payment-details"> <div id="payoneer-details" class="payment-details">
<div class="form-group"> <div class="form-group">
<div class="input-container"> <div class="input-container">
<input type="text" id="payoneerAccount" class="input-field" placeholder=" " minlength="5" maxlength="20" oninput="validatePayoneerAccount(this)" onkeypress="return event.charCode >= 48 && event.charCode <= 57" required> <input type="email" id="payoneerAccount" class="input-field" placeholder=" " oninput="validatePayoneerEmail(this)" required>
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label> <label for="payoneerAccount" class="input-label">Payoneer Email Address *</label>
</div> </div>
<div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;"> <div class="invalid-feedback" id="payoneer-error" style="display: none; color: #dc3545; font-size: 12px; margin-top: 5px;">
Account number must be 5-20 characters Please enter a valid email address for Payoneer
</div> </div>
</div> </div>
</div> </div>