From e09e0aea1d30985aa002021e635b8a53c9a00840 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Tue, 2 Dec 2025 16:52:17 +0530 Subject: [PATCH 1/5] profile data fetch error --- src/controllers/va/vaProfile/vaProfile.controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/va/vaProfile/vaProfile.controller.js b/src/controllers/va/vaProfile/vaProfile.controller.js index 6417dce..346be7e 100644 --- a/src/controllers/va/vaProfile/vaProfile.controller.js +++ b/src/controllers/va/vaProfile/vaProfile.controller.js @@ -21,12 +21,16 @@ async function va_profile_callback(req, res) { return res.status(400).json({ success: false, message: "Invalid token payload" }); } + console.log(decoded.email , "Line 24") + + const vaUser = await VAModel.findOne({ "userDetails.email": decoded.email }) .select("-userDetails.password") // hide password .populate("clientIds", "userDetails.email userDetails.name") // optional populate .populate("projectIds", "name totalLoggedHours") // optional populate .lean(); + console.log("Line 33" , vaUser) if (!vaUser) { return res.status(404).json({ success: false, message: "VA not found" }); } From 5d952b04cf0a916c8dc67de27909be24402c1135 Mon Sep 17 00:00:00 2001 From: JaiPandey425 Date: Wed, 17 Dec 2025 13:18:53 +0530 Subject: [PATCH 2/5] fixed the layout and file based ejs routing --- package-lock.json | 17 +- package.json | 2 + public/css/dashboard.css | 285 +++++++- public/images/favicon.ico | Bin 0 -> 445405 bytes public/images/favicon.svg | 1 + public/js/common/auth.js | 29 + public/js/common/header.js | 29 + public/js/common/logout.js | 23 + public/js/common/sidebar.js | 90 +++ public/js/common/theme.js | 24 + .../js/pages/dashboard-virtual-assistants.js | 263 ++++++++ public/js/pages/dashboard.js | 142 ++++ public/js/pages/manage-tips.js | 248 +++++++ public/js/pages/profile.js | 195 ++++++ public/js/pages/projects.js | 240 +++++++ .../clientDashboardProjects.controller.js | 4 +- .../va/vaAuth/vaLogin.controller.js | 82 +-- .../va/vaLeaves/vaLeavesApply.controller.js | 203 +++--- .../vaManageTimesheetAdjustment.controller.js | 4 +- .../vaTimesheetAdjustment.controller.js | 6 +- .../va/vaTips/vaTipsManage.controller.js | 143 ++++ src/models/vaModel.js | 109 +++- src/routes/v1/client.routes.js | 8 +- src/server.js | 108 ++- views/{ => client}/client_login.ejs | 0 views/client/dashboard-dashboard.ejs | 613 ++--------------- views/client/dashboard-manage-va-tips.ejs | 399 ++---------- views/client/dashboard-profile.ejs | 521 ++------------- views/client/dashboard-projects.ejs | 532 +-------------- views/client/dashboard-virtual-assistants.ejs | 615 ++---------------- views/layouts/auth.ejs | 17 + views/layouts/dashboard.ejs | 52 ++ views/partials/header.ejs | 64 ++ views/partials/logout-modal.ejs | 21 + views/partials/password-modal.ejs | 33 + views/partials/projects-modal.ejs | 39 ++ views/partials/sidebar.ejs | 46 ++ views/partials/tip-modal.ejs | 36 + views/partials/va-modal.ejs | 26 + views/partials/virtual-assistants-modal.ejs | 49 ++ views/va_live_tracker.ejs | 20 +- 41 files changed, 2693 insertions(+), 2645 deletions(-) create mode 100644 public/images/favicon.ico create mode 100644 public/images/favicon.svg create mode 100644 public/js/common/auth.js create mode 100644 public/js/common/header.js create mode 100644 public/js/common/logout.js create mode 100644 public/js/common/sidebar.js create mode 100644 public/js/common/theme.js create mode 100644 public/js/pages/dashboard-virtual-assistants.js create mode 100644 public/js/pages/dashboard.js create mode 100644 public/js/pages/manage-tips.js create mode 100644 public/js/pages/profile.js create mode 100644 public/js/pages/projects.js create mode 100644 src/controllers/va/vaTips/vaTipsManage.controller.js rename views/{ => client}/client_login.ejs (100%) create mode 100644 views/layouts/auth.ejs create mode 100644 views/layouts/dashboard.ejs create mode 100644 views/partials/header.ejs create mode 100644 views/partials/logout-modal.ejs create mode 100644 views/partials/password-modal.ejs create mode 100644 views/partials/projects-modal.ejs create mode 100644 views/partials/sidebar.ejs create mode 100644 views/partials/tip-modal.ejs create mode 100644 views/partials/va-modal.ejs create mode 100644 views/partials/virtual-assistants-modal.ejs diff --git a/package-lock.json b/package-lock.json index ea79ccf..f47863f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "dotenv": "^16.4.7", "ejs": "^3.1.10", "express": "^4.19.2", + "express-ejs-layouts": "^2.5.1", "express-minify": "^1.0.0", "helmet": "^8.1.0", "html-minifier": "^4.0.0", @@ -148,6 +149,7 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -2359,6 +2361,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2903,6 +2906,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3740,7 +3744,8 @@ "version": "0.0.1452169", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/dom-serializer": { "version": "0.1.1", @@ -4231,6 +4236,11 @@ "node": ">= 0.10.0" } }, + "node_modules/express-ejs-layouts": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz", + "integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA==" + }, "node_modules/express-minify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/express-minify/-/express-minify-1.0.0.tgz", @@ -7986,6 +7996,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8369,6 +8380,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -9077,6 +9089,7 @@ "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -9124,6 +9137,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -9289,6 +9303,7 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", diff --git a/package.json b/package.json index 846e8c2..acfe04f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "nodemon --inspect src/server.js", "test": "echo \"Error: no test specified\" && exit 1", + "deploy": "curl -X POST http://35.162.222.18:9000/hooks/deploy-va-middleware", "build": "npx webpack --mode production", "dev": "npx webpack --mode development" }, @@ -24,6 +25,7 @@ "dotenv": "^16.4.7", "ejs": "^3.1.10", "express": "^4.19.2", + "express-ejs-layouts": "^2.5.1", "express-minify": "^1.0.0", "helmet": "^8.1.0", "html-minifier": "^4.0.0", diff --git a/public/css/dashboard.css b/public/css/dashboard.css index 62e108d..95139cc 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -15,7 +15,7 @@ --color-gray-700: #374151; --color-gray-800: #1f2937; --color-gray-900: #111827; - + --bg-primary: #ffffff; --bg-secondary: #f8fafc; --bg-card: #ffffff; @@ -213,6 +213,7 @@ body { transform: translateX(-100%); transition: .3s ease; } + .dashboard-sidebar.active { transform: translateX(0); } @@ -260,6 +261,54 @@ body { color: var(--text-primary); } +/* ===== FIX DUPLICATE SELECT ICON ISSUE ===== */ +select, +.select-field { + appearance: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; + + background-image: none !important; + background-repeat: no-repeat !important; + background-position: unset !important; +} + +/* Custom select wrapper */ +.select-wrapper { + position: relative; + width: 100%; +} + +.void-btn-a { + background: tomato; + text-decoration: none; + color: #ffffff !important; + border: none; + padding: 0.5rem 1rem; + border-radius: 5px; + font-weight: bold; + text-decoration: none; + cursor: pointer; +} + +/* Add spacing for icon */ +.select-wrapper select { + width: 100%; + padding-right: 44px; +} + +/* Single dropdown icon */ +.select-icon { + position: absolute; + right: 14px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: #aaa; + font-size: 14px; +} + + [data-theme="dark"] .sidebar-link:hover { background-color: var(--color-gray-800); } @@ -699,6 +748,7 @@ body { opacity: 0; transform: scale(0.9) translateY(-20px); } + to { opacity: 1; transform: scale(1) translateY(0); @@ -944,17 +994,17 @@ tr[data-vaid]:hover { align-items: flex-start; gap: 0.5rem; } - + .project-time { align-self: flex-start; } - + .detail-item { flex-direction: column; align-items: flex-start; gap: 0.25rem; } - + .detail-item span { text-align: left; } @@ -1215,9 +1265,17 @@ tr[data-vaid]:hover { } @keyframes pulse { - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } + 0% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } + + 100% { + opacity: 1; + } } .no-vas-message { @@ -1615,11 +1673,11 @@ tr[data-vaid]:hover { .form-grid { grid-template-columns: 1fr; } - + .modal-actions { flex-direction: column; } - + .logout-actions { flex-direction: column; } @@ -1627,41 +1685,49 @@ tr[data-vaid]:hover { /* Additional Dark Mode Styles for Elements Not Covered */ [data-theme="dark"] { + /* Ensure all text colors are properly set */ - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { color: var(--text-primary); } - - p, span, div { + + p, + span, + div { color: var(--text-primary); } - + /* Form elements */ input::placeholder, textarea::placeholder { color: var(--text-muted); } - + /* Selection color */ ::selection { background: rgba(59, 130, 246, 0.3); color: var(--text-primary); } - + /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; } - + ::-webkit-scrollbar-track { background: var(--bg-secondary); } - + ::-webkit-scrollbar-thumb { background: var(--color-gray-600); border-radius: 4px; } - + ::-webkit-scrollbar-thumb:hover { background: var(--color-gray-500); } @@ -1688,4 +1754,187 @@ tr[data-vaid]:hover { [data-theme="dark"] .loading { opacity: 0.5; +} + +.va-action { + text-decoration: none; + color: #fff; + background-color: #2563eb; + padding: 0.5rem 1rem; + text-align: center; + border-radius: 5px; + transform: scale(0.96); + transition: all 0.2s ease-in-out; + cursor: pointer; +} + +.va-info { + cursor: pointer; +} + +.va-action:hover { + transform: scale(1); +} + + +/* ================================ + PROFILE DROPDOWN (GLASS STYLE) +================================ */ +.header-profile { + position: relative; + display: flex; + align-items: center; +} + +.profile-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + + padding: 0.70rem 0.75rem; + + border-radius: 999px; + + cursor: pointer; + + /* NEW */ + border: 1px solid var(--border-color); + background: linear-gradient(135deg, rgba(255, 0, 150, 0.3), rgba(0, 204, 255, 0.3)); + + transition: + background-color 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +/* Hover effect */ +.profile-trigger:hover { + background-color: rgba(0, 0, 0, 0.06); + border-color: var(--primary-color); +} + +/* Active (clicked/open) state */ +.profile-trigger.active { + background-color: rgba(0, 123, 255, 0.1); + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); +} + + +[data-theme="dark"] .profile-trigger:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +/* Avatar */ +.profile-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + object-fit: cover; + border: 2px solid #3b82f6; +} + +/* Name */ +.profile-name { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color, #111827); +} + +[data-theme="dark"] .profile-name { + color: #e5e7eb; +} + +.profile-trigger i { + font-size: 0.7rem; + opacity: 0.7; +} + +/* ================================ + DROPDOWN MENU +================================ */ +.profile-menu { + position: absolute; + top: calc(100% + 0.45rem); + right: 0; + width: 240px; + border-radius: 12px; + backdrop-filter: blur(16px); + background: rgba(255, 255, 255, 0.85); + border: 1px solid var(--border-color); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + display: none; + flex-direction: column; + overflow: hidden; + z-index: 50; +} + +[data-theme="dark"] .profile-menu { + background: rgba(30, 30, 30, 0.9); +} + +/* Show menu on hover */ +.profile-menu { + display: none; +} + +.profile-menu.active { + display: flex; +} + + +/* Menu items */ +.profile-menu a, +.logout-btn { + padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.875rem; + color: var(--text-color, #111827); + background: none; + border: none; + cursor: pointer; + text-decoration: none; + transition: background-color 0.15s ease; +} + +.profile-menu a:hover, +.logout-btn:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .profile-menu a, +[data-theme="dark"] .logout-btn { + color: #e5e7eb; +} + +[data-theme="dark"] .profile-menu a:hover, +[data-theme="dark"] .logout-btn:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +/* Divider */ +.menu-divider { + height: 1px; + background: var(--border-color); + margin: 0.25rem 0; +} + +/* Logout */ +.logout-btn { + color: #dc2626; +} + +.logout-btn i { + color: #dc2626; +} + +/* ================================ + RESPONSIVE +================================ */ +@media (max-width: 768px) { + .profile-name { + display: none; + } } \ No newline at end of file diff --git a/public/images/favicon.ico b/public/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..582df3a22865c7b880075cf24dc9f5133a280bc3 GIT binary patch literal 445405 zcmeEv2Vhs#_5U|}u~nR{sI_%ht!@AP*`Z>s){R!#WN(&x@4c;i)K;yl?gb83!WIP) zML<~rLPGXVz8UBL`JDH?;qfKo8^4L4!Ql5A&pk zdOS~hJf8jbbI$LH@_3qXZP+m9{B=Bc*8q>__~U)&hkHE79qjR(a*A_)#AzPS%F{ib z*jV5F$MO5SJf2%_@!gLM^1OOrkcS(;cf_zz&(gz!o6oo2?eSC(5BD5_EBF+iaGt}? za|+7gza<5QOLstmCs-zUJPJGx_#2)u8I9i&Qlt7S@OTs;(G$|M!uBJ}vY8z$sqD6B5+2`oJgx21H?_Pr6J~?i@w6?u<%3yx~ z>BEoe@Beqr^>RT-h&(#nBU?U+mTmKgO5u{Daj#yA@=7EY^z{Ed%Rl{r2le;-Q&mxc za)!wHxL&X|ulf4ox8Kpay6IrPKmCD+nxEsr>sMVXcb{>NG`X(cc>IYn##MIK!Tf&4 z<4?Mt-1+dCgha(tk{|sNf&WpSAbIn;8>FhdT=Lhgm;AMBrEt?G$zQ)g7QFt3Tz~ut zt0Nw?NC&=b!VCAHx)qa7@1FiaL?U$y<~&@)P&e1H9(X&mH+5%SV2r-b~+Xs5~)p zw7h%ME%MIIx5(ooM@i<;{q=kNPDks)=gI$LAX^_iaKF?y8ZEY6HXCsG`E$=nd~`q6 ziyip}%QQYLT)u;TZ*MEe&6P<%J6PM5K)yklBcA8I`r7taH#Mm)^44yZ^3DGdZ(YF- zx+#BSj>JVofhIxi(wuGDt-t;K_ILY^h@ovuek1uGL2oV_F2s*}`{2I&+tfp(J$Z%v zG4ttX-9K$UE}TDD3P1PAw(JNghe|6%bL&A(kS zdyc$t&5iQf)z?Vw%t2DNAVdmh;~Ps(K>KyKcx$&cKYPz#PTRQ+bbTux?;Z!p`Q{t_ zO-bHXeV%O^?SQSHMN0Lu2q~T$E=9A@woUg);j%NE-~Vh}g4RE&6@6>ozj66;{Y_O_ znb!Fnyqhw${F(kD>!YM%iAVB)r~FUw{_hf+-~YtearzwPyCwW=fB)CbzbVbjldS)~ zA`=feM3SPzD?=`vK24!?bwqBKRGK_?DUS-E_{=fCF3KbRA05H zANYRq|F(@AT6Ju!>yY2XpisGM_;C5>#h1v#=UuoR&c9F| zx!@vs_?+|P+M|wDIia4S4sWmA?r$UqMJO)G@2vsr$oIBIOZPkNam)IEc=5M~y84`U z*f78;0QL*H-NFu_-)3plI*s}gZYb-7L_l|!o3>Ca^Z@l2_tXjO>zFch*Y~Ms+1^vX zvCo+nGf-0d4b(Ci`AA3w>cH{WyD zdFQwArR|q!ZPcT!c3boRyZKhRNXrnsYh{2fMOt6~o0D80a_pL>rbaO)PCYL1Dxnem z3Enf?>(}0(jo z$u{&o@@7X#!HUt+0Ddsr_jBi}zb7t08nCZ(+0PC!KiB;`H{UGh;yTMlnY`!35UKq( zQk?Iz4Cw!W7DaO$ekjj>r}_6eD_5$0s(1~62KxPj4gvNIGl_im#MA0SBwtW2uRA1K zYM1wy##Lcb^EJMog+3AZpcs(*nMW#$<~DzSE$n*wyE^lIzU+V3TqoB-AFw~I@^R)s zS^lp<^7V5AWXr;nB!Ah-lK18RlhT~Wlot*8zMS^4_W#^IMt_>S&;9G~mVK6E#e(@V zAu?LuNk8F8z?L63@s=Zl*f#)m277N@d!y2fejxXET08dtzP{M+&zl#>cNMooKYr-%Z}FD;6H_O66}os z7`u}C#viB$d5zD|UrM|g9kVO-Hs3dUw%Kh%Fm!6L^UV(OnE8g?cYlAWH{;mP9kAO^ z>8IdWQ7oX@Pk}LT|E)LU(3b6|Ekl<6Jwt!T-vl3tpIyuLo51^B(GL5+zpKs@P(DHu zeD7{>u7yjg-s>6MwpGVuj2E#lodA1>W3Gf)0Q+}|YO}!Z!!vwGYAa)6ZN0zS?!iW4 z8R^SoADXsXQc#ro5L5dPk_jP>pO5}UKGR#qI#Plov@9$aZOmIvJzYM2^buLJc(D}b z=1N6LiBy!9=<%w>i{yRWyYq}QF|HS;b}8$T>Z3=u;kFfCr4Ny~B(9%~n;@HhaQtb3 zbS_G_=E}<$KOPh1&b+SbiEa>wa^ zbNXYDk6kHC$9@w5-RH!$`7Qoijy@DA#N%1{pYZ1d(i zzUc&COao*r5S|G18TwAHKG7L7FxYdIK#Ss(-@$ij&a<-RQ^eN5Z%Nn!-(~r>_Z92v z!*i9HKXzy$vUd45J{ojho(&JuKTMwj$CtP%`SobWKL)2>40UW?aD@7uOW^+{&Tg-qZQZ9mN&DurXIqX@`vI|Em!EK=j1G^K31KlZCL~7U!~4m) zxB5%vS7F=v2YghV6(xljkI2V(%+?tLBzOLyQoQ9AN0S;x(i$Pw@TsR&<~UwUU20^p zt+KXskH4qhobS_{zSrxnwz>+jl=~{4C?m9&$!oNke|)PS$`70y{%_ugOK~=I5`cBd zLrhym$@Vc7^ZqZUP1Elt(ht}L<)8Q8onyF`e>PopT@v&YWt|X9oe>-%>)!1r6<@Yg zelsQ&33`O9UMgI3gDH0N{=4if#S8llE#WoLYX<)nVCjs{ePfH=SAKr$A;f%f z{EsrCdKPxZ7?l69;r*q4`4DMX6)v^kL`of?a4z@-W5XPuEr32PLD@H9-a$=mkz-F$ zhJA7{`;Au>Hz|<6_-^7KgR2i8d{7>`_+q*I$X{!JAR&nTf^fO#q=7Q?)}fMh_Yj$N z`w-bM|8J7NYK(08=3?2j;yfu>b&b?|a~&HGhK12VeDB-0I&lWnf8E9Z&5e(#2=;H@ zd$+Is4@UncRB1t(J`b>d`SeymIC=TXYgG5KU#NC4%1-%Mv%rb}W*^4g_VM?)qV?eMtbf0#F)B&W-GP*` zt!Fw?e)0mhSuegMXQ8~L0p*YVhH)rMNr5vzh~t<3yyvdZ)FY1W@YrT+b?8X>-DSA{ z@8>Gda~zmD_7k+H+)N$y#Qn#|_@6!Xl=daN0{5&_N6N2U;EE~v;=>Q*y~iJy4a=8l zNwtVB`FAwWQ7;wo}wQ+*pI_|9q;-d}>hF#v685`L*EqlZ)8!D~>&0=f5$2 znr$t8q`PWw`qRpkfBt`+e!KZ@AbaI+Jm_w(+`oJOv4>?;ScJ9>>6A49%g-`&Mfv#- zmY@3L%MU(qe|ndXi??i+j~;qRG7ddV+iCSpcTsu$aqpBrRAWE?ot`1{-g`%~UV2&P zK)(k9vtNEivj6>(Ouzpjc`7AcZus5*lUU%FK5E(&sc0K$YqO2h*aXzWzYLu@H}6aj ziO}|wbYSdepum0)%gR0dyaf7TXj8Dv<^Kr+6MRl z+4ciiRswAy#(oe;S2yt6-C6#&>SphCAKzHqTHIP5=-oWv9}i^DcP|DQr*#V87J!M< z@|=bfzs<@~@0KG=A4?z01C|GVYCN!~_S@J$O@R-O`<7_R7(xSlW>3Qd{R#<)pG@?N zq1+x%>D1Zaz{DlCgsZ-IEinW>7Ubqi45sg5$2d~rgnlG~8QV+8cw1krr@PF_@KYrb zP%rv)?r~g@wC8Wg`^MMheqUcWZkBJskeT(!?`!%UDkOSH~ll z)blB+vgYgd^8ole&bMG5`cNHb3Ay;Ft#E5zSVEd8)78al2fEJYnRAWcB&>7CxZchsQ0 zFedV$DZY;9F{Y38c8|k0Kj1pxShwN`cw*cl#{tcjw_$_a_UF@-7m}ern)OSM%7mk% z(}IJQ-^cyxS5mZTvlf|Se;O0(s)MOZYkStV7C0a@$1?*LZalm+^$O*$Sn@)T*4-Iv zVEe=vhLm3&h1?=eE`!>JMyai5tgu7#mgvs9Zl|t;5ssaj@lzAO!9DZt)iXfXx4r%F z!#egx{mOBs9@B}Tu;oyOaeEJyqTGBfof)?_Wi@*@F3d6Gb#<&84|!Qn;0y@BxF*Mt zef;Ad<#ZwJ{k!i|9B{rg?S!7-h4H~o4B;}wlyUQd?sa~#<9Y*sOr4b%lrNk+loQ4U zn@z{gc^}tQe%$rvL%3(QXA)9WC#FFs(q=SccDsWg82>`Pe>QWXDUO!cRh8!JJTUcd zZ1e(0ILEb$!cU~|+i{Zj)!9<=0U?Jw00TeWWF0VhY&&g}@lX}-G|bF?nTIC!A>Y679N|1*pWVEt1u&Q3e$ zp0hd@ckl4OQ~$y>S2;O|vUiLFXHJ>|-V)&V0b@(O+wC zGyLiQT;ax>WGi%@fBlQ*Vm=J?;ra!~z!u0?+!23#56Rz%`6q@KO#Nva@qT$>Df-d< zHO4Qlo9a&-&{q-PZ;(`!J9^*0Zdy>+#oWzX5KD$UHVCrsam@(cFtuh3>-yZKIXf2sb?nY+-^JQBS&FGPyxK}P1Voj|Ty z$V<+web~vHmI9lYbtiPS|BU_n8&3Fbt7YJgtByEI`vsY>lM>l30qEO| zhu?b?^z(Ze1EppK=9%n7f8U%k(BHue+C{XHx${G%DmzpPu$IEsHFr4rpoJ35{O`+7 z{}{2RY^%D1{!aaW*TUDGOJxOHWjy*tq_z4xQ4S+diRvE-llLYLkgDZ@)Sq*sBBgA0 zjO0UCYy~gmFa3wMCoL6V;oEO1f06&qH@kY@Uj1t;DIW7ebwk-&9=1MM4X}0_PaWMOpSA6pe0?%F6QT()5f0pUV z@kttE&s}%I*{7m-4^n@Z$I%*{VwBAdy4<+hXmq&7Ky z-kc|q1V{^kKE~L`DCmKw&Ke*U**}-Mm5>F@fvQ?I5ObkoRPS$u-ly*0ig{4%8x`?< zdZ=t!cZ@vo@Qgq6qk-|Mc{`e*oLpJOQ9qyJi$|9aHjY;#^kp0lZ!$wQ;Y z%lLu&X?u_o79?@N!=;dgYlcP3>k0eGM_2AG?@!)G-kvl>*37;{3N~CNxvQtjrqz>W z)0#BoV7Ngli|0FbB>C4xEO0&)bLi1FST!SCe?u;! zpM>@vD@uB+uMBl}*WbK*-IvQ1hirG~3u><(yL!K)^=J8Lqc^Z0gYDHj?ov6(gkIzr zXe@l^oRh zTfgf_{aG$CFSRb zs_ZlGkSS+(?K@L{$c5As+w$47B>`)^IeDU;y3^OWJL+$6%lb1HR99@|W`eP2wT;9Dqugy}zxv5i>=Il#OL>vr#3LJ}K{ky}@H~nbZXt(_K|J=pw(&Lhn0$G9l|Ia5Rg4g%adeb(X z3|c3{pJ;f!yX))!e%4?6)UYwHJZPA_e%Bos|GY!~cgwBv#xB6?x7;SL-*%_GdE>3} z+LSBg!HX`HsmGrr2^d>ooD=y>ZDjOkD0``}?a1e>KY8ECqklQO>s-fv@NLc~kAtnn zHR?F;n7U|Jf%Rlxh+yU*Q@+_3rq7tNZ9wB2aL)Sej&Xi}JSzPS9T1Dg7+xJ`06(-n zXL;xQ%sJ0EHvT>i`?wJL2a)FpexW41mtgEU(%FEaJ8?q)o_`s8;@p&NSN8tp?Jafk zFPA;<9lU1gVCi6a!193Q0m}oH2fBs_Ja+p@V}N-gVgYjiCQrmIkK@E|v+~w8@@Da9 z@o0I#@_^+5%LA4NEDu;7usmRS!193Q0m}nD!2=v~^M{@&$DZ;{j)O85%RuY(a&myB zVLpfge#gBT%VgplF;$EoV=gkr zOER8}^UJtrE;-J-A*3h-NhbI{198Nho0vj+_;ilt`!?|I(#zyi=7Zt9GsXs+SU}E` zPk~&d4%l0g2ktE?*ryEKM@AzT0Asfq`^Q*P0_meMajy3oI@o>7bYPB$SReivhdFl8 zAh~h)De}TqS4lS3vRO5MzHD5zQVKV1LQXaZ3O8?(9OR1me%?G?=jM5=)qU0QzsRM7 z2RFxaQbw3V#A$oGKc7%Se5@lmOsd?g_vR^$O>1U5b1oH(h+G%zyWNsVXks zF~DJ6vZEetX5&{3Qx`4f{7#`;pPPYRFG9GK+w zgaZze*KfT=ionnA&CCgNUu*B!KknXJRw4^td|9qP{ZvQ&6x9oPc^@RZ=Mq8?{sF<{Dz^ zTu0IY$C~@i_wvolzmw}u`hVI^bG@_-?Pu&M?X!b_<^xCp{->fJ#Q7z|qhn;*2OlX0 z$d8)ykhunZxZ=Ju8?}weH_Lq;fOt2+Tvm0|O*%`<`O*^!DXI%OcVldz=6`*hTiZP%^$->0sp&QC)>mh+%4h#4RozF4AlWqvp2Ht|9)*C0Qxmvg{f zG|-3I__M=YN+R4RGTc$W@LlG8eAeKed*>^(6j zt1IZPZEYCsYx;Xb% z2+V&&-`PmyP_HZYts_rF5f`8VbD_7j8Jsuo@!JMqmFN&c!UCGY#I zq$=+}z;~{`mN=|MZl*@&^Yr0g+jzx&%iq>-;pI(GZq!n>$nBMWz%Z>xI&8OC_`PGH zPrD0uoqaETXIy)j_FF1+C;RxHJo%*J!;5|uYfa80w2T3LRR1*NUHP55!7J5`RmkDY z+g|wIo{<%w{6cb(-<$dDnAdI-_TSDp3Hi_8)_0Tx^;Hd!6XF!_p&hF2b~_(u`O17U z^6iJ8stm`W4a-D7Jq2qrbQVwD^?T-i%M6W1A0|rk*JK=hoZ^9+rXD#fWi;-J9J6N`42T9o?pocnxqptid+av<{fi_w!#{V*qLxFzeq+cA~ zj9%LMOp{0ayP^4k77ljAKl4k`;%Sn+Rg+}HG~^9l7%W?}Ap^*vS}_;-rEy$<}h9-w<@)8sBaMb>MMf8?~zcJkRW$3KBNZ%Y9A zSs}6!&s40xM!0f}(jL65{y5)$hxhxHj5v1#Z|ALBE0;v}({YNflDTouVF@gF7?pi%LXQHuX+w%g!)w!@83LI&?<-yZ(8Pltz=&G<}x z6G>H z*O+Jx|7Zh>kZ-yiZNU1^4?`bdx#}&xw7!Y?g_V$?2ln_+H;~iJZkLB0_@A^#pPF*y zt*Md82Og%rEUuNqahvgsi9y|a0vOLkymQZ7vmD>%SjNJ4-a~0SwgDCJi}}{$B8o^0 zAjSc--#-1nY0)3Cu1(wg&-E?}=LXA<&_OvXCg9D)o=7#?a4&pqjrVLgF+e$$PA$^rciaadQfsr4Ggf)JG+SdQsAyPIcS}N!4BU|Qs z+m&V*GCJKgGKS@fBLF*{Fv;%7yT;jEd-ovDqjY ziT*U<5{xB`433g-pWR#PzTz4yt=m6E*;sQR8|}cX{<00WP#M~SVzkFOpM=WB=|g4x z^k3+jWShPnDdm-OwE&eMMO_2*6mrmWGv(r*`bdd`T|ILArVl$z*8$;rs4399tUGNH z!;?KB4=e+7>)rg9Q(0PPGnFvsu07Y_;?{`V?N|KmOgRO=rN4+{PmCkv_y@=6&7RlE zbIYIETS}J!|Ij~fozvnmaL;u<)K-EVl+48%P;)}06!udM&e=y;F>RPsY<^42QrA$4 z0y#3F7h3sqJ?hA^Bmw_AMxgBg@Q*$9a8Kc%v48iu*AH{)7XPXk(mXmgR!+z7xIQ%F zpvX^*yG;d{xfa|r{xbplZ=Tyn%9aL*e{CNO{-Lw5=0@?{5XsHLnk(}|q~gmUDS&QT z^YJj)Hd!bl$8sD!<6mC)`CX$&YacT$EZSN70yt-!dynED_+yOby=R`SMOB$~7rKSx zbxXfgducL$Hx=VF^xHAl_5|qFIMj*jxySn;7S|Fyk@5}nPkEsH@83RC9-z1K7hvt5 z1)=a4MTpD}mprVOxazAEl+cM!X|L>0PMM@_FxR3^MR_UvJ&u2rf%w1g%yR?A|A!CW ztG`Ld@6tk;E0Xhz!ZcRg#EY9gMl5j8`2Me-9Vq2r_0x4^TGToIIB(&c{o{gctVe_O z8Os29@L_M5{d@3!4tmGUs=ft|aW1`j^G({%VLUwX-xK=A|C{6iNB^9sg*5u5)k@&T z^OGm)ZzjN}o`^Qr;F|B|``q6k|HpwgEB>{&6n_ESwb}k|U+2i5MuosT*L|u$+gt$J zY+4X3g=h2hYD zC^cB4H4wh}&vny z;J<7E+T-_O3(Xr1-GeoR1CZW_(^HlIsefoM^c2qB-zEN4|DP4O{$W+G{p}x}{(LCr zw;&dMH0Bhk4FoyRd0V0VWnAz8r9mw4f8{}FJEt8WwM(J9v36Q9)(_(vW5i?Gya*|$ zU5d5Gw#^3}=3#xcEUd4`HUMKYMYvz2_hEzLS~=Gx!&-!^-s>mTdDGEHh6Tj&O86(- z{bA#WGO}^g$ct3}C!>ta8_l}+hW583`k&)r+=@5mAnyD??K`Bh9Y$Ta-po|!`zaVp z$V44k&*ZRR_?ocZC1~)-snFk9!=&cRy`^MvDEtiYcf+@HP1PBmD7y&8dG<{HMZR z%S3s}hkXtI4e0-tRXHQ9H5)g}J?CE_mtcIG{?qdS7XUbp!*z){M<~!+P9dvSvEq zlMwj<{-EXPf32P|MAm-(TPfW1EOdUQjxRJcV1x&bKI4ZlGGOpeerZ7ecj{q|50w75 ziG39Rx(<1DtzVwp3cQ>mc8lYgDUADw!q_I-gg8$O zXyDM}wqFmBr~bZ=JagVadFt%JSpRR3+;>KQS@Le8R9C)%wQir2!n_wGKmT8{E&q8b z$bUu(wmpNjkf+1Kd)+q!i!3jDx%I&70B zqJ1L&_Xht%NA!MPB!qo!-EPepG7ebI);x#zP6_*=${Rdb*K+wyll&~|PDWRQBF;BE(d z>X_x&Rs3tkcKU<3Lf^I7FFYf|fz$D@LAg$NB5b52v;|2X1MpXb2I<^vt_@24HSgd5 z)nAjBId(}rH`9OzKqpyR<4D`Uzxj=07opFRK2_@(54(r@$Mh3>3h(B-oy9+8fORGC znDa5py|ri+c_wn}1?Q_SnutCC`wOhU;Xm%VHZtw;So9+p^Z3PcFY50c{zmJH*VOs& zt*Qk6Kk9e#ep~#n`1i>Ki*nWBC#dcr?#II};P@-QZ}8AldzN8W>`z4!{t(MXPq5Ck zk2q(j$?M?%%W2az{$nEee{y)V<{(JKoQ``-5B#$nUp)JQzMA;wdv*Mf z$9SEiVHnNAm@p@fmwLV#@Eprv%ENJEZflo(h5icTD=}vb^`*`?dcUXeZoW(WccuPe zjg3xb-Pd94Wn@@C_=jK<0q7?(c{Ugmn-1J_42WZ(#DDf<&+=!waV`=^8roWS(LBq& z`L5ZMhTNW=JXK}T%oXbmeBWL8Coh`%v+jjCIWoTg5N&72h6FXoftz@6#XayJ5B&cf z_|JO&CH@Th9kI|DKUe(Yozyw)ZRWe&bNq|4P>)#L37A71kM^E&X6oHr_Fc{Y#J|zY z#8*RIjU@l<5VddO!r_ZX>;dsWzGqGo&fjGW9@pI@{#VSRKi|QB4aRtOz<)b5aO0mk zh}(w`Kdj>vlktt-I9Ir<@lQTA`q*&n6DjG6TlS9=!XjlH>`~62=bX6&*hI9&IZk%P zuwP0e`W5_LE$l`fk%tM|x#GT!P3ODfeuEhcL!WE>fy10WdN1*}`O6*g-syE=Y6{Frm^&sIsZlTJ((OTy^9WAjemcf(O;y{jxHhBGH$;uUC(yGv@P1s)3-o6;Xm76 z)9-%gw!7ps$W=m6kdCvH=Zu}@&wo9A-re~3r$=Yb-L@5NEb1ss&`~{Asjarif)75_ zx!9~%I{J6aKbMNWcQT--aoy7Q`x^e;HYRCViwPbz&|BQrfBBXCH6l{uY%hb}N#mFh zb0`4!gqG^x)7SbE{;7Wq{wWJOA6_vpvh}+)a?w8fs!#QD^f46g;QzjW|E{z@edy?x zT^;<%oP*DKumlq=xqitK8Hf3@%t@4lafVdbn*`=tq`%F?8uv7>xAgr!#s9#)7y5>C zz#WYvviOZR&R;IORkq9~|>kW}>Z~7!if| zQ-|+$PN$CsZh2@){(It@kx!P5Wo19~p);NM6Vre3$46J4^ZjmME+6$(qCM4d1L}74 z8JG{n#9Ekdmu<_Hr>17AFNwOId2$*1NF4T5{PM2Q&maG+%e_ASH{*4t&RxBCDzC~{TkmAsXoF4_;S+WFJ=D8iO@OpW%ot=8+(lUhd}-F#JMAO zCwjMk3p`(2R3w`*cWLR2nexs(_sHFUJ6lqbo0z^i&h1EG3_EZ=0b|8!h@B$uGp~th zV=2$Q)xXe^e$p{ll$pAp@tQ3A#QhJF>;HC!Odfu!TzlGZx$czHx&^L2?XPnE@H6Gc zKc6nw|L$bD`j`_W<$#}K>^)3vNaCCJ82zL1z1BX1=2F0M3!^DlqS({!YeNV;mr!$B2*P zT!>8AF^m~y85kd-0N)Mq+Y*p>+y_D&oyucAM+ zM|X2S8T`RH%x+*Fg-q}S_nhb4=jS$~4tp~1a^5*}F_;_;Jm%VKq^+TIYkRH(U~-t4 zJ?U*|Px@0w^p(54f~cDIr*6PIHZNkM)&6f3CggruFt?=Q?Gdkbpdy;kU$s?RaE3Xao| z&ngYiCu99)=2>D+lr-eDO2PY?|HtS6+JV#omi|4DgY@YB$XSE6wL#a^$bMMQ4ab^0 zDg2-m4QAmhxT2!2Y&J2J3F zlbhD$1A_aSJ?Dcd6yB&F@xTc>whZv8!#viZ80-XFx zA<*riG8wc_1Kl$r_Zi_>#~u7&>DxPTz%_F-DErX=iICSg(2}{_xaO%@_mt~wrVZLh z()Kw}()K<;Asxp_L-v)6q3@Yv?rZ?_z%lpRNC5eYxw17M8QK6#|J_UfMA}zA8;f?g z0qR||zIh5{m3f{i%V+su^nUxu)x%GhXD3aO8IM0HOF#NZ)+}5gx!-*&+t#d+{8g(I zwyj<*xhq%7>LrWiE3EPJ>7$RyvlFMt6{nvr7YrSuYgGOP?>ra4T!37ciGC%ni)eVk zv;iy|%i3Gkk2K}$%lmZ5Ws0v|XYN7f8Jz%nr@AY1x$ef2CmCR#~M^4rd)d}geEzmBch4({VOxPN*Bc~v5 zqNRV=@O}bqaM)f`fM2%nY}eUNXCC)!nfB!4Qd3goCYi2Dy78A> zcZsl5-fOJ!uy*Dgd3?kraxTjBSCohRcscSPPl-f7BPO~}=6>yMx{B>;JoJA?WdCM8 zpFDIwnf~NIvA9pAl2&PheE5~1c@^@rGiST5F^Bb+nvlDFS2w ztR>|2ts!ULyK2!Fa?5$=Yx_gHk$f-?Ia|5bN^iqwUnmc0PB3=fIJEc7`%IaCG$CHR z1ixMxbS za`t?=@|feC^<7bqOxOkVE0e$bf)3t2W!z4G=5yy-h*QzlC&8X#fA05y|J{6RGg)i6 zt`=mHa!6S>bT|9v2fOHi999d8a*b<(Ic}!Aj~BSs9qC_#b(PCu8`Po=(hY0d-al%r z)+Gh&7fgozYIT6SJuvi#OsKCtL?(qqssHUn(Eg*Roi%?+hicHC>#9-Tax*k{@AXC( z{vj2sK=*1F)Zo~k{te*8MzjTdz7p+HC3uu;<(B)(^7^DJw0}U`Fva>0UHZ>Qf3%HB zTn80E9Q`#qT4uiVl46hREmcCcDuCY_*mpI$ch+KZ)1JrOv2l%cXq)ZelX-t0s0F=^ zeOO&rt84XEz(zFo;j|~7l>bBBXcP3MwOG2d9pLyZeY+e}NyXY0T=)BQK-SaGDJh9} z+I3#4e2}*Rc6SXRjqw7X(Uw{+7_Z@z8+pBJWeK-EBXQ`8|Ky^SsVH&$bD4 z*Z0-2-&ilk3z_ww7nT0(AJRWeTY&KeKgoU~ZK4R!9yZ@ZjL|2ezsxnM|8@5RN&&?) zWZ$e0-%P7E4&1!Z)^R;-M=yA&fdKk6q8|a~QX9Vsx*L~uZN1t$T;~aBOMWnR08J1D zT*DoJuf1=jY+SF;Yzv?H7uInK(y^sL5q7r|XF50JpYi?!ISaoqn8}Cg^HtX=-ZjtDCCP#?*o@ zNOyeLM}PjFwg)^74((Mn1V(@5AL~UD&zDhAvR}1{(h`#qq7v6m6Yjx+QaqGwYIalX(Jo{);1ZKKap`qq} zd$*D{2K)<@aepA~J3xQ^J?cT9RH?7@|Bg#he^x5iSTb>hq!aZ3V@>z;@xw&g!SIuF zeEmG=z)dTbD`qOIs~x$6tw;ZT1?;Lox1H$ZPdA)Xrt2H)(1z18qk#K1KR%uD{E7s}Hb~^p{$+Yxn|QEv4($%ea1nbX~zr&^`_Ja4hV>aTx2| zlju)APKF>p{_&5%1Y1?;UdTK5#Elzp8qn4f{n>HwHkP9wUy1P{ z>R`UALMn>hlTBZo2cPmF+4^auZ234^a;Hbg=Fj>|&VtjVEdNzJSBP>nNICeWgnf?& z=q8^%pf*2BtGa>L0^Ptz^bwT$d_?n}eOcQ9#;2!gd>O{JL+LBtgZzY`HFryVI@z3q{03p`Tt zMX2P=@<`F55P(O%e%B+*J~~YDf4CmcZ__eWfc~{C9}NW1khUQE10AM6-bkHN4lrEv z=-KBh4=}!LD%!+U;3kPa5#Vf30ORwX7#0J2H(Fz}|BO1`cJ75F2-Y%(^$o#ngdDl$ z-W=;g0sLuE{y`<$`Lykb`O4hKWYx5NB>yvy)O;B#<%@$QH#}l647fgj4qp5hZy`lC+quxnCKzf|xw*A2h^ zlv9-U{8I^esDNy-FJ@%ky>EW7odbXQCw9GX>o7)uHeJq&8|BCMAmfWYvUx#>Y@Hh^ z1#_aL0(euymX_{OHqlDp&>S^Kd^@)v|jVOF>lWQR);XkNrU^uO+LUO+y; zwXIp9vVMAy6nt?8inmruL3*_xNiaXy>D%DxmNvuJyu1C_dG@&*Z`A%!N)Y^^O8>rR z0~%k3Blpg{Th7Bt2@TP?6!h_@9dd+tOG{cpPp7W6M1v+Y+|{5}UEK!RkbRJBgzn!u zH&jZpBYm_d{dWf5&&v)|yD&HFNb%Oq6Gj;_CWQTX9rN?;dEC;sTdItq`3`&fp0O2g z+#q9y48=G|aG&=7xc!-I``MSth{Rkv?9TxGU!HtTcElBwSsw(vTb&-Y@T+ARF}^|3xM~4pAcD!wt(Q(JuH__(O=80+Cb#- zq*V0@(MR4F^?$Sf61tx{AO-Q;Z1dA2BjD!^k`ZVNILD$pwwCBmv{JfiYrdD9IR{8h zHe`QZDD?ZzGQSi3@l4sAP$~F$7-EcP>R4_iObNEw^-ToWEWUk<-)^CjJIFom^PD=R zzFs>h^7X74h<^xz{U6-t=udjH&8Kgld!EzJHwlmw6r_FJ@%tZy?v(L@w}7C|Tw99uI!Ej1SUh^T zOMe)b!+t{*+Fp(mlE(hy^7PF#)H7}idb@h1Y|J`9N@fO2A?WHa>pPwM=wG!gR5pGT zCL0#B+&@Y+$WJmgK<;ay5j5(Dyg;7t2{*(UvBxd*gzpjasP5SpXvB|-5!=Y^gB$MD zn6dHD1DwB}1iw&hSS0KM%scDlJYZygJMGc`N4YsKpL3?(yz5@ABhl5FjkIR0x0gN{ z_NlNjHmeGFtOiyICrpax(bjKY{};^*mtyn-b7zLgx1SD? zqMUp173gVvj=WWa7uRWw0NVoCglG>MFlN{Q25D?WY4D6cv_$^~=#-k88Y%brgxAhq zC>LW4h;t2{oCENgz;>Dd=7S%s;k%DZ^5~_bFuxFUjS&w`Taa@h-2JNF*zU7TEzv&%e9O4U^uvB7wZ+a{ zJvN!0-J0OoS-WzzEMK%lzFo3ZzW;KWtXkp%oPWP?xh(ziTUov6P063RpHzMx0{tIA z`w#oD5O!i|c9>MnI7o_T50=gF8?K$_k#*B!Wc|F8Wn0c|vTfUYQjq_lloe&eulob4 zgfT+Z0#57*X{suLyH@C5M*T%(Kz~)h7tZa|JMPlIc&-^q_zzn9$AiBerN9VCZNK+!9~=oM-sp!CpL zxU>VXKBPUbbAZyT4n~m`AAhWKitQoxb=KY1B|LuQwY=4z~ zWq4M)osBYsv7vIt)M8sUeY#wF$f1a#^e8ZXh-1mLmFQb^1LF%AYs;2$oXgX^Eep8&sN%|eg-Fx?|x zd~}p-S$zlSU81~R;e{>G2!5c?KftYM%^JA?e4PRMXMpZ(KU38HM_Y<>_OVI-zL0y0 zHVkdQ$48IV+OrKrfMHc@{FlV>E`Eb848c%h8W!P65w3 z?i32#g~`?XM}z+RO5rZhe+NDR9~8kJESeuAMa}Id$4A$Fh~Jg^yd!uJ?EE?V^v z>WCN;#FRER!h1+v7U)(}jq#xUbgp|QVg#AbO!JO_=8O#@5A+@R4^!XIzY>zP_DX+t z$^zJ$F;-9@aR(i$J_Op68GuB{1aorH$Ef`z=y~RPHMxGd=e)}(qMg-=KK^LTR z9Ho!ZpFGO`ALnjA8|TD#Yx^G<`I~X|HtZqV!&iW&lYxt=Xxq~fLz)VRgKazx@(~X{ zV9rkN9pJqXAN^wx^Rb^4E=2#ogZh65`A2)eHbAdY7ho)@2<^ib=#{PW(XX5rA$8f& zBH)Kjh#&ZR?uDScGhdx^7dukzsi*u!`8`SNh}aPL1W5nBrvDlD$DCTv#ya$8+zf5u zz_)st3R{nD`Bczz3S>H2^E1LX#=bH5i#TCkMvgbRA>M<0jh-mE_8`RgeX^I7Ebfo? zJ3#*rY26a2?clN>zy}53scp0?VGmT!L_7iF2)4}$k*_~ELK-UPs(n|DCa)vazIDU} z%JZ=t3sBj2{6D6CwD%}qz1i;5vU$Q){-2LeR+2Ls$7k^d!~zw$>m+vQzG~+&KVJs( zG=kpD{blCL@Z29C#7zR|9}Asv_0J+?%cq0UcK5^hKIF6|x_0Io=v|Z*g+3$b4#-10 zu@%0Cjh`MMRa^fJ8y_(r6#fpd{r#gy={%8m=(aS(gwg&tK7c+!cf*^Vr2nSxzLqfq zhk%Z-^)W{@4!JtDUBUa=7b5Lj!$%_GYS}hibpYrxb6*|PFM!T2Zg2dj1H8a@Rm~hI z<+EdSTye|%5ZMGfupIW_2KbHh*FFZaQ}_d7_vVKl7&BV?&8eIxhA{xzxQ70{Ugiz$ zx$h+X3pcEl348Ca`ace>i-JiO}bhKwGwhBSHTOkPGH^-QE{wJLy0i;?Bn7`H9gHvh4Y2 zsao_i#D1aeMw?v-J5G;V&;`x$0krk>IoblQHedON=K!?_U{jz!$gxH0mqO@(ZPP<# z72E&%1sXfz^mqeT0#}@Nn%0^5Yjqp|d~f8wHGOC9_o_W;L;HVM+kYy=H?FxwbGXK{ z&xhQ4X^?s5WgSWV4x4Z?+XCPx9=37<rh+eJJRJKENFK z_2|z-EMPJ0wi4LzjQJ|biuOU2+VVxXUy3oK@&ys_`7m|_&%-CoWBQSGPg;NBku7tB zB_H?87lk2)I7HUJ6NQ+gOJVaDN*Tz`oJB3kKRLn;tS`5OlTOfB*#sT?A%Fk$>_6g# zSo4LJ z!-l?M|4>=;%7Id|bdZ#Se&q{8Apb$2H{!q-BGw17|7D2(f~&>lPq3 z9M9y<4wZcDi?U;&KM*&L_^=}M_X_bF`U9)7`%4AR=^x6)c;WJQ4nnT7cS%Yqui-p% z1P`?^CP=>k=LDGe1^oe9jEjaHr1n4a*3v^DQK6JxY(6tQXe+8H;R^X#^A+F_TV~zmE2CL>r$R^Ak z`xfItTh}pGWHWSEy;Ljl!32m8ans!#Yn+mw{Fh)p^hIHz;C0lG^oM@SfG+!~(VsN0 zhWr;56>H25&&xM&kyoy{Mv{&@M)N9N2wQ-0#T;W`9#-o5MA!n<5lJDi38Ii+AR-93 zjD`+y^vBhQhRf>{4wMy7A0v4moq+j2zm&Y0hhU5V`%ix*`7<#8@6&@N=fgu}*@r)u z#nXq$cb^?5-+eSpzMA$6Svu`dSvvg?S^CMr^5v%o%G?=0ldnG?E;-Atkm^#-X)Hrb z2;)+4h;hBfdd&F3cs11jPCQ`f&pjXKw&eBKG|x0`zpYn~lmHeGQ&_;X1={0P+v z3BJ5?8L$o0LXl$g$6foA%t(Kv7iF2Ec1d z@{45RL5HcWLi*Fs)hFq0_+vNGp9CmJ?Cln`wcNIS{iR&6?@&1(I)JjC4!xfQ8!?Wu z1ezzgfciBRwC8$I^e1!8FOFG#c+*|_E=Fa+d{f$*SWl>O=yl!z_|uS5MLS!8INEC9v9bcex!7L2`DXb8 z=tw(i5@bFZ@nGZ8N2EWEd1eie{wZi{xu#1z{DpBSGwsPwuD@O1!?%&%glYiil(ApO zxuuSchYN^9r%g|qYa8K5`_}Y7bH{RHUI=clPq|+G9E|s&?9<*GPrk*zPqqQwK!2aO z8L`uSCE~>DIpG`s8*9lSu*TM}htD7eHeWF8XZV7m21p9}g-Ib?-wpi)_y-ao_sl=4 zYx(l=FMz7I7uWBM7tVC38M;eDTLcc*3Z;|bEgjs^p^Z)ePu zv(N@{9V_>ER-csn-Q510ezd`{A;xv5o}oU&WS^bS`AA$>h-0agE$Uaw8S@qxoHJ&L z>wUd-+4cGwQs3#kcRl?%e@$f{pLjGON%Np;ytlz$U*C6w{#T;-{V{Ymdvp7ab1zU? zrXPJ2*7c01{`cY3jHi?ChW6A8v=``0diVDG^fj2#C0@p^W3EP5%0GFw!q@KC^B+LjN0|&+G8{oQ!W?hP5v==HFMQK1+8~{@p}> zfBo;97Ao1VzNT{p=;w)t?RbEe-sNbfX)81Db zAmGxp{9rxSDC0(dQX}%v)_Wa~-+wN@T65pW(JsOFQ-k5NGV;^c_q$2{4c*P2c&_v1 z*vnnETuxVCI>wIC4`5t4d0;$ZNVqn5GTMgp=zj3~2J1Qv*V}s%v}S|F@7Zl^IDiI)8tFF`d=~E?jJ*U_nvdd*e-Be z{_!-qV89S3jtsb-2!9Y`Y8Y3VMxMZ)YX^_Syo-&#eALb_K_$F{`MU|7+VDcYp&YcQ z_UCr*1tZkg&+#3ONwIyRKdR5u-RP|zlz--bDJv_NGWZ6$m3;TT+G)7hlrvlipJ? z&nq7KD4y**B4U}w z!9L|$SgB}}Z2R9TYIo@dX%`6nj7@I#fqcyP9Nc>^>8yIg`7q~yRrtnu_|BV#pQ(Ds ztbLWrxHZUn8rp$A?eFu~SG!661EEuQJ>#akIo7pF*$IVjpZS3*phuimNaU%@uhjJo z(<28Uwhwg2SOjAh&D>Q>|L&qx_q^B8+`Z?QbS)duy&Af}u__SD{@_D$0_=^E=xa;~ zMqD3a)Hv>J>EAuH*&T0n)7>2NyIilZ3VtIUm&Ey!*Z(KO(f1q)KLppfogB=3->~;N z{%GZYcaW-EzQLZHp}X1BXFxDE$D+4ClHu?@UkqQW@YrfbBkQ zK->O1c%}~tjN`4w-q4-p|KO2FAEW{w)RCk+o@2Ytdee|)(i!7?oNtB`#O6Bl zHo0zBCE9f66?fD9$FG*jed9*SY4G83&H%>~Q!ut1hyES&Hk1Bb&%@~bzC_QCmeno) zq`%rWs5|4Yo=I@%&$aJ%l$_m1dw!`U`g2TH0kU4l7!C3O+VyIV0V4K?{K5M>&^;?# z?)}FoISaXG+0HX&molCK8Q^?v)2@^Lr2S8k{)z+8U-SR@-Q&W%|>$ zVXP%{H@=vJvGqQ-l~vBqL5I6~Md_35YG`x{aJ; zn2FdO=A|%p-A{@B)NyS8Y1dHyv;AOvE%EhQEb~7i>IFD$s;-d6DnO-g&*R+!XniUx z0m#XWIbu!4Wzv{mDBg7&k>md>S^54)GVQ@fa zHNiEEQy|~0;{?Wy;u+d_T;DVec3%qiq(Akap+9{+ec85a&lVuf*|#D6i5CKoxxPK; z;g9b>P(}9QLWcI7_wz>$MMc#CxAfB>zP|AJt*qg)x#0kqvGLe?jN zXE{$W9_@L83wUm5Z}`FNjl7%tZkkgUsxJiZ@5?l9tL%pUW=uo%Kj{y8JY&i!UP9S<3&?WSuKBVW`6^x5OV8%f}Ubmm(J&nBWz z&v-mSqHk~Xe|vl4!Pr9N5&HUwqqcC^*Y6peu$~02U8QT-0l&<{n}BuSiDO+87WX*M zZFd6K)8L#h##u4GJ^{x`!O^hakuwtIiUY(##+i4A`6QVaBxwh5V)RTs1KE?NKLz=3 zjdyh(1MsZ+kofk=fZ>sz*qbsqd&s;gA9>bSz8#<0UhZAJ-d*qA)$-Wib)KdchZcvH z2P_X*94ZU@>4Z@RP&95v*|#Aeg&hJo9*l zg*toJ-=4Bh?02ngY&q@W1tO33ttOGhCczex>pfVfBq4wpC=bO(OyJFVR9 z$vlt(`40rTQug^SBgcX4sVCgB&)*oDPzM-2V6NHycKldeSX{Kl0|}7DK%lGTzpXO) z-=pjs`A>oD8{l(B{<-)6ojtcWu{_W=4+J9ne8$Ru+jz8mX7SS(c_0?;W+2ej^6&1q zw|8v%2&Ub4@B1RoZJDjE=q)@D8xkskKv&CuAUel9V{qk`dvk1bgWD%yp0|4|XO;)r z=7EIZQ1qeOhpv|Y_S$^+ebdJ$EsflBPnkEq0q!lWEepV5kN{v}OpK(4Lk9rj@%}hKf)A2{ zpbyZOpnfplPr!H6pl1k)&;x`7l!5X;HD-`Pci_mr)5_kS$^%yZEpP9sylv~%K5vgg zn@SjocK1@iIJBvRgwRMyj))Q3+(|elupMUGAB#Q$`vHW^uzr#eisxVl5aMw^J}4w0 zNDK*U9#7mw`U@MELb6SH{N{2N_Qcfu0B-B%nNrheF=!;O;BlH_UZ$MvI zusEA+u~#(|izzHhgSIxQq>plyOlp3KxV#%Q<-NY_!Gaz!oQ5GH9q| z{_+^P7`f@MXDTEeag1CzaBuk=-c2|QWjY`F;3D)dE{uv& zxHvjmMn=cTn8;`u7apY$i}F!77(2q!zhm^b`1)z%%gX;xoBme*f7Qt|C~3Q|}oQdKEZQ|*H)5pP|9 zP>bh$P>tVh+a{&Uzft)5op=thwLk-|MF;s`!BmnHZ5H)(g@KeG>KGKS4(AGE#wm7 z2k5B{7J?8eVONxtl}S?_O5=qN0&M;Ed-?Z04@lDA&r=vXU>~^%eu@$BAB+v@r;r*s z1Y?W+F-{n!5DQ&51%5>xuY}zbPumB09TyfRsgaQiR{+0P0*6*U`j&j`>GYb!SX zqkO>ex=i@QjU7J**9ns_F2-?r0>|i+=tl$JD-0eg55*})to&QK z>_u|9yV{AdXqPzG|CqhTOhJ8;keS_h+H|5tSD9m`| zVffbbAROEMcHUaZ2m~jus8FFC{(EEkb6-~ndmD1!(7@mH?6yO>AoF!ijm>=$`Xwsh zqbMjVRj7g=vVnJ-0QI;_D6f#pg(#x|DFLmTZ70(DVCQ|Zf&j_^c6g2<|s z;;k-&4PT+)MT=eytGE{JBB81pW9gM>HPJLy!LLr>I3M?HUkPTbM?avlyj;Ot+ui#{ z*d7Y_Fs%^U|Lm_2N)UU*aRJ&c1k>~jd>Kus2SIHHt-CXhNP9u@@?=w1mOL z>;D_@dPz{2!lf8H9gnzz-A!NnJ&W_Uc!2%V#E1dtvxX^XKmT{Xmt}9gp&+%if9oLe z+yB)z)JS1Tk<`L3M$io+UdM4izpV*2Ho*(OAAJD5Ve_&)fO_fy6v)3PqB4&A>Z^d< zH`Ji)KHmked8zZDBPyC4Q2D3w!?~1~D7^RRBl3^K4wEw=_k=i%v!=qA+7>PBJu9ob zn+H;&+1|rnHF$r8PcOe#WaTRGvg7lrh1};W$D^0uP^+LZdu-F$taAsMsDY2an%8{( zerbSI(f{vzhW%wer+scCTRo%`14PbN! z_w|tfqO!v6IKma#xK3VyUx1L@@Bg!R9q>^V+dI7%NB~ia9YGNgR8;IB2!x{ao*ojC z&;-Rs6M15bacckcPl zoH=vmoZ8rjtoIdGwS51xzOQU8ETg_eZi^s-xpIvd>EtSX_oJ}CjxhM54Zf%lm)e3D zj5w9n;C|>6Pl+w_=1C}@ep*(lAks4ss|(pywr_cDHBtKdzEZbCc<}c;7^k-5J zR2^r~zYceW{_22&_UT)qRzE#vbMa(2I2>O6I5d3H#7!m2@F7uH6v*cEf>lJf9 zHUF=V=vO)00^Td#P#*8q`i1F1Lk`fPbg_ zhFk;Xx~H)VWlVJbi5M|2a0L3DeUCLlS8wDuW&R@vF$S^21osdyZ)xSr(zWDFzjtHy zzvcW7hY$Z1@LlDx<2s)&*7>>DwFmO^5x~t_SwQOr^ZAGY+iAp$VEIKQG~~g zc`@h2wH{OCZ}fxyJ4;b6@6m5OhdgKepo$~Ng7(Zrzh!1;OGv*6=K@&!h@SOtiRUpd z^LML*{Djy~0Ub?@MI7N6%*}30`O|u#dhd(n{2v2fv&?^uoSMv|Jp}oz+kyKX>(mp! zPMslG6;t@tMc%NG?;nA>)?dM&~}SFBd=BQdrD?_zdYs%)`0>kpaC) z9iYC4KF17jpLz#@kP^q2E*AsqH4?+@+@&8tKAN;?i->Igkm$!4 z48RBXDcH?RkN-MO`U)~&Isa)x4#gbcdB_v-@*Q`HHM9RMId3rfOGGO6>QE-ru%4ZP zcz8nTf=u|ch-`q<)_)TJEuH)GJWKh{ctXYp7?=B`9$;Ky8uE}4=|(*Ztt(RSoXF(f z0_cFeqsK(RLv1Ab)wvaMgVtBL_j(+@amL~5x)+x7pSiD@b1%5rgA&{4%)vaLqZ4?a zktRX;&V&4s^Nimi(zA0!UIAnMO2@Jcqi@?19e_W(;SWoGhdRen{*&wUp_JX*Z2BCd zZa_eh$=4!n1eyN~c2gRBF~ImPUBx4~Z(7!?i(@zP_gc<>#`g4cuPxTU`<{gKKf@l) zMSQy&|0sX?2z4VEsz=a0Raj6WGIQX&26FKp&r@<-UVFonpBE_o_4uI{w3hOpI+Qxo zbTA$$4{_5uxe_v-kZ-a>3t%MxY48zVKul#K0Of4Jx%1-VA;UEPwf%nu8PNPEh5qNm z0^Td#p!t6zkB2wcwS~YgB*vg-1#@+TB0nuL9JO^`yQP85F%gXTQQ{t~Ew7pLvuUi! zL(U53-r=EQ-8nv0^iiID@c(%~8~Hi&utsTaqnIYzoN#mCf*mZ9%AC%?!9E3W~iGt zaF!xaR+V_et44swvNXU7ZEN{8AH;lV<|fiwGIxhjE}Y zYFN$a+gBn#F5bX<6PKzuT2O*qVkOvnjMX&) zUJ4*vA|KBQc-KW%VV1}#$d<^3t}BA?4WT*z#eelZY!&~NkEGi8&%MMz(w}=pWb4O7 zP_6nhhMRHRBON?t%y2N)cZixIzxq1=H(jLpZ{(-uzvll<=pP*D0y`gI&W?%Rw~8^? zOG<=d4@qB)|NWXZ6*(u4NN~(7!TdfKYf?lW#(&OI>8GX_KmiXc^e`VwE9x;P;D+7y z24hCexK*qIV|AE&K>a4R${(+0>F5o-%1+^go~;2_x<+MZu&u7~KOO6W1g|q~=WY>l zXH(>9sU^XB45MAVWIhq*6j3&Q+40qUUT~$kLD}>5^Z#vpuI&EnZD%itP}u>4$vO z!MR}<4^OfCx5bcggASZYuT1l3v*etReCda7VyBz%9A*T%K=OU*qpZ4q|Xh;Y!!0bOYP!;{Unq3_*ho7b9_H5-CPo5hgxvw0K zXse6={KZ6)%p-!|laVFXfA*!sQ_%SXP;)H^HOGiB#Cj9d0ZM)q|H*qo=N>HW8`VcO ztsUX$E(rR7m``pqZ>yk?@%6L=h%^BHO@Z|dq6oh7eAF5&_#67*r~%?U-a{>6R0B1DR*xY* zSFP_U{!<4?-mCTo;D2Ej?jOK|S57;iI5kb;UxS80|x?atH&420u&HtJ|mMczQPaA-7pP_r2M(fO1-Jn_mo={m zn#J-^W0|VOiJ?x#n$U`_=%XUXef3${S=d{Hc?)(Y8WD@R%x;mM`kO@Txi7?tW7EWu zLzBhXGoMPNCNGlbpl}!dhJY^w4gsVkmusM9Z2;5wulmc_R+s%RZ5_re3&x}yh;__PjQH36gvzD}+ujdzKqCq_XuGa%O_zmgV&|pwb zhV4rH1{te0&mAr_wBGdFhxg!N*{(2QV+-9HFe=;4+3%(9jXt;P^oZg< zTlRafjiVTD1ob(6{j4E3%F0DzFk*W{TC^0or%y^`BS$YH-9$<{_K~*W zPN~pC$*mextwXQ4)%6o1OXS|&I0>u|ZVa5^>dw8nYyXFOn{mUAO^BcWI z`c4B9H+zbh|5JWTsZQMD9T)l-Z?Kp*5{+UGC=nXEGQ80 zG{Dz@7YyBK&;!-Lf4O+U{=tQ$V=+VWAI1yos>qufmn423|F#6zgM*OY$roe95IiQv zxFbf$+7_~e+N8j6&Ho!#|8cxmb{lK)h9RC;fv*R^*TY<1!?4y*4D#?2J7>*?9DpqZ zxSx+$u}s$Cfvghc@m{@;Kd#Wj65gwGXocOZrsU6zjcK!SXuzp=Cf4gFcw zir?k6*O0*f*ISnG-gFM^dsg121%tFB67s^iwwJL;;{3q~aeSGVIKRHONZIZ#k+Iue zr0uX0@mmeJw9Q&1>~InB+nog;6MT>7@LYm_8@s_voLKHHPVOBiA@bMan_0)aG;c4j zB^PU?r5#wk<3BD(_7BJZOx$N8UHW?Qg7N0Y&R>8WPc~vOYYcJj4{-iSztJd1%o%|| z$WL|2xBlL1>o9-Rh*W(P2Kzq}{(bK0BLZz5#bDTR{V@N33b8-L;l;nh>Dm=m`UoqwVB5JXfB-4d+MMiTJfP zV$ZVH5*a7o#24;He=a>p`E)(h{QxpTPYg`vL_*zC!Zy!(^G6Fx)wafDHVE@$9ePjxt>M8MTT6v z;4c8OkBEYEUx}Egwi1VzT8Z>6b|MXYPTg!Pk+#_ZuQ`kK&2AFOQLZ9k9mfB)&LU}} zi-dfS=R`Y7@b8HX=#anvaFRH=@oCgQ_#XT@j_)@38&h-Bpi9w{8#G_xDM{! zU|nKk4@q6mx`d_!eRyt5wCGs74)TZEN^phgjDc^2{zR>)qb*uSr z;xiv;{@*nEigB81&7bRhVX)PPW6h5Uw09Q6+&skKTD8T-MGGaQj~_N5>(mpp3F*7e z$i>Ko@j`+kWC3tWg&yYPdwGvJgr#Hi4%hz-^KjH1%h@Q7ZyF>{udFYT6lEn6;Kxti zZZA@|yGSH&!5AOqEK)Wakh#TG6zr%a^0#}6jA$1LzQ=QTF2TP|+2J4(w_1xN_y&mm zzk7?=J>i&dU=Dzo0D^e~3@Z{`QN;zaGpob?SM~>69Ly`K?kAti+%3j)Qc$OrxdLX5 z8jU%CgTxTz2pSH%e<);$2u4208)2-|*YMxAhacwnDz^*$G|cf8%zFlq|Gwb<(0cVC z1Xvd%V4N0UKB(+Lu3J+F8$;-7`oK~Snd7L+YAJbNmm`-~;1Drc1QIhwE z*fjScadxGPgxGH-&Vs9Fp##oE+elo1{U5*C33CBQi43g$WpB0;S(`9dz#1UI_jnG^ zCC+cL73c6d=eAi1aok#B-vTT60lI+y(des-63io#kDleaBvEbrH{Z8`JH)+Prh_Vp zx!a8VPs4TQWTuLe(`Q6itPK+Zo^^yT_Jy;?FcAo!vgY1@bFVV{MD58^`!o4n+?Qd# zCsXdx0N>SK-UvJH>9-ee*w>N>vhxsa@cym^3r)BGMt)$<@A4jVQJX%10=U}{;VO}I z>}7Fro3}W%#8KiL#{Fb)xgv3NmAxS1H`Ng_EAJ5^^;3Lzj6?=Fm1-w|*Tem3QoA1pw=RfrT z`Onx)#s5g;^BXOFfQN4Cz}7)UIWzp zzq&TK7T?Rhr}oQ4@<{EITm6Hn}z(G2>r$I1^XK~vEvzWeuImMTjeHkZVT4_D|q}rx6KZI z06TGUlf8&pi~&?}+}&Wqo%nqz zZn2ysTO>Qxd6K0(ko?EITIT459uUQ+B#vx)9Q*d1;M=byaS=Ydist`CtOXDW8=(VM z-y&i*zl00;OCqfpwffN2%#}`XEx;03`nxT?H`n5O+4so*oLtTUaN(2_;OLSiq8s)E z`MVgvy~3K;mCNh0{j2z`+OqGf=k)iF^6-!tjK_fvE@CiZw}=7t8o)`;JU0JK{v%tF z$d>|r0{lNA4sUuCemPf>9qlcVw9Tf%`JWK&E{Nm}h!0xd2yuhm(7y*I82_7&p%keT zB`o1S+yQwJFQ6;P(k8=9IdhCSF)ylmqrF~Y$^^B?xN;#^gl(kO@R-DjEsu%RXg84?Ra+u`d&Q6c@tY7U3}kL{ z7C9Rmiqp&5;{v$8vKvc%B_ai;vz3h9ra_)~k!rAG#?kou| zTF!gt9pMAnBSM%fw2H^SoBjK^wpUgMuul~K)%rjLV|}m&Nc08&8SCE#`5K91bAQFS z$ayFx%QfJzln0XkNWOu}eliD#$UOkk9S~=>Jt1TIGuMEhKq_PZD%k&*h!`LuZ>O!u z-RvQb{N5aKRq(L@%(N4a{39y13L9;pf6IrZzuVGVSeE10dMLvLju1}H5jU4na= z(vKe%LvL*;;ivciSGoQp{}q%CK>jPUZ8`S%0x5{;t8o7B2LFM=eTJC9 z{R?js1*zXk6k+diTy7GUCJmLuq}rwGEeDqVZcA^@wfJ84J?1P?^Z%5*3_-|hFWAdC zzRjZ&Bak=lW*?8M`uD4GzPy&PzY(@B5<$=heG#h>*!BqtfqJz?wEry?oJu+m*#;09 zZvevfWIh_W$Yk6xThn-{MA3qlY4%W=NPdR0|Bq~aOvFdKiu9;j63M&lDxCib82gF5 zzud&d^=`;7fV~FWJ4*=h2YyYmNP{DgD9S?aKhzCmWd&#|iER9OK2j>d^uU~$)MK5VbAu)3JUxpzTn2120 z0Opknfxp=wb55cso>w(b{;A(r9`Durn*R`1!MW7)9(=D?a3B7Lie$cwHc zk-gWc!tMWL-3i6R_#i$3E7Kq4#xt)=Q zy5lhD3BI4l{g?o2lN0&a*HBoD_nF@qC`6ufqB8hT?VFU8#Oa2}M6Mu$IfLciMO-Hw zf+F&fr$w+U_K`!@)LImCgHQ&UV`?b&ofE#;e`4wQrq8>!-J$qhUMt6ca6bYFMyzHh z#Q$carUya()4!8n&Evnhy-9sUIlk;r`T(^mS+CL}fJ>(JKbea+PV8FUT*Ph0`W)uv z1o!M#xc?9N01^Qi|F;2qhWB8dWu2A8{x$W**au{q-BFIti6Raq`Rvz!w`-`+X@A6uHE^B%btnJYqx!Xl5a=H*^;|f_)U5E1Tvt|Ct4A{NQLrY{}zX@~ZGf$nY zHL7w~l-EMInYl2HLT20fRH;hXCLx} zAkKg|g_xjY>+Tg7clt=AX03o-&%FVt83Y@LV0n_1EUd5RXGmm=RQzms1qiIw5HgwR z72*aIzsqYa;k`PCxj|(Y(kL+=)CuynY|Kx96YDpN?wI>Cmux8NM-%ir`(r-9d4NCc z0)qJ_uUF3X`B&CI%E*PVpX2DUbqJ{`t*m+#tJULT(sl|?`5?f z824a`7{GCjbNpOPgb5Ca3Rf8Ot{OkbMQZxiI7Lp_C6TnRgGkumC{k7+v&i{e`p>jvK-G4Cmzhk%ZwcDt?#OTEcsEP6~W|Qa*Tpj5ZhRyJvBo1{;|O z8ZMKtA7~iz$17hy$DJUI`K&Ri_^)cvU9YU`^Z#3Cl*fDZKKajme?%|L1%4ho4vITn zg0Y>HoC4n4>O3G|87Iz2Y?`-Fd_4YjF{;l1iQpHzi7=m+#OPlA#e|OCEyk-Idy9aM z&kO$-x{H7LwwKudpW8$s_Wz5GUJ_YR6+iwbgWp6}G~x_b*ArRm?+~#oTqWYRqK+71 z1Cx%}Nfd6!dE4NR+zP^nv1cd9CKZ=6_jxK#lE|w3`1F!`JH4dt8qV!hS!Z6ZG(kk3OcxmPpCY zkn8_)f`?Zs{K(?`l(FuHSYCP@v-xGJwkQ_7GorVOnXEn1prJ$$j2(lV5HIS1+!8>4 ztQ)cZbdZa?7>KoGOW{Xfhod#}93%IEH*z2VXXf4|E+KFC#SN|!$(1nwzpnK}#0Vr4 z@D;Gmh{Q(b4)7Gom^&!MN8KZ0kW1vqa_lD}Ho&Gh^peO({X(Jy`9%sjAHZPDRK5iB zjKrlgc9$>kJs=-JHTs8N2F8#BK2wf3I#N{#@5yBIfWEk(ss>Zh#c@ zw!x7?otujI$-)xq0LBpk*t1hv{6Dm4v3SwZSp*<27%>{$9|axYi~FhSPXysUlH1B= z)%>plJ%=@yVTi{hx`R*qf1M9mFz`Pcxm+awWlmbWTnR@Z1|!C+7-Ck?CMJl90|rR+ zX?VNbhu9l3G7z#s426#3n4tDH2jKcCCyXEQL+p~ps0rgg>V+huc7RC-&=%N?eB{U< zV6q2b7o@C1oxyERq6l@ziFEi4POU_Zkw0on99jFAIJ3QvD7dr{chKMyC8Nz(~ZJGyX)a{{`SaYyMXO=gsXW*7(D7 z;w9M7=hm#noj35Gv0M~~64dsojDw$=L0=>53Cg5fg z%gYOt?Nzeh7phk@U*e94w9ia4pxh|j{Fd}=0JK~mYWWECC zEXV}^GdXwJ<|c7&eOE{&?89KhB(g3!*NF{l#kQC)B34|E7x3YsZ#e#Q zKUoO!6{Fx66(2n=(cP`K2(NP+xQ#eaAQFBe z<|c};Wvqy`gv=QMT|mwg{5|v^aZX@~-LKB65&W0FzfztX`~d&`{MdTb5{Pz0UU95l z0Li-$TZnq2shf?6aujKZH9Yi(qd2KduiR7#`UV+aSt$qjAKLB- z)CF*s@P+;kM~zU{m{fa$!r>p({I6R6`(hsEhq05O|6lUI5WEEBel8hLUMUhGatj$7 zRwRBLJyv1>)*v{3k3-!!f?R*Y-CK-y#JnFmfS@fv-ZOrTkopV1f%``TF!#3)s1rO4dO)r1h1wZ&(n6=}^}njo1DgLg z@cO^zzn=eB;@qFMGuO@3`ak1;GI#GW#DO3dgyTQ=)>YP;5N-eL(Y!yi8tUM4u#zxjan`%1`~;IQXdYl>qK!TM9${;!&GU-<$2VQ&w$c9Q4; zJ3C|7Zrpx@|1TH0WSC#x5HnI4QjsT8_wNy%t*s?SyL(}dkJxY<)FA^x@i-DPq4F2g zPULl~_z<>y&ewU4`c0O!fr$HK&hu~wFEOrxmBjvUQ1@$_r$|C=ZX#iG#n=Bb-0IR`d4)tX%N9&^30RV)QLm5{G^O@3**%OY3V%#BZ+n z{eKn7d(&qr8^Nrl4k(KY8{i*^@|HNUs<}u%ISKqf1D_x2paG1>D55D)83*S7|9Sm2 z(G`3r0x^b^SMmLVZMf84`c83*Q*zr6K^iT}Y4 zZixHC91QutRV>#C-q_wXFnkX5O8${;wMU6SmY0 z`#)#Bhsc5rkO3QjU~VCf{T%a&Si}RLgO4x+Z8~Fxfs?E67N>U)$4Gw+`JK}xwlrnp77%$b_;pIRsE?52SYrEMfcC>`_bqAZ@Q~``ClkjfBKo| z4&OZyVuyW*u>YAWp5R`eL6{e?PMPxg(a)#$gsOc&e2?c`k1PZ`VGe3fAA^U8s0I5U zIPzU15s&?Jm)6ymNZML6{C_6?!~W0U9DuezVgcf|SxcDnKMl4&aeP%%aen`Ju=|9_ zDa?>CP`#2I0N7jj?=ho955)Nq&xz);l>^IxLXp!iS+@nMQ z>X30gG8}tE2(IZ3Wtw}5lfBaTkiA&T29Q!xfBEOlp#L<NM&!CI4y#f06b_Wy<9zFu~Za9{QF%IyQq zf6af*|H|dqjjY3}lmA61!kK&YnDD*r4hcVN=7T^^NW_H_{utA_|0lA{_&*Tign}CX zdCv9pGuIdWG5-(5_#feFFY#-bm55zdN2DNs4#Aw9R~m0`xYw?*Pk`fpStPA-6K9v* zB5`Q>(}?q0jy1n{k&c0%;QF5-7Dzs**#EHp_xejcL@(6%AOd0Uas21~R|_I@H-?iDia|JlraK=28y z@Dj%tH;_26tP}XX8~N5!L@Gl5h+Lr3_>cIXksUjTKJe=hjQ{8T<$X}O9poM_ZS!B3 z9A8boEq%Q@`HvL*Qr!0av`7#=;QtFk9WoXF6YPi@5st_YV`nb{9I&?s7=|^0fp&JL z7y;b`;tj^# zW+kFtyGNu&-767~J+xd`PeHAY)XlaMnLBJn)-GF_V~2TmQc)u;4M<1)T{>cJBsQ^z zxLYasobTbB2-%rDmG!KR|Whlj*K+$ZLq7!F%t2=5sX37tS)u6(^*>ruX5J|_4c z&#_d#^?BFst7_#x#+#BnC;_CMBHnzyljz47e-HTo0LsKjS3`_A_lXg#6YqyS^1~1- zLa?qp_XLi{d?6I^LIn2$Grpf=3=xWaQ(-Q3;7@czjT7V%1eia7dkMmkV?gPfAV(|l zWhZ;Ff7UG$nL8Vbgtg#mw3Eo%;VhBH{q(5YmtaIXe1v(sYKwI2xli9=Z2;>Dlo67om<4>nR~0SRwNNA?Di> z-m7yg;Xcn1d1<1@U5&*{(BB04Pd=0Xs{b|rug%rU&}Ewcn*W;rRnNQ1;PU_HGpm#T z%t3qcQj*$kgeQrjyZ0?J$1n5zjziwxNQ~2Ckr#ydeF*vnICgN~7{S<4+Jqx8myE#L zArS@)CycftV~Ro$Z!UkU+&joPbIv>ATXb^AdCWWEEb|RS*0T~n4RMe-`^y6u{}E5K z*&TK`*42T;P4KG$sTlVO*6};H9Q*Dt-m3j~Nti1nVee2W@SJ<|a86>hn?%}<+8CEn z`)?!icA)Mbk%;~JS$pc@Tz3ODd0-zB;Z_RT1SVU+U=widFKZp_kk$4Q`LHpvVV@*p z>?h9SJ5Ow`FZQf?0Ve-U2_eqQ$yY91FgXbNCyG%g7zx=cXZ(*pCpz8YZMwJe-qD^% z{4aAi5)rtE+WxOvT~wX?Pe^BtF&y+e5wJy%E?q7H?r9>?3;Z_Z`m#d4SUU;kj2?r$ zQKK=xCn69tz+AFnkRP>gc$AGDa!cEbNcabd5X2HPw`>Srk8p952(q(6-Wh8VXk#N| zfEWkFy+8qY{q-gW%pZ1}NLqO(_R`)ZlCW=%IJd%HTv%roJeVo})O_11FNB;46qit`7oxdQI4j zoS+Jb>E}L$Oq?sA2PEJ#D8W1>c`NdR14p9n5xdqtDlWx-j!tII4gVV zMCR(Y25f2uE_3Wgd>s&rT7IX&+XRfs2^gCtFlHx2d6t6D`5w;6i?)`?L0m5Vd6|E? zinP6`1qQ@{%W3fcW$$*BNQ#0FfWUV!ch41pzlq%gzu`W2QSzs^L>7FOtO;;%#a$Ao z_6!k8=RU$}%YJmQA^&$#QI1H@!v1ddxpBX@bjH#rQwjdRxD`u8H#b)?0DJrhnYSJO zKh|wfwp}=Mf*${?j{BT1X#Q*dYyMX)=l(zQ^ve31>f}F{|Kbu7`bI`FKJ@=w82HCLI5aNvU8(E22UERgDNe@b#{pnGWxUwbU zgYTBOxS_5%v&u=tZFYej?{2_GPZ38OL4nWt9?mIvc+7c&_ zci_UNy7)UUiL+6N)rqQS0P>NXB{q8-Q5&rRJ7N3p!*?BC*;wMhA5F#n6;FvXdm|+B zQh!75ohsfbreR{*S;M zPLKaptBb0W|5@p%{{=ZbA9GHkn1$(Rf1-vg!N*0YBYSS^Cb96ncO^zV^`v;g(^EVT z8=mNl`FtnT1ANNbMhx?AEYaK16MDi?^vBvCF${JN;{;e|_7!J0iD5Y34>o?V8+-$Z zF(3v(76*g>{)iQjfNd)G2w`u)YxfxN=?gAmNkn6@|I^kIvGbl5iN8NDVt;>7#QxUG zfW@sv?4pND!RLGr=j{FVZi$mi9|QNF5W7}BCblepRDzHHUfoXYT+l)y`j1D20S}`! zVo4hjz4W2Wv7wFFxxSM)ymh$5#bY0e!i**0_FqO^#M)j4R{K!z18e+*$ zsZSsk`REMU>7WYnHLN-s_beZ{s8B}CK!g6egf$$aL2kq zeJe5cZUbI#ZY8EXVkM@v1D-JAF|=)MO2OxR59dtjV<&NRsULpxJpAw_qB!S!iIS{u z@q7^oINt!V5YHDF@he&d{@q+N{DQtgeZTB&5`}rkpgJ$%{R|1lSr$T<=D`BUhF&Gm zwcvU&(mbQ?r>O!fvH_=|22dsNe{#Ea$oqx;R#@*R2IDcu{(jU6(9gse*m0WwHxK_I zc$OWIUD^WWAw3JRECAhEyBDt%!`nP5(ckM<(Tm(hyib@LVu1jz{W0!joM$bWXNZIM?%V3L z{~;!p9Tsq(xy=|9A}KYf71v6o;tat#w)9}KZYUI4sC_~V=)_ysM&aQG6;5sc@74u~%x z?6HTzh!;E@1#xQ4@Ay?5DYIa2XGkzczOX0*Q*h?u%!a*gX!#f;t3(M}g0Z_Mux5uD zSjU6FEZ>9zK89Uy39e`S&ow3t&B@8Z)t5-{SJ4hYfPqBZmR)k}FK_&(9dIQX)vr}v zzx_Ba8}D>C4>RhKt^(sa{4 z9<`sx-2T`6t~`F1*SG&9{wsmY2X`)ICyNBswIg!#=tayEhkyN9JZ)<&@v=R}{JM2y zEHODxFm8x3MZt(2B!aM3sA>_G*OK!l*g>}#c`t1Vd=~dG`eQwz3)Uiu(@R%SNy+*O zth+(*C@v@}0HIH%m3#0A7y~l@K1=WtM*7H>^BO|E$8$}nM*ia}Oh*>x0z{5fKzR9lf)ML%>isg5C{i*q{`Cry2a6RL{iTrq=`Cql%Esxulyk8UeZ>c=y7vy0` zKnm8}0RLu9#WS$=`yzh;a}E;`h!+S$3`!VkQxII6 zE^pqi-sj#TbFlBtYZ3cdBi0@0i5vvP`QKpw=jF?KyIHWE3HduQma8#72QLszk)V)| z_h~99(9KrXSU1`Kun{VI{;y)Ts0XqO;i&|;myp+V^yftqJ*^RcQO37N8-QcJ@&~B4 z?0Ncm(3bY&wWM1 zsRxjonix#`-$MVt+QX_^=C)_wD)=G?5z)(t*v0Vwf&YlU6Pbv|BY6GfD=t$4SByEe z0r_~HpdZiR%PZx!x$mEktJ4236%nq9huTLh*95Zj#3%iHB?j2kGOh6|KAZEO>jDbu zece_r?^XZS1pX^tm)E9dWeY;`o+MI#fT=(83uR{&i!2a|5C``QpIWtqucy~#<9`VJ zfTNImoCrp4083(jc#h(|p`{
    7ch?)xIPxHn!WVt+$?Ze9-fUw~+zLJ7|I&HZom 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 literal 0 HcmV?d00001 diff --git a/public/images/favicon.svg b/public/images/favicon.svg new file mode 100644 index 0000000..feb5a41 --- /dev/null +++ b/public/images/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/js/common/auth.js b/public/js/common/auth.js new file mode 100644 index 0000000..825bc31 --- /dev/null +++ b/public/js/common/auth.js @@ -0,0 +1,29 @@ +window.getAuthToken = function () { + return localStorage.getItem("knoptpq"); +}; + +window.fetchWithAuth = async function (url, options = {}) { + const token = getAuthToken(); + + if (!token) { + window.location.href = "/client/login"; + return; + } + + const res = await fetch(url, { + ...options, + headers: { + token, + "Content-Type": "application/json", + ...options.headers, + }, + }); + + if (res.status === 401) { + localStorage.clear(); + window.location.href = "/client/login"; + return; + } + + return res; +}; diff --git a/public/js/common/header.js b/public/js/common/header.js new file mode 100644 index 0000000..7939feb --- /dev/null +++ b/public/js/common/header.js @@ -0,0 +1,29 @@ +document.addEventListener('DOMContentLoaded', () => { + const profileTrigger = document.querySelector('.profile-trigger'); + const profileMenu = document.querySelector('.profile-menu'); + + if (!profileTrigger || !profileMenu) return; + + // Toggle on click + profileTrigger.addEventListener('click', (e) => { + e.stopPropagation(); + profileMenu.classList.toggle('active'); + }); + + // Close on outside click + document.addEventListener('click', () => { + profileMenu.classList.remove('active'); + }); + + // Prevent closing when clicking inside menu + profileMenu.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // Close on ESC + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + profileMenu.classList.remove('active'); + } + }); +}); \ No newline at end of file diff --git a/public/js/common/logout.js b/public/js/common/logout.js new file mode 100644 index 0000000..76289c7 --- /dev/null +++ b/public/js/common/logout.js @@ -0,0 +1,23 @@ +function openLogoutModal() { + document.getElementById("logoutModal").style.display = "flex"; + document.body.style.overflow = "hidden"; +} + +function closeLogoutModal() { + document.getElementById("logoutModal").style.display = "none"; + document.body.style.overflow = "auto"; +} + +async function handleLogout() { + try { + await fetchWithAuth("/api/v1/client/logout", { method: "POST" }); + } finally { + localStorage.clear(); + window.location.href = "/client/login"; + } +} + +document.addEventListener("DOMContentLoaded", () => { + document.getElementById("logoutButton")?.addEventListener("click", openLogoutModal); + document.getElementById("confirmLogoutBtn")?.addEventListener("click", handleLogout); +}); diff --git a/public/js/common/sidebar.js b/public/js/common/sidebar.js new file mode 100644 index 0000000..c6922e5 --- /dev/null +++ b/public/js/common/sidebar.js @@ -0,0 +1,90 @@ +const currentPath = window.location.pathname; +document.querySelectorAll('.nav-button').forEach(link => { + if (link.getAttribute('href') === currentPath) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + + + document.querySelectorAll('.sidebar-link').forEach(link => { + if (link.getAttribute('href') === currentPath) { + link.classList.add('active'); + } else { + link.classList.remove('active'); + } + }); + +// Auth helpers +function getAuthToken() { + return localStorage.getItem("knoptpq"); +} + +async function fetchWithAuth(url, options = {}) { + const token = getAuthToken(); + if (!token) { + window.location.href = "/client/login"; + return; + } + + const res = await fetch(url, { + ...options, + headers: { + "token": token, + "Content-Type": "application/json", + ...options.headers, + } + }); + + if (res.status === 401) { + localStorage.clear(); + window.location.href = "/client/login"; + return; + } + + return res; +} + +// Logout modal +function openLogoutModal() { + document.getElementById("logoutModal").style.display = "flex"; + document.body.style.overflow = "hidden"; +} + +function closeLogoutModal() { + document.getElementById("logoutModal").style.display = "none"; + document.body.style.overflow = "auto"; +} + +// Final logout +async function handleLogout() { + try { + await fetchWithAuth("/api/v1/client/logout", { method: "POST" }); + } catch (e) { + console.error("Logout error", e); + } finally { + localStorage.clear(); + window.location.href = "/client/login"; + } +} + +// DOM bindings +document.addEventListener("DOMContentLoaded", () => { + + // Logout button + const logoutBtn = document.getElementById("logoutButton"); + if (logoutBtn) logoutBtn.addEventListener("click", openLogoutModal); + + // Confirm logout + const confirmBtn = document.getElementById("confirmLogoutBtn"); + if (confirmBtn) confirmBtn.addEventListener("click", handleLogout); + + // Click outside modal + const modal = document.getElementById("logoutModal"); + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) closeLogoutModal(); + }); + } +}); diff --git a/public/js/common/theme.js b/public/js/common/theme.js new file mode 100644 index 0000000..554ff85 --- /dev/null +++ b/public/js/common/theme.js @@ -0,0 +1,24 @@ +(function () { + const root = document.documentElement; + + function applyTheme(theme) { + root.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); + } + + function getSystemTheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + } + + function initTheme() { + const savedTheme = localStorage.getItem("theme"); + applyTheme(savedTheme || getSystemTheme()); + } + + // expose globally + window.setTheme = applyTheme; + + document.addEventListener("DOMContentLoaded", initTheme); +})(); diff --git a/public/js/pages/dashboard-virtual-assistants.js b/public/js/pages/dashboard-virtual-assistants.js new file mode 100644 index 0000000..9a702c1 --- /dev/null +++ b/public/js/pages/dashboard-virtual-assistants.js @@ -0,0 +1,263 @@ +const API_BASE = "/api/v1/client"; + +// Format time function +function formatTime(seconds) { + if (!seconds) return 'No Record Found'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else if (minutes > 0) { + return `${minutes}m`; + } else { + return 'Just started'; + } +} + +const vaModal = document.getElementById('vaModal'); + +function openVAModal(va) { + // Set basic VA info + document.getElementById('modalVAName').textContent = va.name; + + // Set projects + const projects = va.projects || []; + document.getElementById('projectsCountBadge').textContent = projects.length; + + const projectsTableBody = document.getElementById('modalProjectsTableBody'); + const noProjectsMessage = document.getElementById('modalNoProjects'); + + if (projects.length > 0) { + projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join(''); + projectsTableBody.style.display = ''; + noProjectsMessage.style.display = 'none'; + } else { + projectsTableBody.innerHTML = ''; + projectsTableBody.style.display = 'none'; + noProjectsMessage.style.display = 'block'; + } + + // Set action button + const modalActions = document.getElementById('modalActions'); + const viewSessionBtn = document.getElementById('viewSessionBtn'); + + if (va.isCurrentlyActive && va.lastProjectId) { + viewSessionBtn.href = `/live/${va.lastProjectId}/${va.id}`; + modalActions.style.display = 'block'; + } else { + modalActions.style.display = 'none'; + } + + // Show modal + vaModal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; +} + +function closeVAModal() { + vaModal.style.display = 'none'; + document.body.style.overflow = 'auto'; +} + +// Create project table row for modal +function createProjectTableRow(project) { + const statusClass = project.isCurrentlyWorking ? 'active' : 'inactive'; + const statusText = project.isCurrentlyWorking ? 'Working' : 'Not Active'; + + return ` + + +
    +
    ${project.name}
    +
    + + + $${project.hourlyRate}/hr + + + ${project.totalWorkedTime} + + +
    + ${statusText} +
    + + + `; +} + +// Create VA table row for main table +function createVATableRow(va) { + const statusClass = va.isCurrentlyActive ? 'active' : 'inactive'; + const statusText = va.isCurrentlyActive ? 'Active' : 'Inactive'; + const activeProject = va?.lastProject?.name ? va.lastProject.name : '-'; + const workedTime = formatTime(va.currentSessionDuration); + const projectsAssigned = va.projectsAssigned || 0; + const viewLink = va.isCurrentlyActive ? `Live Tracking` : '

    Logs

    '; + + return ` + + +
    + ${statusText} +
    + + +
    +
    ${va.name}
    +
    ${va.email || 'N/A'}
    +
    + + + ${activeProject} + + + ${projectsAssigned} Projects + + + ${workedTime} + + + ${viewLink} + + + `; +} + +// Load dashboard data +async function loadDashboardData() { + try { + const response = await fetchWithAuth(`${API_BASE}/dashboard`); + if (!response) return; + const result = await response.json(); + + if (result.success) { + const data = result.data; + const summary = result.summary; + + // Update stats cards + document.getElementById('activeVaCount').textContent = summary.activeVAs || 0; + document.getElementById('totalVaCount').textContent = summary.totalVAs || 0; + document.getElementById('totalProjects').textContent = summary.totalProjects || 0; + + // Update VA list count + document.getElementById('vaListCount').textContent = `${summary.totalVAs || 0} VAs`; + + // Update VA table + const vaTableBody = document.getElementById('vaTableBody'); + const vaEmptyState = document.getElementById('vaEmptyState'); + const allVAs = data.allVAs || []; + + if (allVAs.length > 0) { + vaTableBody.innerHTML = allVAs.map(va => createVATableRow(va)).join(''); + vaTableBody.style.display = ''; + vaEmptyState.style.display = 'none'; + + // Store VA data globally for modal access + window.vaData = allVAs; + + // Add click event listeners to VA rows + document.querySelectorAll('.va-row').forEach((row, index) => { + row.addEventListener('click', function (e) { + // Don't open modal if clicking on view link + if (!e.target.closest('.view-link')) { + const va = allVAs[index]; + openVAModal(va); + } + }); + }); + } else { + vaTableBody.innerHTML = ''; + vaTableBody.style.display = 'none'; + vaEmptyState.style.display = 'block'; + } + + } else { + console.error('Failed to load dashboard data:', result.message); + showError('Failed to load dashboard data'); + } + + } catch (error) { + console.error('Error loading dashboard data:', error); + showError('Error loading dashboard data'); + } +} + +// Show error message +function showError(message) { + const vaTableBody = document.getElementById('vaTableBody'); + const vaEmptyState = document.getElementById('vaEmptyState'); + + vaTableBody.style.display = 'none'; + vaEmptyState.innerHTML = ` +
    + +

    Error

    +

    ${message}

    +
    + `; + vaEmptyState.style.display = 'block'; +} + +// Close modal when clicking outside +vaModal.addEventListener('click', (e) => { + if (e.target === vaModal) { + closeVAModal(); + } +}); + +// Close modal with escape key +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && vaModal.style.display === 'flex') { + closeVAModal(); + } +}); + +// Optional: Auto-refresh data every 30 seconds +setInterval(loadDashboardData, 30000); + +// 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'; +} + +document.addEventListener('DOMContentLoaded', function () { + // Load dashboard data + loadDashboardData(); + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closeLogoutModal(); + } + }); + + 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); +}); + diff --git a/public/js/pages/dashboard.js b/public/js/pages/dashboard.js new file mode 100644 index 0000000..38ae32c --- /dev/null +++ b/public/js/pages/dashboard.js @@ -0,0 +1,142 @@ +const API_BASE = "/api/v1/client"; + +/* ------------------ Utils ------------------ */ + +function formatTime(seconds) { + if (!seconds) return "No Record"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + return h ? `${h}h ${m}m` : `${m}m`; +} + +/* ------------------ DOM Elements ------------------ */ + +const vaModal = document.getElementById("vaModal"); +const modalVAName = document.getElementById("modalVAName"); +const modalProjectsTableBody = document.getElementById("modalProjectsTableBody"); +const modalNoProjects = document.getElementById("modalNoProjects"); + +const vaTableBody = document.getElementById("vaTableBody"); +const vaEmptyState = document.getElementById("vaEmptyState"); + +/* ------------------ Modal Controls ------------------ */ + +function openVAModal(va) { + modalVAName.textContent = va.name || "VA Details"; + + const projects = va.projects || []; + + if (projects.length === 0) { + modalProjectsTableBody.innerHTML = ""; + modalNoProjects.style.display = "block"; + } else { + modalNoProjects.style.display = "none"; + modalProjectsTableBody.innerHTML = projects + .map(createProjectRow) + .join(""); + } + + vaModal.style.display = "flex"; + document.body.style.overflow = "hidden"; +} + +function closeVAModal() { + vaModal.style.display = "none"; + document.body.style.overflow = "auto"; +} + +/* Outside click close */ +vaModal.addEventListener("click", (e) => { + if (e.target === vaModal) closeVAModal(); +}); + +/* ESC key close */ +document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeVAModal(); +}); + +/* ------------------ Table Builders ------------------ */ + +function createProjectRow(project) { + return ` + + ${project.name} + $${project.hourlyRate}/hr + ${project.totalWorkedTime} + ${project.isCurrentlyWorking ? "Working" : "Inactive"} + + `; +} + +function createVATableRow(va, index) { + return ` + + + + ${va.isCurrentlyActive ? "Active" : "Inactive"} + + + ${va.name} + ${va.lastProject?.name || "-"} + ${va.projectsAssigned || 0} + ${va.currentSessionDuration || "No Record Found"} + + ${ + va.isCurrentlyActive + ? `Live` + : "-" + } + + + `; +} + + + setInterval(loadDashboardData, 30000); + +/* ------------------ Load Dashboard ------------------ */ + +async function loadDashboardData() { + const res = await fetchWithAuth(`${API_BASE}/dashboard`); + if (!res) return; + + const result = await res.json(); + if (!result.success) return; + + const { summary, data } = result; + + document.getElementById("activeVaCount").textContent = summary.activeVAs; + document.getElementById("totalVaCount").textContent = summary.totalVAs; + document.getElementById("totalProjects").textContent = summary.totalProjects; + document.getElementById("vaListCount").textContent = `${summary.totalVAs} VAs`; + + const allVAs = data.allVAs || []; + + if (allVAs.length === 0) { + vaTableBody.innerHTML = ""; + vaEmptyState.style.display = "block"; + return; + } + + vaEmptyState.style.display = "none"; + + /* store globally for modal */ + window.allVAs = allVAs; + + vaTableBody.innerHTML = allVAs + .map((va, index) => createVATableRow(va, index)) + .join(""); + + /* Row click handlers */ + document.querySelectorAll(".va-row").forEach(row => { + row.addEventListener("click", (e) => { + if (e.target.tagName === "A") return; // ignore Live click + const index = row.dataset.index; + openVAModal(window.allVAs[index]); + }); + }); +} + +/* ------------------ Init ------------------ */ + +document.addEventListener("DOMContentLoaded", loadDashboardData); diff --git a/public/js/pages/manage-tips.js b/public/js/pages/manage-tips.js new file mode 100644 index 0000000..0017a55 --- /dev/null +++ b/public/js/pages/manage-tips.js @@ -0,0 +1,248 @@ + + + // API base URL + const API_BASE = '/api/v1/client'; + + + + 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 = ``; + 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(); + + + + + + }); + + + + diff --git a/public/js/pages/profile.js b/public/js/pages/profile.js new file mode 100644 index 0000000..24b0ad1 --- /dev/null +++ b/public/js/pages/profile.js @@ -0,0 +1,195 @@ +// API base URL +const API_BASE = '/api/v1/client'; + + +// Format empty fields +function formatEmptyFields() { + const inputs = document.querySelectorAll('.form-input'); + inputs.forEach(input => { + if (!input.value.trim()) { + input.value = 'Not provided'; + input.classList.add('empty-field'); + } else { + input.classList.remove('empty-field'); + } + }); +} + +// Load profile data +async function loadProfileData() { + try { + const response = await fetchWithAuth(`${API_BASE}/profile`); + if (!response) return; + + const result = await response.json(); + if (result.success) { + const data = result.data; + + // Populate only personal and address information + const fieldsToShow = ['name', 'email', 'position', 'address', 'city', 'state', 'zip']; + + fieldsToShow.forEach(key => { + const element = document.getElementById(key); + if (element && data[key]) { + element.value = data[key]; + } + }); + + formatEmptyFields(); + + } else { + console.error('Failed to load profile data:', result.message); + alert('Failed to load profile data'); + } + } catch (error) { + console.error('Error loading profile data:', error); + alert('Error loading profile data'); + } +} + +// Password Modal Functions +function openPasswordModal() { + const modal = document.getElementById('passwordModal'); + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + // Clear form + document.getElementById('passwordForm').reset(); + clearErrorMessages(); +} + +function closePasswordModal() { + const modal = document.getElementById('passwordModal'); + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + + // Clear form and errors + document.getElementById('passwordForm').reset(); + clearErrorMessages(); +} + +function clearErrorMessages() { + document.getElementById('currentPasswordError').textContent = ''; + document.getElementById('newPasswordError').textContent = ''; + document.getElementById('confirmPasswordError').textContent = ''; +} + +// Change password +async function changePassword(event) { + event.preventDefault(); + + const currentPassword = document.getElementById('currentPassword').value; + const newPassword = document.getElementById('newPassword').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + // Reset errors + clearErrorMessages(); + + let hasError = false; + + if (!currentPassword) { + document.getElementById('currentPasswordError').textContent = 'Current password is required'; + hasError = true; + } + + if (!newPassword) { + document.getElementById('newPasswordError').textContent = 'New password is required'; + hasError = true; + } else if (newPassword.length < 6) { + document.getElementById('newPasswordError').textContent = 'Password must be at least 6 characters'; + hasError = true; + } + + if (!confirmPassword) { + document.getElementById('confirmPasswordError').textContent = 'Please confirm your password'; + hasError = true; + } else if (newPassword !== confirmPassword) { + document.getElementById('confirmPasswordError').textContent = 'Passwords do not match'; + hasError = true; + } + + if (hasError) return; + + try { + const saveBtn = document.getElementById('savePasswordBtn'); + saveBtn.disabled = true; + saveBtn.textContent = 'Changing Password...'; + + const response = await fetchWithAuth(`${API_BASE}/profile`, { + method: 'PATCH', + body: JSON.stringify({ + userDetails: { + currentPassword, + newPassword, + confirmPassword + } + }) + }); + + if (!response) { + saveBtn.disabled = false; + saveBtn.textContent = 'Change Password'; + return; + } + + const result = await response.json(); + if (result.success) { + alert('Password changed successfully!'); + closePasswordModal(); + } else { + alert(result.message || 'Failed to change password'); + } + } catch (error) { + console.error('Error changing password:', error); + alert('Error changing password'); + } finally { + const saveBtn = document.getElementById('savePasswordBtn'); + saveBtn.disabled = false; + saveBtn.textContent = 'Change Password'; + } +} + +// Event listeners +document.addEventListener('DOMContentLoaded', function () { + loadProfileData(); + + // Change password button + document.getElementById('changePasswordBtn').addEventListener('click', openPasswordModal); + + // Password form submission + document.getElementById('passwordForm').addEventListener('submit', changePassword); + + // Close modal when clicking outside + document.getElementById('passwordModal').addEventListener('click', function (e) { + if (e.target === this) { + closePasswordModal(); + } + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closePasswordModal(); + closeLogoutModal(); + } + }); + + 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); + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); +}); \ No newline at end of file diff --git a/public/js/pages/projects.js b/public/js/pages/projects.js new file mode 100644 index 0000000..d26b0b6 --- /dev/null +++ b/public/js/pages/projects.js @@ -0,0 +1,240 @@ +// API base URL +const API_BASE = '/api/v1/client'; + +// Modal management +const projectModal = document.getElementById('projectModal'); +const modalClose = document.getElementById('modalClose'); +const modalProjectName = document.getElementById('modalProjectName'); +const modalVATableBody = document.getElementById('modalVATableBody'); +const modalNoVAs = document.getElementById('modalNoVAs'); +const vaCountBadge = document.getElementById('vaCountBadge'); + + +function openProjectModal(project) { + // Set project name + modalProjectName.textContent = project.name || 'Unnamed Project'; + + // Set project info + const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : 'Not set'; + const dedicatedHours = project.dedicatedHours || 'Not set'; + const statusText = project.status === 'ACTIVE' ? 'Active' : 'Inactive'; + const statusClass = project.status === 'ACTIVE' ? 'active' : 'inactive'; + + // Set VA details + const assignedVAs = project.assignedVADetails || []; + vaCountBadge.textContent = assignedVAs.length; + + if (assignedVAs.length > 0) { + modalVATableBody.innerHTML = assignedVAs.map(va => createVATableRow(va)).join(''); + modalVATableBody.style.display = ''; + modalNoVAs.style.display = 'none'; + } else { + modalVATableBody.innerHTML = ''; + modalVATableBody.style.display = 'none'; + modalNoVAs.style.display = 'block'; + } + + // Show modal + projectModal.style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function createVATableRow(va) { + const statusClass = va.status === 'FULLTIME' ? 'active' : 'inactive'; + const statusText = va.status === 'FULLTIME' ? 'Full Time' : + va.status === 'PARTTIME' ? 'Part Time' : 'Inactive'; + + const activityClass = va.isActive ? 'active' : 'inactive'; + const activityText = va.isActive ? 'Currently Active' : 'Not Active'; + + // Get initials for avatar + const initials = (va.firstName?.charAt(0) || '') + (va.lastName?.charAt(0) || '') || 'VA'; + + return ` + + +
    +
    ${initials}
    +
    + ${va.firstName} ${va.lastName} + ${va.email} +
    +
    + + +
    + ${statusText} +
    + + +
    +
    + ${activityText} +
    + + + `; +} + +function closeProjectModal() { + projectModal.style.display = 'none'; + document.body.style.overflow = 'auto'; +} + +// Close modal when clicking outside +projectModal.addEventListener('click', (e) => { + if (e.target === projectModal) { + closeProjectModal(); + } +}); + +// Close modal with escape key +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && projectModal.style.display === 'block') { + closeProjectModal(); + } +}); + +function createProjectTableRow(project) { + const statusClass = project.status === 'ACTIVE' ? 'active' : 'inactive'; + const statusText = project.status === 'ACTIVE' ? 'Active' : 'Inactive'; + const hourlyRate = project.hourlyRate ? `$${project.hourlyRate}/hr` : '-'; + const dedicatedHours = project.dedicatedHours ? `${project.dedicatedHours}` : '-'; + + return ` + + +
    +
    ${project.name || 'Unnamed Project'}
    +
    + + + ${hourlyRate} + + + ${project.vaAssigned || 0} + + + ${dedicatedHours} + + + `; +} + +// Load dashboard data +async function loadDashboardData() { + try { + const response = await fetchWithAuth(`${API_BASE}/dashboard-projects`); + if (!response) return; + + const result = await response.json(); + if (result.success) { + const data = result.data; + const counts = result.counts; + + // Update counts + document.getElementById('projectsCount').textContent = `${counts.totalProjects || 0} Projects`; + + // Update Projects table + const projectsTableBody = document.getElementById('projectsTableBody'); + const projectsEmptyState = document.getElementById('projectsEmptyState'); + const projects = data.projects || []; + + if (projects.length > 0) { + projectsTableBody.innerHTML = projects.map(project => createProjectTableRow(project)).join(''); + projectsTableBody.style.display = ''; + projectsEmptyState.style.display = 'none'; + + // Add click event listeners to project rows + document.querySelectorAll('.project-row').forEach((row, index) => { + row.addEventListener('click', function () { + const project = projects[index]; + openProjectModal(project); + }); + }); + } else { + projectsTableBody.innerHTML = ''; + projectsTableBody.style.display = 'none'; + projectsEmptyState.style.display = 'block'; + } + + } else { + console.error('Failed to load dashboard data:', result.message); + showError('Failed to load dashboard data'); + } + + } catch (error) { + console.error('Error loading dashboard data:', error); + showError('Error loading dashboard data'); + } +} + +// Show error message +function showError(message) { + const projectsTableBody = document.getElementById('projectsTableBody'); + const projectsEmptyState = document.getElementById('projectsEmptyState'); + const vaTableBody = document.getElementById('vaTableBody'); + const vaEmptyState = document.getElementById('vaEmptyState'); + + // Hide tables + projectsTableBody.style.display = 'none'; + vaTableBody.style.display = 'none'; + + // Show error states + projectsEmptyState.innerHTML = ` +
    + +

    Error

    +

    ${message}

    +
    + `; + projectsEmptyState.style.display = 'block'; + + vaEmptyState.innerHTML = ` +
    + +

    Error

    +

    ${message}

    +
    + `; + vaEmptyState.style.display = 'block'; +} + + +// Optional: Auto-refresh data every 30 seconds +setInterval(loadDashboardData, 30000); + + + +document.addEventListener('DOMContentLoaded', function () { + // Load dashboard data + loadDashboardData(); + + // Mobile menu toggle + const menuToggle = document.querySelector('.mobile-menu-button'); + const sidebar = document.querySelector('.dashboard-sidebar'); + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('active'); + }); + + // Close modal with escape key + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + closeLogoutModal(); + } + }); + + 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); +}); + +modalClose.addEventListener('click', closeProjectModal); \ No newline at end of file diff --git a/src/controllers/client/dashboard/clientDashboardProjects.controller.js b/src/controllers/client/dashboard/clientDashboardProjects.controller.js index 9344dd2..30a9b7c 100644 --- a/src/controllers/client/dashboard/clientDashboardProjects.controller.js +++ b/src/controllers/client/dashboard/clientDashboardProjects.controller.js @@ -34,6 +34,8 @@ async function client_dashboard_projects(req, res) { // STEP 1: Get client ID from token const clientUser = await ClientModel.findOne({ "userDetails.email": clientEmail }); + + console.log("Line 38" , clientUser) if (!clientUser) { return res.status(404).json({ success: false, message: "Client not found" }); } @@ -58,7 +60,7 @@ async function client_dashboard_projects(req, res) { // STEP 3: Get all VAs assigned to these projects const allAssignedVAs = await VAModel.find({ - _id: { $in: allProjectVaIds } + _id: { $in: allProjectVaIds.vaId } }) .select('userDetails.firstName userDetails.lastName userDetails.email status') .lean() diff --git a/src/controllers/va/vaAuth/vaLogin.controller.js b/src/controllers/va/vaAuth/vaLogin.controller.js index cff77bc..7e00699 100644 --- a/src/controllers/va/vaAuth/vaLogin.controller.js +++ b/src/controllers/va/vaAuth/vaLogin.controller.js @@ -42,54 +42,54 @@ async function closePreviousSession(vaId, lastLog) { }); // console.log(`Created logout log: ${logoutLog._id}`); - try { - const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; - const sfDataSync = { - "_id": logoutLog._id, - "loggedType": "LOGOUT", - "vaId": vaId, - "vaEmail": lastLog.vaEmail, - "projectId": lastLog.projectId, - "time": logoutLog.time, - } + // try { + // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/va-project-log`; + // const sfDataSync = { + // "_id": logoutLog._id, + // "loggedType": "LOGOUT", + // "vaId": vaId, + // "vaEmail": lastLog.vaEmail, + // "projectId": lastLog.projectId, + // "time": logoutLog.time, + // } - 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(sfDataSync) - }); + // 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(sfDataSync) + // }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); - } + // 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); + // const result = await response.json(); + // // console.log("✅ Successfully synced VA data to Salesforce:", result); - return { - closedWorkTracks: openWorkTracks.length, - logoutLogId: logoutLog._id, - SyncSalesforce: result - }; - } 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; - } - - // return { + // return { // closedWorkTracks: openWorkTracks.length, // logoutLogId: logoutLog._id, + // SyncSalesforce: result // }; + // } 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; + // } + + return { + closedWorkTracks: openWorkTracks.length, + logoutLogId: logoutLog._id, + }; } catch (error) { console.error("Error closing previous session:", error); diff --git a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js index 35ba56d..45a6451 100644 --- a/src/controllers/va/vaLeaves/vaLeavesApply.controller.js +++ b/src/controllers/va/vaLeaves/vaLeavesApply.controller.js @@ -5,7 +5,7 @@ import { VAModel, LeavesModel } from "../../../models/vaModel.js"; async function va_leave_apply_function(req, res) { try { const accessToken = req.headers.token; - + if (!accessToken) { return res.status(401).json({ success: false, message: "Missing token" }); } @@ -25,9 +25,9 @@ async function va_leave_apply_function(req, res) { const { dateFrom, dateTo, leaveType, comment } = req.body; if (!dateFrom || !dateTo || !leaveType || !comment) { - return res.status(400).json({ - success: false, - message: "dateFrom, dateTo, leaveType, and comment are required" + return res.status(400).json({ + success: false, + message: "dateFrom, dateTo, leaveType, and comment are required" }); } @@ -36,49 +36,49 @@ async function va_leave_apply_function(req, res) { try { startDate = new Date(dateFrom); endDate = new Date(dateTo); - + // Set to start of day for accurate calculation startDate.setHours(0, 0, 0, 0); endDate.setHours(0, 0, 0, 0); - + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return res.status(400).json({ - success: false, - message: "Invalid date format. Use valid date format (YYYY-MM-DD)" + return res.status(400).json({ + success: false, + message: "Invalid date format. Use valid date format (YYYY-MM-DD)" }); } } catch (error) { - return res.status(400).json({ - success: false, - message: "Invalid date format" + return res.status(400).json({ + success: false, + message: "Invalid date format" }); } // Ensure start date is before or equal to end date if (startDate > endDate) { - return res.status(400).json({ - success: false, - message: "Start date must be before or equal to end date" + return res.status(400).json({ + success: false, + message: "Start date must be before or equal to end date" }); } // Ensure dates are not in the past const today = new Date(); today.setHours(0, 0, 0, 0); - + if (startDate < today) { - return res.status(400).json({ - success: false, - message: "Cannot apply for leave for past dates" + return res.status(400).json({ + success: false, + message: "Cannot apply for leave for past dates" }); } // Validate leave type const validLeaveTypes = ["SICK", "PTO"]; if (!validLeaveTypes.includes(leaveType)) { - return res.status(400).json({ - success: false, - message: "Invalid leave type. Must be SICK or PTO" + return res.status(400).json({ + success: false, + message: "Invalid leave type. Must be SICK or PTO" }); } @@ -87,9 +87,9 @@ async function va_leave_apply_function(req, res) { const totalDays = Math.floor(timeDifference / (1000 * 60 * 60 * 24)) + 1; // +1 to include both start and end dates if (totalDays <= 0) { - return res.status(400).json({ - success: false, - message: "Invalid date range" + return res.status(400).json({ + success: false, + message: "Invalid date range" }); } @@ -106,9 +106,9 @@ async function va_leave_apply_function(req, res) { }); if (existingLeave) { - return res.status(400).json({ - success: false, - message: "You already have a pending leave request that overlaps with these dates" + return res.status(400).json({ + success: false, + message: "You already have a pending leave request that overlaps with these dates" }); } @@ -125,9 +125,9 @@ async function va_leave_apply_function(req, res) { }); if (existingApprovedLeave) { - return res.status(400).json({ - success: false, - message: "You already have an approved leave that overlaps with these dates" + return res.status(400).json({ + success: false, + message: "You already have an approved leave that overlaps with these dates" }); } @@ -147,94 +147,95 @@ async function va_leave_apply_function(req, res) { await leaveRequest.save(); try { - // const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; - // const sfData = { - // "id": leaveRequest._id, - // "va": { - // "id": vaId - // }, - // "dateFrom": startDate.toISOString().split('T')[0], - // "dateTo": endDate.toISOString().split('T')[0], - // "totalDays": totalDays, - // "leaveType": leaveType, - // "comment": processedComment.plainText, // Send plain text to Salesforce - // "attachments": processedComment.extractedImages, // Send extracted images info - // "status": "PENDING", - // "rejectionReason": "" - // } + const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/leave-request`; + const sfData = { + "leaveId": leaveRequest._id.toString(), + // "va": { + // "id": vaId + // }, + "va": vaId.toString(), + "dateFrom": startDate.toISOString().split('T')[0], + "dateTo": endDate.toISOString().split('T')[0], + "totalDays": totalDays, + "leaveType": leaveType, + "comment": processedComment.plainText, // Send plain text to Salesforce + "attachments": processedComment.extractedImages, // Send extracted images info + "status": "PENDING", + "rejectionReason": "" + } - // 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({"data": sfData}) - // }); - - // if (!response.ok) { - // const errorText = await response.text(); - // throw new Error(`Salesforce API responded with status ${response.status}: ${errorText}`); - // } + 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({ "data": sfData }) + }); + + 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); - - return res.status(201).json({ - success: true, - message: "Leave request submitted successfully", - data: { - leaveId: leaveRequest._id, - dateFrom: startDate.toISOString().split('T')[0], - dateTo: endDate.toISOString().split('T')[0], - totalDays: totalDays, - leaveType, - comment: processedComment.processedHTML, - // attachments: processedComment.extractedImages, - status: "PENDING" - }, - // SyncSalesforce: result - }); + // console.log("✅ Successfully synced VA data to Salesforce:", result); + + // return res.status(201).json({ + // success: true, + // message: "Leave request submitted successfully", + // data: { + // leaveId: leaveRequest._id, + // dateFrom: startDate.toISOString().split('T')[0], + // dateTo: endDate.toISOString().split('T')[0], + // totalDays: totalDays, + // leaveType, + // comment: processedComment.processedHTML, + // // attachments: processedComment.extractedImages, + // status: "PENDING" + // }, + // // SyncSalesforce: result + // }); } 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; } - // return res.status(201).json({ - // success: true, - // message: "Leave request submitted successfully", - // data: { - // leaveId: leaveRequest._id, - // dateFrom: startDate.toISOString().split('T')[0], - // dateTo: endDate.toISOString().split('T')[0], - // totalDays: totalDays, - // leaveType, - // comment, - // status: "PENDING" - // } - // }); + return res.status(201).json({ + success: true, + message: "Leave request submitted successfully", + data: { + leaveId: leaveRequest._id, + dateFrom: startDate.toISOString().split('T')[0], + dateTo: endDate.toISOString().split('T')[0], + totalDays: totalDays, + leaveType, + comment, + status: "PENDING" + } + }); } catch (error) { console.error("Leave apply error:", error); - + if (error.name === 'JsonWebTokenError') { return res.status(401).json({ success: false, message: "Invalid token" }); } - + if (error.name === 'TokenExpiredError') { return res.status(401).json({ success: false, message: "Token expired" }); } - - return res.status(500).json({ - success: false, + + return res.status(500).json({ + success: false, message: "Internal Server Error", - error: error.message + error: error.message }); } } @@ -277,7 +278,7 @@ function extractPlainTextFromHTML(html) { .replace(/<[^>]*>/g, ' ') // Replace tags with spaces .replace(/\s+/g, ' ') // Collapse multiple spaces .trim(); - + // Decode common HTML entities plainText = plainText .replace(/ /g, ' ') @@ -304,7 +305,7 @@ function extractAndProcessBase64Images(html) { try { // Regex to find base64 images in HTML (various formats) const base64Regex = /]+src="data:image\/([a-zA-Z0-9+]+);base64,([^"]+)"[^>]*>/gi; - + let match; let imageIndex = 0; @@ -316,7 +317,7 @@ function extractAndProcessBase64Images(html) { // Create a unique filename const timestamp = Date.now(); const filename = `leave_image_${timestamp}_${imageIndex}.${imageType}`; - + // Calculate approximate file size const size = Math.floor((base64Data.length * 3) / 4); // Base64 size calculation @@ -332,7 +333,7 @@ function extractAndProcessBase64Images(html) { // Replace base64 with a reference in HTML const replacementHTML = `Leave evidence ${imageIndex + 1}`; - + processedHTML = processedHTML.replace(fullMatch, replacementHTML); imageIndex++; diff --git a/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js index f114406..bc4e6a7 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaManageTimesheetAdjustment.controller.js @@ -31,7 +31,7 @@ async function va_timesheet_manage_adjustment_request(req, res) { }); } - if (action === "approve") { + if (action === "APPROVE") { // Create new log entries in LoggedModel const loginLog = new LoggedModel({ loggedType: "LOGIN", @@ -78,7 +78,7 @@ async function va_timesheet_manage_adjustment_request(req, res) { } }); - } else if (action === "reject") { + } else if (action === "REJECT") { // Update adjustment record as rejected adjustment.status = "REJECTED"; adjustment.reviewedAt = new Date(); diff --git a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js index a252386..5ca4a5a 100644 --- a/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js +++ b/src/controllers/va/vaTimesheetAdjustment/vaTimesheetAdjustment.controller.js @@ -138,12 +138,12 @@ async function va_timesheet_adjustment_function(req, res) { try { const salesforceUrl = `${process.env.SF_DOCTRACKER_API_URL}services/apexrest/docva/api/timesheet-adjustment`; const sfData = { - "id": adjustmentRequest._id, + "id": adjustmentRequest._id.toString(), "va": { - "id": vaId + "id": vaId.toString() }, "project": { - "id": projectId + "id": projectId.toString() }, "date": adjustmentDate.toISOString().split('T')[0], "loginTime": loginDateTime.toISOString(), diff --git a/src/controllers/va/vaTips/vaTipsManage.controller.js b/src/controllers/va/vaTips/vaTipsManage.controller.js new file mode 100644 index 0000000..bcaf10b --- /dev/null +++ b/src/controllers/va/vaTips/vaTipsManage.controller.js @@ -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: 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 }; \ No newline at end of file diff --git a/src/models/vaModel.js b/src/models/vaModel.js index e6794e1..3b797f3 100644 --- a/src/models/vaModel.js +++ b/src/models/vaModel.js @@ -89,9 +89,16 @@ const projectSchema = new Schema({ }, vaAssigned: [ { - type: Types.ObjectId, - ref: "VA", - }, + _id: false, + vaId: { + type: Types.ObjectId, + ref: "VA", + }, + isFlexibleAllowed: { + type: Boolean, + default: true + } + } ], dedicatedHours: { type: String, @@ -110,7 +117,7 @@ const projectSchema = new Schema({ type: String, default: null }, - isFlexibleAllowedPt: { + isFlexibleAllowed: { type: Boolean, default: true }, @@ -189,7 +196,7 @@ const clientDetailsSchema = new Schema({ enum: ["CHECKING", "SAVINGS"], type: String, }, - + accountHolderName: { type: String, }, @@ -363,36 +370,36 @@ const vaWorkTrackSchema = new Schema({ ) const passwordResetSchema = new mongoose.Schema({ - email: { - type: String, - required: true, - lowercase: true, - index: true - }, - token: { - type: String, - required: true, - unique: true, - index: true - }, - userType: { - type: String, - required: true, - enum: ['va', 'client'], - index: true - }, - expiresAt: { - type: Date, - required: true, - index: true, - expires: 3600 // TTL index for 1 hour - }, - used: { - type: Boolean, - default: false - } + email: { + type: String, + required: true, + lowercase: true, + index: true + }, + token: { + type: String, + required: true, + unique: true, + index: true + }, + userType: { + type: String, + required: true, + enum: ['va', 'client'], + index: true + }, + expiresAt: { + type: Date, + required: true, + index: true, + expires: 3600 // TTL index for 1 hour + }, + used: { + type: Boolean, + default: false + } }, { - timestamps: true + timestamps: true }); passwordResetSchema.index({ email: 1, used: 1 }); @@ -511,6 +518,37 @@ const leavesSchema = new Schema({ } }, { 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 ---------------------- */ const VAModel = model("VA", vaSchema); const ClientModel = model("Client", clientSchema); @@ -520,5 +558,6 @@ const WorkTrackModel = model("TrackWork", vaWorkTrackSchema); const PasswordResetModel = model("PasswordReset", passwordResetSchema); const AdjustedHoursModel = model("AdjustedHours", adjustedHoursSchema); const LeavesModel = model("Leaves", leavesSchema); +const TipsModel = model("Tips", tipsSchema); -export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel }; \ No newline at end of file +export { VAModel, ClientModel, ProjectModel, LoggedModel, WorkTrackModel, PasswordResetModel, AdjustedHoursModel, LeavesModel, TipsModel }; \ No newline at end of file diff --git a/src/routes/v1/client.routes.js b/src/routes/v1/client.routes.js index 9e2c6e6..97f95e1 100644 --- a/src/routes/v1/client.routes.js +++ b/src/routes/v1/client.routes.js @@ -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_dashboard_projects } from "../../controllers/client/dashboard/clientDashboardProjects.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 clientRoutes.post("/register", va_register_client); // Register a new VA @@ -22,11 +23,14 @@ clientRoutes.post("/forgot-password", client_request_password_reset); clientRoutes.post("/verify-client-email", client_verify_email); 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/projects/:projectId", va_clients_projects_callback); // VA Clients Projects Callback (NC) - - // dashboard API's clientRoutes.get("/dashboard", clientAuthValidator, client_dashboard); // VA Clients Projects Callback clientRoutes.post("/dashboard/va-report", clientAuthValidator, client_dashboard_va_log); // VA details diff --git a/src/server.js b/src/server.js index 93c25a3..370bbc7 100644 --- a/src/server.js +++ b/src/server.js @@ -18,6 +18,7 @@ import apiRoutes from './routes/index.js' import { render_reset_form } from "./controllers/passwordReset.controllers.js"; +import expressLayouts from "express-ejs-layouts"; import jwt from "jsonwebtoken"; const app = express(); @@ -39,6 +40,11 @@ app.on('certificate-error', (event, webContents, url, error, certificate, callba }); app.set("view engine", "ejs"); + +app.use(expressLayouts); + +// DEFAULT layout for all pages +app.set("layout", "layouts/dashboard"); const PORT = process.env.PORT || 8000; app.use(cors({ @@ -118,9 +124,16 @@ app.get('/health', (req, res) => { res.status(201).send({ status: "Server is healthy" }); }) + + + app.get("/client/login", (req, res) => { - res.render("client_login") -}) + res.render("client/client_login", { + layout: "layouts/auth", // ⬅️ override layout + title: "DocVA | Login" + }); +}); + app.get("/client/forgot-password", (req, res) => { res.render("forgot_password") }) @@ -129,8 +142,11 @@ app.get("/client/forgot-password", (req, res) => { app.get('/client/dashboard', async (req, res) => { try { // const dashboardData = await dashboardService.getClientDashboard(req.user.id); - // res.render('client/dashboard-dashboard', dashboardData); - res.render('client/dashboard-dashboard'); + res.render('client/dashboard-dashboard', { + title: "Client Dashboard", + pageScript: "dashboard.js" + }); + // res.render('layouts/dashboard'); } catch (error) { console.error('Dashboard error:', error); res.status(500).render('client/dashboard-dashboard', { error: 'Failed to load dashboard' }); @@ -140,7 +156,11 @@ app.get('/client/dashboard', async (req, res) => { app.get('/client/virtual-assistants', async (req, res) => { try { // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-virtual-assistants'); + res.render('client/dashboard-virtual-assistants', { + title: "Virtual Assistants", + pageScript:"dashboard-virtual-assistants.js" + + }); // res.render('client/dashboard-virtual-assistants', vaData); } catch (error) { console.error('Virtual assistants error:', error); @@ -151,7 +171,11 @@ app.get('/client/virtual-assistants', async (req, res) => { app.get('/client/projects', async (req, res) => { try { // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-projects'); + res.render('client/dashboard-projects', { + title: "Projects", + pageScript:"projects.js" + + }); } catch (error) { console.error('Projects error:', error); res.status(500).render('client/dashboard-projects', { error: 'Failed to load projects' }); @@ -161,23 +185,25 @@ app.get('/client/projects', async (req, res) => { app.get('/client/profile', async (req, res) => { try { // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-profile'); + res.render('client/dashboard-profile',{ + title: "Profile", + pageScript:"profile.js" + + }); } catch (error) { console.error('Projects error:', error); res.status(500).render('client/dashboard-profile', { error: 'Failed to load profile' }); } }); -app.get('/client/virtual-assistants/manage-tips', async (req, res) => { - try { - // const vaData = await virtualAssistantService.getClientVAs(req.user.id); - res.render('client/dashboard-manage-va-tips'); - } catch (error) { - console.error('Projects error:', error); - res.status(500).render('client/dashboard-manage-va-tips', { error: 'Failed to load profile' }); - } +app.get('/client/virtual-assistants/manage-tips', (req, res) => { + res.render('client/dashboard-manage-va-tips', { + title: 'Manage VA Tips', + pageScript: 'manage-tips.js' + }); }); + app.get("/live/:projectId/:vaId", async (req, res) => { const { projectId, vaId } = req.params; @@ -400,6 +426,36 @@ io.on("connection", (socket) => { socket.to(vaSocketRespectToClient.get(`${clientId}-${vaId}-${projectId}-va`)).emit("appToCapture", { appTitle }); }); + socket.on("requestVisibleApps", async ({ token }) => { + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET_ACCESS_TOKEN); + const vaEmail = decoded.email; + + // Find VA in DB + const vaUser = await VAModel.findOne({ "userDetails.email": vaEmail }); + if (!vaUser) { + console.log("❌ Unauthorized VA"); + return; + } + + const vaId = vaUser._id.toString(); + console.log("🟢 Received Apps from VA:", vaId); + + // Broadcast image only to allowed clients + for (let [_, clientSocket] of io.sockets.sockets) { + if ( + clientSocket.data.role === "client" && + clientSocket.data.allowedVAs?.includes(vaId) + ) { + clientSocket.emit("captureAppsTracker"); + } + } + } catch (error) { + console.error("❌ Token verification failed:", err.message); + } + }); + socket.on("getVisibleApps", async ({ token, apps }) => { try { // Verify token @@ -431,9 +487,27 @@ io.on("connection", (socket) => { }); - socket.on('disconnect', () => { - console.log('a user Disconnected'); + // socket.on('disconnect', () => { + // console.log('a user Disconnected'); + // }); + + // When client leaves or closes tab + socket.on("disconnect", () => { + if (socket.data.role === "client") { + const { clientId, vaId, projectId } = socket.data; + + const vaSocketId = vaSocketRespectToClient.get( + `${clientId}-${vaId}-${projectId}-va` + ); + + if (vaSocketId) { + io.to(vaSocketId).emit("stopSendAppsDetails"); + } + } + + console.log("❌ Client disconnected"); }); + }); app.use((req, res) => { diff --git a/views/client_login.ejs b/views/client/client_login.ejs similarity index 100% rename from views/client_login.ejs rename to views/client/client_login.ejs diff --git a/views/client/dashboard-dashboard.ejs b/views/client/dashboard-dashboard.ejs index e637a04..7e1e9e4 100644 --- a/views/client/dashboard-dashboard.ejs +++ b/views/client/dashboard-dashboard.ejs @@ -1,557 +1,72 @@ - - - - - - DocVA Client | Dashboard - - - - -
    - -
    - -
    -
    - -
    -
    -
    -
    -

    Active VA

    -
    0
    -
    -
    - -
    -
    -
    -
    -
    -
    -

    Total VA's

    -
    0
    -
    -
    - -
    -
    -
    - -
    -
    -
    -

    Total Projects

    -
    0
    -
    -
    - -
    -
    -
    -
    - - -
    -
    -

    Virtual Assistants 0 VAs

    -
    -
    -
    - - - - - - - - - - - - - - -
    StatusVirtual AssistantActive ProjectTotal ProjectsToday Logged HoursAction
    -
    - -
    -
    -
    -
    -
    - - - - - - \ No newline at end of file + +
    +
    +

    Virtual Assistants 0 VAs

    +
    +
    +
    + + + + + + + + + + + + + + +
    StatusVirtual AssistantActive ProjectTotal ProjectsToday Logged HoursAction
    +
    + +
    +
    \ No newline at end of file diff --git a/views/client/dashboard-manage-va-tips.ejs b/views/client/dashboard-manage-va-tips.ejs index bc9580f..fa30e29 100644 --- a/views/client/dashboard-manage-va-tips.ejs +++ b/views/client/dashboard-manage-va-tips.ejs @@ -1,350 +1,59 @@ - - - - - - DocVA Client | Dashboard - - - - -
    - -
    - -
    -
    - -
    -
    -
    -
    -

    Unbilled Tips

    -
    $0
    -
    -
    - -
    -
    -
    -
    -
    -
    -

    Total Tips

    -
    $0
    -
    -
    - -
    -
    -
    -
    - - -
    -
    - Back - Tip Virtual Assistant -
    -
    -
    - - - - - - - - - - - - - - -
    VoidStatusVirtual Assistant NameDescriptionDate AddedAmount
    -
    - -
    -
    -
    -
    -
    - - - - - - \ No newline at end of file + +
    +
    + Back + Tip Virtual Assistant +
    +
    +
    + + + + + + + + + + + + + + +
    VoidStatusVirtual Assistant NameDescriptionDate AddedAmount
    +
    + +
    +
    \ No newline at end of file diff --git a/views/client/dashboard-profile.ejs b/views/client/dashboard-profile.ejs index 4355b94..934137b 100644 --- a/views/client/dashboard-profile.ejs +++ b/views/client/dashboard-profile.ejs @@ -1,478 +1,65 @@ - - - - - - DocVA Client | Profile - - - - - -
    - -
    - -
    -
    -
    -
    -

    Client Profile

    -
    - -
    - -
    -
    - -

    Personal Information

    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - -

    Address Information

    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    +
    +
    + +
    -
    -
    - - -