Added credits tab in client dashboard
This commit is contained in:
parent
41f5eff4d7
commit
bc3b81e12f
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/client/projects" class="sidebar-link">
|
<a href="/client/projects" class="sidebar-link">
|
||||||
<i class="fas fa-file-invoice"></i>
|
<i class="fas fa-file-invoice"></i>
|
||||||
<span>Projects</span>
|
<span>Projects</span>
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue