Added credits tab in client dashboard
This commit is contained in:
parent
41f5eff4d7
commit
bc3b81e12f
|
|
@ -165,7 +165,6 @@ body {
|
|||
}
|
||||
|
||||
.mobile-menu-button {
|
||||
display: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
align-items: center;
|
||||
|
|
@ -201,6 +200,9 @@ body {
|
|||
.dashboard-sidebar {
|
||||
display: block;
|
||||
}
|
||||
.mobile-menu-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -217,6 +219,10 @@ body {
|
|||
.dashboard-sidebar.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
|
@ -2203,4 +2209,20 @@ tr[data-vaid]:hover {
|
|||
font-size: 12px;
|
||||
transform: none;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
profileMenu.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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) {
|
||||
showError('Please enter your Payoneer account number', 'payoneerAccount', step);
|
||||
isValid = false;
|
||||
} else if (!validateEmail(payoneerAccount)) {
|
||||
showError('Please enter a valid email address for Payoneer', 'payoneerAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (paymentMethod === 'veem') {
|
||||
const veemAccount = document.getElementById('veemAccount').value;
|
||||
if (!veemAccount) {
|
||||
showError('Please enter your Veem account number', 'veemAccount', step);
|
||||
isValid = false;
|
||||
} else if (!validateEmail(veemAccount)) {
|
||||
showError('Please enter a valid email address for Veem', 'veemAccount', step);
|
||||
isValid = false;
|
||||
}
|
||||
} else if (paymentMethod === 'wise') {
|
||||
const wiseEmail = document.getElementById('wiseEmail').value;
|
||||
|
|
@ -763,6 +769,48 @@
|
|||
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 generatePassword() {
|
||||
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 { 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 { va_credits_get } from "../../controllers/client/dashboard/clientDashboardCredits.controller.js";
|
||||
|
||||
// clients routes
|
||||
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.delete("/tips/:tipId", clientAuthValidator, va_tips_delete);
|
||||
|
||||
clientRoutes.get("/credits", clientAuthValidator, va_credits_get);
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>
|
||||
<%= title || 'Dashboard' %>
|
||||
</title>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<li>
|
||||
<a href="/client/projects" class="sidebar-link">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
<span>Projects</span>
|
||||
|
|
@ -33,6 +33,13 @@
|
|||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="/client/credits" class="sidebar-link">
|
||||
<i class="fas fa-wallet"></i>
|
||||
<span>Credits</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="javascript:void(0)" id="logoutButton" class="sidebar-link logout">
|
||||
|
|
|
|||
|
|
@ -248,11 +248,11 @@
|
|||
<div id="veem-details" class="payment-details">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<label for="veemAccount" class="input-label">Veem Account Number *</label>
|
||||
<input type="email" id="veemAccount" class="input-field" placeholder=" " oninput="validateVeemEmail(this)" required>
|
||||
<label for="veemAccount" class="input-label">Veem Email Address *</label>
|
||||
</div>
|
||||
<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>
|
||||
|
|
@ -281,11 +281,11 @@
|
|||
<div id="payoneer-details" class="payment-details">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<label for="payoneerAccount" class="input-label">Payoneer Account Number *</label>
|
||||
<input type="email" id="payoneerAccount" class="input-field" placeholder=" " oninput="validatePayoneerEmail(this)" required>
|
||||
<label for="payoneerAccount" class="input-label">Payoneer Email Address *</label>
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue