Client and VA Update and VA Tip
This commit is contained in:
parent
15f0f3d782
commit
4d617d3113
|
|
@ -74,7 +74,7 @@ async function va_register(req, res) {
|
||||||
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
res.status(201).json({ success: true, data: created, SyncSalesforce: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to sync with Salesforce:", error.message);
|
console.error("❌ Failed to sync with Salesforce:", error.message);
|
||||||
|
await VAModel.findOneAndDelete({_id: created.userDetails._id});
|
||||||
// Create a custom error to distinguish Salesforce errors
|
// Create a custom error to distinguish Salesforce errors
|
||||||
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
const salesforceError = new Error(`Salesforce sync failed: ${error.message}`);
|
||||||
salesforceError.isSalesforceError = true;
|
salesforceError.isSalesforceError = true;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
|
import mongoose from "mongoose";
|
||||||
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
import { AdjustedHoursModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
// Get all adjustment requests (with filtering options)
|
// Get all adjustment requests (with filtering options)
|
||||||
async function get_adjustment_requests_list(req, res) {
|
async function get_adjustment_requests_list(req, res) {
|
||||||
try {
|
try {
|
||||||
const { status, projectId, vaId } = req.params;
|
const { status, projectId, vaId } = req.query;
|
||||||
|
|
||||||
// Build filter object
|
// Build filter object
|
||||||
const filter = {};
|
const filter = {};
|
||||||
if (status) filter.status = status;
|
// if (status) filter.status = status;
|
||||||
if (projectId) filter.projectId = projectId;
|
// if (projectId) filter.projectId = projectId;
|
||||||
if (vaId) filter.vaId = vaId;
|
// if (vaId) filter.vaId = vaId;
|
||||||
|
|
||||||
|
if (status && status !== "undefined") filter.status = status;
|
||||||
|
if (projectId && mongoose.Types.ObjectId.isValid(projectId)) {
|
||||||
|
filter.projectId = new mongoose.Types.ObjectId(projectId);
|
||||||
|
}
|
||||||
|
if (vaId && mongoose.Types.ObjectId.isValid(vaId)) {
|
||||||
|
filter.vaId = new mongoose.Types.ObjectId(vaId);
|
||||||
|
}
|
||||||
|
|
||||||
const adjustments = await AdjustedHoursModel.find(filter)
|
const adjustments = await AdjustedHoursModel.find(filter)
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { VAModel, TipsModel, ClientModel } from "../../../models/vaModel.js";
|
||||||
|
|
||||||
|
// Approve or reject leave
|
||||||
|
async function va_tips_manage_function(req, res) {
|
||||||
|
|
||||||
|
const { clientId, amount, description, vaId } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const vaUser = await VAModel.findById(vaId);
|
||||||
|
if (!vaUser) {
|
||||||
|
return res.status(404).json({ success: false, message: "Virtual Assistant not found" });
|
||||||
|
}
|
||||||
|
const clUser = await ClientModel.findById(clientId);
|
||||||
|
if (!clUser) {
|
||||||
|
return res.status(404).json({ success: false, message: "Client not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tip entry
|
||||||
|
const newTip = {
|
||||||
|
clientId,
|
||||||
|
status: "Unbilled",
|
||||||
|
amount: parseFloat(amount).toFixed(2),
|
||||||
|
description,
|
||||||
|
vaName: vaUser.userDetails.firstName ? vaUser.userDetails.firstName : "" + " " + vaUser.userDetails.lastName ? vaUser.userDetails.lastName : "",
|
||||||
|
vaId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdTip = await TipsModel.create(newTip);
|
||||||
|
|
||||||
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
|
||||||
|
|
||||||
|
const sfData = {
|
||||||
|
"id": createdTip._id.toString(),
|
||||||
|
"vaId": createdTip.vaId.toString(),
|
||||||
|
"status": "Unbilled",
|
||||||
|
"clientId": createdTip.clientId.toString(),
|
||||||
|
"comment": createdTip.description,
|
||||||
|
"tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(salesforceUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// Add any required headers for Salesforce authentication
|
||||||
|
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...sfData })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log("✅ Successfully synced Tip data to Salesforce");
|
||||||
|
return res.status(201).json({ success: true, data: createdTip });
|
||||||
|
} else {
|
||||||
|
TipsModel.findByIdAndDelete(createdTip._id);
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ success: false, message: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function va_tips_get(req, res) {
|
||||||
|
|
||||||
|
const { clientId } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clUser = await ClientModel.findById(clientId);
|
||||||
|
if (!clUser) {
|
||||||
|
return res.status(404).json({ success: false, message: "Client not found" });
|
||||||
|
}
|
||||||
|
const data = await TipsModel.find({ clientId: clientId, status: "Unbilled" });
|
||||||
|
return res.status(200).json({ success: true, data: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ success: false, message: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function va_tips_update(req, res) {
|
||||||
|
const { tipId } = req.params;
|
||||||
|
const { update } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tipData = await TipsModel.findById(tipId);
|
||||||
|
if (!tipData) {
|
||||||
|
return res.status(404).json({ success: false, message: "Tip not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await TipsModel.updateOne({ _id: tipId }, update);
|
||||||
|
return res.status(200).json({ success: true, message: "Tip updated successfully" });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ success: false, message: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function va_tips_delete(req, res) {
|
||||||
|
const { tipId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-tips`;
|
||||||
|
|
||||||
|
// const sfData = {
|
||||||
|
// "id": createdTip._id.toString(),
|
||||||
|
// "vaId": createdTip.vaId.toString(),
|
||||||
|
// "status": "Unbilled",
|
||||||
|
// "clientId": createdTip.clientId.toString(),
|
||||||
|
// "comment": createdTip.description,
|
||||||
|
// "tipAmount": Number(parseFloat(createdTip.amount).toFixed(2)), // Convert to cents
|
||||||
|
// }
|
||||||
|
|
||||||
|
const response = await fetch(salesforceUrl, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
// Add any required headers for Salesforce authentication
|
||||||
|
// 'Authorization': 'Bearer ' + process.env.SF_ACCESS_TOKEN
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ "id": tipId})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await TipsModel.findOneAndDelete({ _id: tipId });
|
||||||
|
return res.status(200).json({ success: true, message: "Tip updated successfully" });
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ success: false, message: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { va_tips_manage_function, va_tips_get, va_tips_update, va_tips_delete };
|
||||||
|
|
@ -50,6 +50,15 @@ async function authValidator(req, res, next) {
|
||||||
|
|
||||||
async function clientAuthValidator(req, res, next) {
|
async function clientAuthValidator(req, res, next) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
const sf_token = req.headers.sf_token;
|
||||||
|
if (sf_token) {
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sf_token) return;
|
||||||
|
|
||||||
const accessToken = req.headers.token;
|
const accessToken = req.headers.token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,37 @@ const leavesSchema = new Schema({
|
||||||
}
|
}
|
||||||
}, { timestamps: true });
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
|
||||||
|
/* ---------------------- Tips Schema ---------------------- */
|
||||||
|
const tipsSchema = new Schema({
|
||||||
|
clientId: {
|
||||||
|
type: Types.ObjectId,
|
||||||
|
ref: "Client"
|
||||||
|
},
|
||||||
|
vaId: {
|
||||||
|
type: Types.ObjectId,
|
||||||
|
ref: "VA"
|
||||||
|
},
|
||||||
|
vaName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ["Unbilled", "Billed"],
|
||||||
|
default: "Unbilled",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
}
|
||||||
|
}, { timestamps: true })
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------- Model Exports ---------------------- */
|
/* ---------------------- Model Exports ---------------------- */
|
||||||
const VAModel = model("VA", vaSchema);
|
const VAModel = model("VA", vaSchema);
|
||||||
const ClientModel = model("Client", clientSchema);
|
const ClientModel = model("Client", clientSchema);
|
||||||
|
|
@ -520,5 +551,6 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema);
|
||||||
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
|
const PasswordResetModel = model("PasswordReset", passwordResetSchema);
|
||||||
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
|
const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema);
|
||||||
const LeavesModel = model("Leaves", leavesSchema);
|
const LeavesModel = model("Leaves", leavesSchema);
|
||||||
|
const TipsModel = model("Tips", tipsSchema);
|
||||||
|
|
||||||
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel };
|
export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel };
|
||||||
|
|
@ -13,6 +13,7 @@ import { client_dashboard_va_log } from "../../controllers/client/dashboard/clie
|
||||||
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
|
import { client_verify_email } from "../../controllers/client/vaClientEmailVerify.controller.js";
|
||||||
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
|
import { client_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.controller.js";
|
||||||
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";
|
||||||
|
|
||||||
// clients routes
|
// clients routes
|
||||||
clientRoutes.post("/register", va_register_client); // Register a new VA
|
clientRoutes.post("/register", va_register_client); // Register a new VA
|
||||||
|
|
@ -22,6 +23,11 @@ clientRoutes.post("/forgot-password", client_request_password_reset);
|
||||||
clientRoutes.post("/verify-client-email", client_verify_email);
|
clientRoutes.post("/verify-client-email", client_verify_email);
|
||||||
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
clientRoutes.post("/logout", clientAuthValidator, client_logout_callback);
|
||||||
|
|
||||||
|
clientRoutes.post("/tips", clientAuthValidator, va_tips_manage_function);
|
||||||
|
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("/: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)
|
||||||
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
clientRoutes.get("/:clientId/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ vaRoutes.delete("/profile/:id", va_remove_function); // VA delete Callback
|
||||||
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
vaRoutes.get("/timesheet/summary/:projectId/:date/:vaId?", authValidator, va_timesheet_summary_function); // Get Timesheet Adjustment for VA on specific project and date
|
||||||
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
vaRoutes.post("/timesheet/adjustment/request", authValidator, va_timesheet_adjustment_function); // Timesheet Adjustment for VA
|
||||||
vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
vaRoutes.get("/timesheet/adjustments/list/:projectId?/:vaId?/:status?", get_adjustment_requests_list); // show all adjustment list to approve and reject
|
||||||
|
|
||||||
|
|
||||||
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
vaRoutes.patch("/timesheet/adjustments/:adjustmentId", va_timesheet_manage_adjustment_request);
|
||||||
|
|
||||||
// Leaves routes
|
// Leaves routes
|
||||||
|
|
@ -56,7 +58,7 @@ vaRoutes.get("/leaves/status/:status", authValidator, va_leave_list_function); /
|
||||||
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
|
vaRoutes.get("/leaves/type/:leaveType", authValidator, va_leave_list_function); // Filter by leave type
|
||||||
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
vaRoutes.get("/leaves/status/:status/type/:leaveType", authValidator, va_leave_list_function); // leaves Filter
|
||||||
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
|
vaRoutes.post("/leave/request", authValidator, va_leave_apply_function); // Leave for VA
|
||||||
vaRoutes.patch("/leave/:leaveId", va_leave_manage_function);
|
vaRoutes.patch("/leave/:leaveId", authValidator, va_leave_manage_function);
|
||||||
|
|
||||||
// timesheet adjustment routes
|
// timesheet adjustment routes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
const convertToObjectId = (id) => {
|
||||||
|
return mongoose.Types.ObjectId.isValid(id) ? new mongoose.Types.ObjectId(id) : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default convertToObjectId;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en" data-theme="light">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
@ -7,6 +8,21 @@
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<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">
|
<link rel="stylesheet" href="/public/css/dashboard.css">
|
||||||
</head>
|
</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">
|
<body class="dashboard-body">
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<nav class="dashboard-header">
|
<nav class="dashboard-header">
|
||||||
|
|
@ -155,9 +171,8 @@
|
||||||
<form class="modal-form" id="tipForm">
|
<form class="modal-form" id="tipForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Select Virtual Assistant</label>
|
<label class="form-label">Select Virtual Assistant</label>
|
||||||
<select name="vaForTip" class="form-input select-field" id="vaForTip" required>
|
<select name="vaForTip" class="form-input" id="vaForTip" required>
|
||||||
<option value="">-- Select Virtual Assistant --</option>
|
<option value="">-- Select Virtual Assistant --</option>
|
||||||
<option value="va 1">VA 1</option>
|
|
||||||
<!-- Options will be populated dynamically -->
|
<!-- Options will be populated dynamically -->
|
||||||
</select>
|
</select>
|
||||||
<div class="error-message" id="vaForTipError"></div>
|
<div class="error-message" id="vaForTipError"></div>
|
||||||
|
|
@ -174,7 +189,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
|
<button type="button" class="cancel-btn" onclick="closeTipModal()">Cancel</button>
|
||||||
<button type="submit" class="save-btn" id="savePasswordBtn">Submit Tip</button>
|
<button type="submit" class="save-btn" id="submitTipBtn">Submit Tip</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -194,7 +209,8 @@
|
||||||
<i class="fas fa-sign-out-alt"></i>
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="logout-title">Are you sure you want to logout?</h3>
|
<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>
|
<p class="logout-message">You will be logged out of your account and redirected to the login page.
|
||||||
|
</p>
|
||||||
<div class="logout-actions">
|
<div class="logout-actions">
|
||||||
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
<button class="logout-cancel-btn" onclick="closeLogoutModal()">Cancel</button>
|
||||||
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
<button class="logout-confirm-btn" id="confirmLogoutBtn">Yes, Logout</button>
|
||||||
|
|
@ -203,148 +219,330 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Theme management
|
// Theme management
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem('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, {
|
function getSystemTheme() {
|
||||||
...options,
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
headers: {
|
}
|
||||||
'token': `${token}`,
|
|
||||||
'Content-Type': 'application/json',
|
// Initialize theme
|
||||||
...options.headers,
|
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;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
const response = await fetch(url, {
|
||||||
localStorage.removeItem('knoptpq');
|
...options,
|
||||||
localStorage.removeItem('refreshToken');
|
headers: {
|
||||||
window.location.href = '/client/login';
|
'token': `${token}`,
|
||||||
return;
|
'type': 'client',
|
||||||
}
|
'Content-Type': 'application/json',
|
||||||
|
'ngrok-skip-browser-warning': 69420,
|
||||||
return response;
|
...options.headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
|
||||||
try {
|
|
||||||
// Call logout API
|
|
||||||
await fetchWithAuth(`${API_BASE}/logout`, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout API error:', error);
|
if (response.status === 401) {
|
||||||
} finally {
|
localStorage.removeItem('knoptpq');
|
||||||
// Clear local storage and redirect
|
localStorage.removeItem('refreshToken');
|
||||||
localStorage.removeItem('knoptpq');
|
window.location.href = '/client/login';
|
||||||
localStorage.removeItem('refreshToken');
|
return;
|
||||||
window.location.href = '/client/login';
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Logout Modal Functions
|
async function handleLogout() {
|
||||||
function openLogoutModal() {
|
try {
|
||||||
const modal = document.getElementById('logoutModal');
|
// Call logout API
|
||||||
modal.style.display = 'flex';
|
await fetchWithAuth(`${API_BASE}/logout`, {
|
||||||
document.body.style.overflow = 'hidden';
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeLogoutModal() {
|
// Logout Modal Functions
|
||||||
const modal = document.getElementById('logoutModal');
|
function openLogoutModal() {
|
||||||
modal.style.display = 'none';
|
const modal = document.getElementById('logoutModal');
|
||||||
document.body.style.overflow = 'auto';
|
modal.style.display = 'flex';
|
||||||
}
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
function openTipModal() {
|
function closeLogoutModal() {
|
||||||
const modal = document.getElementById('tipModal');
|
const modal = document.getElementById('logoutModal');
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'none';
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
// Clear form
|
function openTipModal() {
|
||||||
document.getElementById('tipForm').reset();
|
const modal = document.getElementById('tipModal');
|
||||||
clearErrorMessages();
|
modal.style.display = 'flex';
|
||||||
}
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
function closeTipModal() {
|
// Clear form
|
||||||
const modal = document.getElementById('tipModal');
|
document.getElementById('tipForm').reset();
|
||||||
modal.style.display = 'none';
|
clearErrorMessages();
|
||||||
document.body.style.overflow = 'auto';
|
}
|
||||||
|
|
||||||
// Clear form and errors
|
function closeTipModal() {
|
||||||
document.getElementById('tipForm').reset();
|
const modal = document.getElementById('tipModal');
|
||||||
clearErrorMessages();
|
modal.style.display = 'none';
|
||||||
}
|
document.body.style.overflow = 'auto';
|
||||||
|
|
||||||
function clearErrorMessages() {
|
// Clear form and errors
|
||||||
document.getElementById('vaForTipError').textContent = '';
|
document.getElementById('tipForm').reset();
|
||||||
document.getElementById('vaTipAmountError').textContent = '';
|
clearErrorMessages();
|
||||||
document.getElementById('vaTipDescriptionError').textContent = '';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
function clearErrorMessages() {
|
||||||
|
document.getElementById('vaForTipError').textContent = '';
|
||||||
|
document.getElementById('vaTipAmountError').textContent = '';
|
||||||
|
document.getElementById('vaTipDescriptionError').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Mobile menu toggle
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const menuToggle = document.querySelector('.mobile-menu-button');
|
|
||||||
const sidebar = document.querySelector('.dashboard-sidebar');
|
// Mobile menu toggle
|
||||||
menuToggle.addEventListener('click', () => {
|
const menuToggle = document.querySelector('.mobile-menu-button');
|
||||||
sidebar.classList.toggle('active');
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('tipVaAddBtn').addEventListener('click', openTipModal);
|
// ------------------------------
|
||||||
|
// TIP FORM SUBMISSION HANDLER
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
document.getElementById('tipModal').addEventListener('click', function(e) {
|
document.getElementById('tipForm').addEventListener('submit', async function (e) {
|
||||||
if (e.target === this) {
|
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();
|
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";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal with escape key
|
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
if (e.key === 'Escape') {
|
// Load VAs for tip selection
|
||||||
closeLogoutModal();
|
async function loadVAs() {
|
||||||
closeTipModal();
|
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.getElementById('logoutModal').addEventListener('click', function(e) {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
if (e.target === this) {
|
// Load VAs for tip selection
|
||||||
closeLogoutModal();
|
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();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in New Issue